feat(file-source): add metadata field mapping configuration

Add new metadata mappings feature that allows users to map custom
metadata field names to standard task properties. This enables support
for different metadata naming conventions (e.g., mapping "proj" to
"project", "ctx" to "context").

- Add metadata mappings configuration UI with add/remove/toggle controls
- Include field validation to prevent duplicate target mappings
- Add MetadataMappingConfig type definition
- Update default settings and tests to include metadataMappings
- Integrate mapping support throughout FileSource system
- Add handleTaskEditInFile method to FluentActionHandlers
- Improve settings tab organization with better visual structure
This commit is contained in:
Quorafind 2025-10-28 12:46:12 +08:00
parent 35e86c37a5
commit f7456af12d
18 changed files with 892 additions and 399 deletions

View file

@ -71,6 +71,7 @@ describe('FileTaskManager - Frontmatter Title Protection', () => {
templates: { enabled: false, templatePaths: [], checkTemplateMetadata: false },
paths: { enabled: false, taskPaths: [], matchMode: 'prefix' }
},
metadataMappings: [],
fileTaskProperties: {
contentSource: 'title',
stripExtension: true,

View file

@ -1686,6 +1686,7 @@ export const DEFAULT_SETTINGS: TaskProgressBarSettings = {
matchMode: "prefix",
},
},
metadataMappings: [],
fileTaskProperties: {
contentSource: "filename",
stripExtension: true,

View file

@ -123,6 +123,13 @@ export class FluentActionHandlers extends Component {
}
}
/**
* Handle editing a task directly in its source file.
*/
async handleTaskEditInFile(task: Task): Promise<void> {
await this.editTask(task);
}
/**
* Toggle task completion status
*/

View file

@ -7,7 +7,10 @@
import { Setting, Notice } from "obsidian";
import type TaskProgressBarPlugin from "@/index";
import type { FileSourceConfiguration } from "@/types/file-source";
import type {
FileSourceConfiguration,
MetadataMappingConfig,
} from "@/types/file-source";
import { t } from "@/translations/helper";
import { ListConfigModal } from "@/components/ui/modals/ListConfigModal";
@ -21,13 +24,13 @@ export interface FileSourceSettingsOptions {
export function createFileSourceSettings(
containerEl: HTMLElement,
plugin: TaskProgressBarPlugin,
options: FileSourceSettingsOptions = {}
options: FileSourceSettingsOptions = {},
): void {
const config = plugin.settings?.fileSource;
if (!config) {
console.warn(
"[FileSourceSettings] Missing fileSource configuration on plugin settings"
"[FileSourceSettings] Missing fileSource configuration on plugin settings",
);
return;
}
@ -41,6 +44,11 @@ export function createFileSourceSettings(
// Recognition strategies section
createRecognitionStrategiesSection(containerEl, plugin, config);
// Metadata mappings only apply when metadata recognition is active
if (config.recognitionStrategies.metadata.enabled) {
createMetadataMappingsSection(containerEl, plugin);
}
// File task properties section
createFileTaskPropertiesSection(containerEl, plugin, config);
@ -61,7 +69,7 @@ export function createFileSourceSettings(
function createEnableToggle(
containerEl: HTMLElement,
plugin: TaskProgressBarPlugin,
config: FileSourceConfiguration
config: FileSourceConfiguration,
): void {
// Don't create duplicate header since we're now embedded in IndexSettingsTab
@ -69,8 +77,8 @@ function createEnableToggle(
.setName(t("Enable File Task"))
.setDesc(
t(
"Allow files to be recognized and treated as tasks based on their metadata, tags, or file paths. This provides advanced recognition strategies beyond simple metadata parsing."
)
"Allow files to be recognized and treated as tasks based on their metadata, tags, or file paths. This provides advanced recognition strategies beyond simple metadata parsing.",
),
)
.addToggle((toggle) =>
toggle.setValue(config.enabled).onChange(async (value) => {
@ -80,7 +88,7 @@ function createEnableToggle(
// Refresh the settings display
containerEl.empty();
createFileSourceSettings(containerEl, plugin);
})
}),
);
}
@ -90,28 +98,28 @@ function createEnableToggle(
function createRecognitionStrategiesSection(
containerEl: HTMLElement,
plugin: TaskProgressBarPlugin,
config: FileSourceConfiguration
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."
)
"Configure how files are recognized as tasks. At least one strategy must be enabled.",
),
);
// Metadata strategy
const metadataContainer = containerEl.createDiv(
"file-source-strategy-container"
"file-source-strategy-container",
);
new Setting(metadataContainer)
.setName(t("Metadata-based Recognition"))
.setDesc(
t(
"Recognize files as tasks if they have specific frontmatter fields"
)
"Recognize files as tasks if they have specific frontmatter fields",
),
)
.addToggle((toggle) =>
toggle
@ -123,7 +131,7 @@ function createRecognitionStrategiesSection(
// Refresh to show/hide fields
containerEl.empty();
createFileSourceSettings(containerEl, plugin);
})
}),
);
if (config.recognitionStrategies.metadata.enabled) {
@ -131,8 +139,8 @@ function createRecognitionStrategiesSection(
.setName(t("Task Fields"))
.setDesc(
t(
"Configure metadata fields that indicate a file should be treated as a task (e.g., dueDate, status, priority)"
)
"Configure metadata fields that indicate a file should be treated as a task (e.g., dueDate, status, priority)",
),
)
.addButton((button) => {
const getTaskFields = () => {
@ -151,7 +159,7 @@ function createRecognitionStrategiesSection(
interpolation: {
count: fields.length.toString(),
},
})
}),
);
}
};
@ -161,7 +169,7 @@ function createRecognitionStrategiesSection(
new ListConfigModal(plugin, {
title: t("Configure Task Fields"),
description: t(
"Add metadata fields that indicate a file should be treated as a task (e.g., dueDate, status, priority)"
"Add metadata fields that indicate a file should be treated as a task (e.g., dueDate, status, priority)",
),
placeholder: t("Enter metadata field name"),
values: getTaskFields(),
@ -172,9 +180,9 @@ function createRecognitionStrategiesSection(
updateButtonText();
new Notice(
t(
"Task fields updated. Rebuild the task index to apply to existing files."
"Task fields updated. Rebuild the task index to apply to existing files.",
),
6000
6000,
);
},
}).open();
@ -185,25 +193,25 @@ function createRecognitionStrategiesSection(
.setName(t("Require All Fields"))
.setDesc(
t(
"Require all specified fields to be present (otherwise any field is sufficient)"
)
"Require all specified fields to be present (otherwise any field is sufficient)",
),
)
.addToggle((toggle) =>
toggle
.setValue(
config.recognitionStrategies.metadata.requireAllFields
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"
"file-source-strategy-container",
);
new Setting(tagContainer)
@ -219,7 +227,7 @@ function createRecognitionStrategiesSection(
// Refresh to show/hide fields
containerEl.empty();
createFileSourceSettings(containerEl, plugin);
})
}),
);
if (config.recognitionStrategies.tags.enabled) {
@ -227,8 +235,8 @@ function createRecognitionStrategiesSection(
.setName(t("Task Tags"))
.setDesc(
t(
"Configure tags that indicate a file should be treated as a task (e.g., #task, #todo, #actionable)"
)
"Configure tags that indicate a file should be treated as a task (e.g., #task, #todo, #actionable)",
),
)
.addButton((button) => {
const getTaskTags = () => {
@ -245,7 +253,7 @@ function createRecognitionStrategiesSection(
interpolation: {
count: tags.length.toString(),
},
})
}),
);
}
};
@ -255,7 +263,7 @@ function createRecognitionStrategiesSection(
new ListConfigModal(plugin, {
title: t("Configure Task Tags"),
description: t(
"Add tags that indicate a file should be treated as a task (e.g., #task, #todo, #actionable)"
"Add tags that indicate a file should be treated as a task (e.g., #task, #todo, #actionable)",
),
placeholder: t("Enter tag (e.g., #task)"),
values: getTaskTags(),
@ -266,9 +274,9 @@ function createRecognitionStrategiesSection(
updateButtonText();
new Notice(
t(
"Task tags updated. Rebuild the task index to apply to existing files."
"Task tags updated. Rebuild the task index to apply to existing files.",
),
6000
6000,
);
},
}).open();
@ -289,14 +297,14 @@ function createRecognitionStrategiesSection(
plugin.settings.fileSource.recognitionStrategies.tags.matchMode =
value;
await plugin.saveSettings();
}
)
},
),
);
}
// Path strategy
const pathContainer = containerEl.createDiv(
"file-source-strategy-container"
"file-source-strategy-container",
);
new Setting(pathContainer)
@ -312,7 +320,7 @@ function createRecognitionStrategiesSection(
// Refresh settings interface
containerEl.empty();
createFileSourceSettings(containerEl, plugin);
})
}),
);
if (config.recognitionStrategies.paths.enabled) {
@ -320,8 +328,8 @@ function createRecognitionStrategiesSection(
.setName(t("Task Paths"))
.setDesc(
t(
"Configure paths that contain task files (e.g., Projects/, Tasks/2024/, Work/TODO/)"
)
"Configure paths that contain task files (e.g., Projects/, Tasks/2024/, Work/TODO/)",
),
)
.addButton((button) => {
const getTaskPaths = () => {
@ -338,7 +346,7 @@ function createRecognitionStrategiesSection(
interpolation: {
count: paths.length.toString(),
},
})
}),
);
}
};
@ -348,10 +356,10 @@ function createRecognitionStrategiesSection(
new ListConfigModal(plugin, {
title: t("Configure Task Paths"),
description: t(
"Add paths that contain task files (e.g., Projects/, Tasks/2024/, Work/TODO/)"
"Add paths that contain task files (e.g., Projects/, Tasks/2024/, Work/TODO/)",
),
placeholder: t(
"Enter path (e.g., Projects/, Tasks/**/*.md)"
"Enter path (e.g., Projects/, Tasks/**/*.md)",
),
values: getTaskPaths(),
onSave: async (values) => {
@ -361,9 +369,9 @@ function createRecognitionStrategiesSection(
updateButtonText();
new Notice(
t(
"Task paths updated. Rebuild the task index to apply to existing files."
"Task paths updated. Rebuild the task index to apply to existing files.",
),
6000
6000,
);
},
}).open();
@ -377,11 +385,11 @@ function createRecognitionStrategiesSection(
dropdown
.addOption(
"prefix",
t("Prefix (e.g., Projects/ matches Projects/App.md)")
t("Prefix (e.g., Projects/ matches Projects/App.md)"),
)
.addOption(
"glob",
t("Glob pattern (e.g., Projects/**/*.md)")
t("Glob pattern (e.g., Projects/**/*.md)"),
)
.addOption("regex", t("Regular expression (advanced)"))
.setValue(config.recognitionStrategies.paths.matchMode)
@ -392,7 +400,7 @@ function createRecognitionStrategiesSection(
// Refresh to show updated examples
containerEl.empty();
createFileSourceSettings(containerEl, plugin);
})
}),
);
// Add examples based on current mode
@ -444,13 +452,166 @@ function createRecognitionStrategiesSection(
}
}
/**
* Create metadata mappings section
*/
function createMetadataMappingsSection(
containerEl: HTMLElement,
plugin: TaskProgressBarPlugin,
): void {
new Setting(containerEl)
.setName(t("Metadata Mappings"))
.setDesc(t("Configure how metadata fields are mapped and transformed"))
.setHeading();
const metadataMappingsContainer = containerEl.createDiv({
cls: "file-source-metadata-mappings-container",
});
const ensureMappingsArray = () => {
if (
!Array.isArray(plugin.settings.fileSource.metadataMappings)
) {
plugin.settings.fileSource.metadataMappings = [];
}
};
const targetOptions = [
"status",
"project",
"context",
"area",
"priority",
"tags",
"startDate",
"scheduledDate",
"dueDate",
"completedDate",
"createdDate",
"recurrence",
];
const refreshMetadataMappings = () => {
metadataMappingsContainer.empty();
ensureMappingsArray();
const mappings =
plugin.settings.fileSource
.metadataMappings as MetadataMappingConfig[];
if (mappings.length === 0) {
metadataMappingsContainer.createDiv({
cls: "setting-item-description",
text: t("No metadata mappings configured yet."),
});
}
const usedTargetKeys = new Set(
mappings
.filter(
(mapping) => mapping.enabled && mapping.targetKey,
)
.map((mapping) => mapping.targetKey),
);
mappings.forEach((mapping, index) => {
const mappingRow = metadataMappingsContainer.createDiv({
cls: "file-source-metadata-mapping-row",
});
const availableTargetKeys = targetOptions.filter(
(key) =>
!usedTargetKeys.has(key) ||
key === mapping.targetKey,
);
new Setting(mappingRow)
.setName(`${t("Mapping")} ${index + 1}`)
.addText((text) => {
text
.setPlaceholder(t("Source key (e.g., proj)"))
.setValue(mapping.sourceKey)
.onChange(async (value) => {
ensureMappingsArray();
plugin.settings.fileSource.metadataMappings[
index
].sourceKey = value;
await plugin.saveSettings();
});
})
.addDropdown((dropdown) => {
dropdown.addOption("", t("Select target field"));
availableTargetKeys.forEach((key) => {
dropdown.addOption(key, key);
});
dropdown
.setValue(mapping.targetKey)
.onChange(async (value) => {
ensureMappingsArray();
plugin.settings.fileSource.metadataMappings[
index
].targetKey = value;
await plugin.saveSettings();
refreshMetadataMappings();
});
})
.addToggle((toggle) => {
toggle
.setTooltip(t("Enabled"))
.setValue(mapping.enabled)
.onChange(async (value) => {
ensureMappingsArray();
plugin.settings.fileSource.metadataMappings[
index
].enabled = value;
await plugin.saveSettings();
refreshMetadataMappings();
});
})
.addButton((button) => {
button
.setIcon("trash")
.setTooltip(t("Remove"))
.onClick(async () => {
ensureMappingsArray();
plugin.settings.fileSource.metadataMappings.splice(
index,
1,
);
await plugin.saveSettings();
refreshMetadataMappings();
});
});
});
new Setting(metadataMappingsContainer).addButton((button) =>
button
.setButtonText(t("Add Metadata Mapping"))
.setCta()
.onClick(async () => {
ensureMappingsArray();
plugin.settings.fileSource.metadataMappings.push({
sourceKey: "",
targetKey: "",
enabled: true,
});
await plugin.saveSettings();
refreshMetadataMappings();
}),
);
};
refreshMetadataMappings();
}
/**
* Create file task properties section
*/
function createFileTaskPropertiesSection(
containerEl: HTMLElement,
plugin: TaskProgressBarPlugin,
config: FileSourceConfiguration
config: FileSourceConfiguration,
): void {
new Setting(containerEl)
.setHeading()
@ -460,8 +621,8 @@ function createFileTaskPropertiesSection(
.setName(t("Task Title Source"))
.setDesc(
t(
"What should be used as the task title when a file becomes a task"
)
"What should be used as the task title when a file becomes a task",
),
)
.addDropdown((dropdown) =>
dropdown
@ -479,8 +640,8 @@ function createFileTaskPropertiesSection(
// Refresh to show/hide custom field input
containerEl.empty();
createFileSourceSettings(containerEl, plugin);
}
)
},
),
);
if (config.fileTaskProperties.contentSource === "custom") {
@ -491,13 +652,13 @@ function createFileTaskPropertiesSection(
text
.setPlaceholder("taskContent")
.setValue(
config.fileTaskProperties.customContentField || ""
config.fileTaskProperties.customContentField || "",
)
.onChange(async (value) => {
plugin.settings.fileSource.fileTaskProperties.customContentField =
value;
await plugin.saveSettings();
})
}),
);
}
@ -506,8 +667,8 @@ function createFileTaskPropertiesSection(
.setName(t("Strip File Extension"))
.setDesc(
t(
"Remove the .md extension from filename when using as task content"
)
"Remove the .md extension from filename when using as task content",
),
)
.addToggle((toggle) =>
toggle
@ -516,7 +677,7 @@ function createFileTaskPropertiesSection(
plugin.settings.fileSource.fileTaskProperties.stripExtension =
value;
await plugin.saveSettings();
})
}),
);
}
@ -524,8 +685,8 @@ function createFileTaskPropertiesSection(
.setName(t("Prefer Frontmatter Title"))
.setDesc(
t(
"When updating task content, prefer updating frontmatter title over renaming the file. This protects the original filename."
)
"When updating task content, prefer updating frontmatter title over renaming the file. This protects the original filename.",
),
)
.addToggle((toggle) =>
toggle
@ -534,7 +695,7 @@ function createFileTaskPropertiesSection(
plugin.settings.fileSource.fileTaskProperties.preferFrontmatterTitle =
value;
await plugin.saveSettings();
})
}),
);
new Setting(containerEl)
@ -548,7 +709,7 @@ function createFileTaskPropertiesSection(
plugin.settings.fileSource.fileTaskProperties.defaultStatus =
value;
await plugin.saveSettings();
})
}),
);
}
@ -558,22 +719,23 @@ function createFileTaskPropertiesSection(
function createStatusMappingSection(
containerEl: HTMLElement,
plugin: TaskProgressBarPlugin,
config: FileSourceConfiguration
config: FileSourceConfiguration,
): void {
new Setting(containerEl)
.setName(t("Status Mapping"))
.setHeading()
.setDesc(
t(
"Map between human-readable metadata values (e.g., 'completed') and task symbols (e.g., 'x')."
)
"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"
)
"Automatically convert between metadata status values and task symbols",
),
)
.addToggle((toggle) =>
toggle
@ -595,7 +757,7 @@ function createStatusMappingSection(
// Refresh to show/hide mapping options
containerEl.empty();
createFileSourceSettings(containerEl, plugin);
})
}),
);
if (config.statusMapping && config.statusMapping.enabled) {
@ -604,8 +766,8 @@ function createStatusMappingSection(
.setName(t("Sync from Task Status Settings"))
.setDesc(
t(
"Populate FileSource status mapping from your checkbox status configuration"
)
"Populate FileSource status mapping from your checkbox status configuration",
),
)
.addButton((button) =>
button
@ -619,7 +781,7 @@ function createStatusMappingSection(
// Delegate to orchestrator so in-memory FileSource mapping syncs immediately
orchestrator.updateSettings(plugin.settings);
new Notice(
t("FileSource status mapping synced")
t("FileSource status mapping synced"),
);
} else {
// Fallback: derive symbol->metadata mapping from Task Status settings
@ -630,7 +792,7 @@ function createStatusMappingSection(
>;
const symbolToType: Record<string, string> = {};
for (const [type, symbols] of Object.entries(
taskStatuses
taskStatuses,
)) {
const list = String(symbols)
.split("|")
@ -668,7 +830,7 @@ function createStatusMappingSection(
plugin.settings.fileSource.statusMapping.symbolToMetadata =
{};
for (const [symbol, type] of Object.entries(
symbolToType
symbolToType,
)) {
const md = typeToMetadata[type];
if (md)
@ -678,17 +840,17 @@ function createStatusMappingSection(
}
await plugin.saveSettings();
new Notice(
t("FileSource status mapping synced")
t("FileSource status mapping synced"),
);
}
} catch (e) {
console.error(
"Failed to sync FileSource status mapping:",
e
e,
);
new Notice(t("Failed to sync mapping"));
}
})
}),
);
new Setting(containerEl)
@ -701,7 +863,7 @@ function createStatusMappingSection(
plugin.settings.fileSource.statusMapping.caseSensitive =
value;
await plugin.saveSettings();
})
}),
);
new Setting(containerEl)
@ -714,12 +876,12 @@ function createStatusMappingSection(
plugin.settings.fileSource.statusMapping.autoDetect =
value;
await plugin.saveSettings();
})
}),
);
// Common status mappings display
const mappingsContainer = containerEl.createDiv(
"file-source-status-mappings"
"file-source-status-mappings",
);
mappingsContainer.createEl("h5", { text: t("Common Mappings") });
@ -760,7 +922,7 @@ function createStatusMappingSection(
const customMappingDesc = containerEl.createEl("p");
customMappingDesc.textContent = t(
"Add custom status mappings for your workflow."
"Add custom status mappings for your workflow.",
);
// Add mapping input
@ -790,7 +952,7 @@ function createStatusMappingSection(
text.setValue("");
}
}
})
}),
)
.addButton((button) =>
button
@ -799,17 +961,17 @@ function createStatusMappingSection(
.onClick(() => {
// Trigger the text change event with the current value
const textInput = containerEl.querySelector(
".setting-item:last-child input[type='text']"
".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"
"setting-item-description",
);
integrationNote.createEl("strong", { text: t("Note:") });
integrationNote.createEl("span", {
@ -817,34 +979,35 @@ function createStatusMappingSection(
" " +
t("Status mappings work with your Task Status Settings. ") +
t(
"The symbols defined here should match those in your checkbox status configuration."
"The symbols defined here should match those in your checkbox status configuration.",
),
});
}
}
/**
* @deprecated
* Create performance section
*/
function createPerformanceSection(
containerEl: HTMLElement,
plugin: TaskProgressBarPlugin,
config: FileSourceConfiguration
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 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();
// }),
// );
// Note: Worker Processing setting has been moved to IndexSettingsTab.ts > Performance Configuration section
// This avoids duplication and provides centralized control for all worker processing
@ -853,8 +1016,8 @@ function createPerformanceSection(
.setName(t("Cache TTL"))
.setDesc(
t(
"Time-to-live for cached results in milliseconds (default: 300000 = 5 minutes)"
)
"Time-to-live for cached results in milliseconds (default: 300000 = 5 minutes)",
),
)
.addText((text) =>
text
@ -864,7 +1027,7 @@ function createPerformanceSection(
const ttl = parseInt(value) || 300000;
plugin.settings.fileSource.performance.cacheTTL = ttl;
await plugin.saveSettings();
})
}),
);
}
@ -874,13 +1037,13 @@ function createPerformanceSection(
function createAdvancedSection(
containerEl: HTMLElement,
plugin: TaskProgressBarPlugin,
config: FileSourceConfiguration
config: FileSourceConfiguration,
): void {
new Setting(containerEl).setHeading().setName(t("Advanced"));
// new Setting(containerEl).setHeading().setName(t("Advanced"));
// Statistics section
const statsContainer = containerEl.createDiv("file-source-stats");
statsContainer.createEl("h5", { text: t("File Task Status") });
new Setting(statsContainer).setHeading().setName(t("Statistics"));
const statusText = config.enabled
? t("File Task is enabled and monitoring files")

View file

@ -9,6 +9,7 @@ import type {
FileFilterScopeControls,
TaskProgressBarSettings,
} from "@/common/setting-definition";
import { FileSourceConfiguration } from "@/types/file-source";
/**
* Renders the Index Settings tab that consolidates all indexing-related settings
@ -16,15 +17,15 @@ import type {
*/
export function renderIndexSettingsTab(
settingTab: TaskProgressBarSettingTab,
containerEl: HTMLElement
containerEl: HTMLElement,
) {
// Main heading
new Setting(containerEl)
.setName(t("Index & Task Source Configuration"))
.setDesc(
t(
"Configure how Task Genius discovers and indexes tasks from various sources including inline tasks, file metadata, and projects."
)
"Configure how Task Genius discovers and indexes tasks from various sources including inline tasks, file metadata, and projects.",
),
)
.setHeading();
@ -43,9 +44,9 @@ export function renderIndexSettingsTab(
// Show restart notice
new Notice(
t(
"Please restart Obsidian for the Indexer change to take effect."
"Please restart Obsidian for the Indexer change to take effect.",
),
8000
8000,
);
});
});
@ -68,9 +69,9 @@ export function renderIndexSettingsTab(
cls: "fluent-view-tab-icon",
});
setIcon(inlineTabIcon, "check-square");
inlineSwitcherButton.createSpan({cls: "fluent-view-tab-label"}).setText(
t("Checkbox Tasks")
);
inlineSwitcherButton
.createSpan({ cls: "fluent-view-tab-label" })
.setText(t("Checkbox Tasks"));
const fileSwitcherButton = switcherRow.createEl("button", {
cls: "fluent-view-tab clickable-icon",
@ -79,9 +80,9 @@ export function renderIndexSettingsTab(
cls: "fluent-view-tab-icon",
});
setIcon(fileTabIcon, "file-text");
fileSwitcherButton.createSpan({cls: "fluent-view-tab-label"}).setText(
t("File Tasks")
);
fileSwitcherButton
.createSpan({ cls: "fluent-view-tab-label" })
.setText(t("File Tasks"));
const sourcePanels = taskSourceWrapper.createDiv({
cls: "tg-index-task-source-panels",
@ -96,30 +97,29 @@ export function renderIndexSettingsTab(
// Inline task configuration content is rendered into inlineContainer
(() => {
let containerEl: HTMLElement = inlineContainer;
const inlineContentEnabled =
scopeControls.inlineTasksEnabled !== false;
const inlineContentEnabled = scopeControls.inlineTasksEnabled !== false;
new Setting(containerEl)
.setName(t("Enable checkbox tasks"))
.setDesc(
t(
"Index markdown checkbox tasks. Disable this if you only want to use file-based or external task sources."
)
"Index markdown checkbox tasks. Disable this if you only want to use file-based or external task sources.",
),
)
.addToggle((toggle) => {
toggle.setValue(inlineContentEnabled);
toggle.onChange((value) => {
const controls = ensureScopeControls(
settingTab.plugin.settings
settingTab.plugin.settings,
);
controls.inlineTasksEnabled = value;
settingTab.applySettingsUpdate();
if (!value) {
new Notice(
t(
"Checkbox task indexing disabled. The index will prune inline tasks shortly."
"Checkbox task indexing disabled. The index will prune inline tasks shortly.",
),
6000
6000,
);
}
updateInlineBodyState(value);
@ -144,8 +144,8 @@ export function renderIndexSettingsTab(
.setName(t("Prefer metadata format of task"))
.setDesc(
t(
"You can choose dataview format or tasks format, that will influence both index and save format."
)
"You can choose dataview format or tasks format, that will influence both index and save format.",
),
)
.addDropdown((dropdown) => {
dropdown
@ -153,9 +153,8 @@ export function renderIndexSettingsTab(
.addOption("tasks", "Tasks")
.setValue(settingTab.plugin.settings.preferMetadataFormat)
.onChange(async (value) => {
settingTab.plugin.settings.preferMetadataFormat = value as
| "dataview"
| "tasks";
settingTab.plugin.settings.preferMetadataFormat =
value as "dataview" | "tasks";
settingTab.applySettingsUpdate();
// Re-render the settings to update prefix configuration UI
setTimeout(() => {
@ -169,16 +168,18 @@ export function renderIndexSettingsTab(
.setName(t("Enable custom date formats"))
.setDesc(
t(
"Enable custom date format patterns for parsing dates. When enabled, the parser will try your custom formats before falling back to default formats."
)
"Enable custom date format patterns for parsing dates. When enabled, the parser will try your custom formats before falling back to default formats.",
),
)
.addToggle((toggle) => {
toggle
.setValue(
settingTab.plugin.settings.enableCustomDateFormats ?? false
settingTab.plugin.settings.enableCustomDateFormats ??
false,
)
.onChange((value) => {
settingTab.plugin.settings.enableCustomDateFormats = value;
settingTab.plugin.settings.enableCustomDateFormats =
value;
settingTab.applySettingsUpdate();
settingTab.display(); // Refresh to show/hide custom formats settings
});
@ -187,14 +188,12 @@ export function renderIndexSettingsTab(
if (settingTab.plugin.settings.enableCustomDateFormats) {
new Setting(containerEl)
.setName(t("Custom date formats"))
.setDesc(
t(
"Configure custom date format patterns."
)
)
.setDesc(t("Configure custom date format patterns."))
.addButton((button) => {
const getCustomFormats = () => {
return settingTab.plugin.settings.customDateFormats ?? [];
return (
settingTab.plugin.settings.customDateFormats ?? []
);
};
const updateButtonText = () => {
@ -207,7 +206,7 @@ export function renderIndexSettingsTab(
interpolation: {
count: formats.length.toString(),
},
})
}),
);
}
};
@ -217,10 +216,10 @@ export function renderIndexSettingsTab(
new ListConfigModal(settingTab.plugin, {
title: t("Configure Custom Date Formats"),
description: t(
"Add custom date format patterns. Date patterns: yyyy (4-digit year), yy (2-digit year), MM (2-digit month), M (1-2 digit month), dd (2-digit day), d (1-2 digit day), MMM (short month name), MMMM (full month name). Time patterns: HH (2-digit hour), mm (2-digit minute), ss (2-digit second). Use single quotes for literals (e.g., 'T' for ISO format)."
"Add custom date format patterns. Date patterns: yyyy (4-digit year), yy (2-digit year), MM (2-digit month), M (1-2 digit month), dd (2-digit day), d (1-2 digit day), MMM (short month name), MMMM (full month name). Time patterns: HH (2-digit hour), mm (2-digit minute), ss (2-digit second). Use single quotes for literals (e.g., 'T' for ISO format).",
),
placeholder: t(
"Enter date format (e.g., yyyy-MM-dd or yyyyMMdd_HHmmss)"
"Enter date format (e.g., yyyy-MM-dd or yyyyMMdd_HHmmss)",
),
values: getCustomFormats(),
onSave: (values) => {
@ -230,9 +229,9 @@ export function renderIndexSettingsTab(
updateButtonText();
new Notice(
t(
"Date formats updated. The parser will now recognize these custom formats."
"Date formats updated. The parser will now recognize these custom formats.",
),
6000
6000,
);
},
}).open();
@ -250,17 +249,17 @@ export function renderIndexSettingsTab(
});
const exampleFormats = [
{format: "yyyy-MM-dd", example: "2025-08-16"},
{format: "dd/MM/yyyy", example: "16/08/2025"},
{format: "MM-dd-yyyy", example: "08-16-2025"},
{format: "yyyy.MM.dd", example: "2025.08.16"},
{format: "yyyyMMdd", example: "20250816"},
{format: "yyyyMMdd_HHmmss", example: "20250816_144403"},
{format: "yyyyMMddHHmmss", example: "20250816144403"},
{format: "yyyy-MM-dd'T'HH:mm", example: "2025-08-16T14:44"},
{format: "dd MMM yyyy", example: "16 Aug 2025"},
{format: "MMM dd, yyyy", example: "Aug 16, 2025"},
{format: "yyyy年MM月dd日", example: "2025年08月16日"},
{ format: "yyyy-MM-dd", example: "2025-08-16" },
{ format: "dd/MM/yyyy", example: "16/08/2025" },
{ format: "MM-dd-yyyy", example: "08-16-2025" },
{ format: "yyyy.MM.dd", example: "2025.08.16" },
{ format: "yyyyMMdd", example: "20250816" },
{ format: "yyyyMMdd_HHmmss", example: "20250816_144403" },
{ format: "yyyyMMddHHmmss", example: "20250816144403" },
{ format: "yyyy-MM-dd'T'HH:mm", example: "2025-08-16T14:44" },
{ format: "dd MMM yyyy", example: "16 Aug 2025" },
{ format: "MMM dd, yyyy", example: "Aug 16, 2025" },
{ format: "yyyy年MM月dd日", example: "2025年08月16日" },
];
const table = examplesContainer.createEl("table", {
@ -268,13 +267,13 @@ export function renderIndexSettingsTab(
});
const headerRow = table.createEl("tr");
headerRow.createEl("th", {text: t("Format Pattern")});
headerRow.createEl("th", {text: t("Example")});
headerRow.createEl("th", { text: t("Format Pattern") });
headerRow.createEl("th", { text: t("Example") });
exampleFormats.forEach(({format, example}) => {
exampleFormats.forEach(({ format, example }) => {
const row = table.createEl("tr");
row.createEl("td", {text: format});
row.createEl("td", {text: example});
row.createEl("td", { text: format });
row.createEl("td", { text: example });
});
}
@ -288,23 +287,23 @@ export function renderIndexSettingsTab(
.setDesc(
isDataviewFormat
? t(
"Customize the prefix used for project tags in dataview format (e.g., 'project' for [project:: myproject]). Changes require reindexing."
)
"Customize the prefix used for project tags in dataview format (e.g., 'project' for [project:: myproject]). Changes require reindexing.",
)
: t(
"Customize the prefix used for project tags (e.g., 'project' for #project/myproject). Changes require reindexing."
)
"Customize the prefix used for project tags (e.g., 'project' for #project/myproject). Changes require reindexing.",
),
)
.addText((text) => {
text.setPlaceholder("project")
.setValue(
settingTab.plugin.settings.projectTagPrefix[
settingTab.plugin.settings.preferMetadataFormat
]
],
)
.onChange(async (value) => {
settingTab.plugin.settings.projectTagPrefix[
settingTab.plugin.settings.preferMetadataFormat
] = value || "project";
] = value || "project";
settingTab.applySettingsUpdate();
});
});
@ -315,23 +314,23 @@ export function renderIndexSettingsTab(
.setDesc(
isDataviewFormat
? t(
"Customize the prefix used for context tags in dataview format (e.g., 'context' for [context:: home]). Changes require reindexing."
)
"Customize the prefix used for context tags in dataview format (e.g., 'context' for [context:: home]). Changes require reindexing.",
)
: t(
"Customize the prefix used for context tags (e.g., '@home' for @home). Changes require reindexing."
)
"Customize the prefix used for context tags (e.g., '@home' for @home). Changes require reindexing.",
),
)
.addText((text) => {
text.setPlaceholder("context")
.setValue(
settingTab.plugin.settings.contextTagPrefix[
settingTab.plugin.settings.preferMetadataFormat
]
],
)
.onChange(async (value) => {
settingTab.plugin.settings.contextTagPrefix[
settingTab.plugin.settings.preferMetadataFormat
] = value || (isDataviewFormat ? "context" : "@");
] = value || (isDataviewFormat ? "context" : "@");
settingTab.applySettingsUpdate();
});
});
@ -340,12 +339,13 @@ export function renderIndexSettingsTab(
.setName(t("Ignore all tasks behind heading"))
.setDesc(
t(
"Configure headings to ignore. Tasks under these headings will be excluded from indexing."
)
"Configure headings to ignore. Tasks under these headings will be excluded from indexing.",
),
)
.addButton((button) => {
const getIgnoreHeadings = () => {
const value = settingTab.plugin.settings.ignoreHeading || "";
const value =
settingTab.plugin.settings.ignoreHeading || "";
return value
.split(",")
.map((h) => h.trim())
@ -362,7 +362,7 @@ export function renderIndexSettingsTab(
interpolation: {
count: headings.length.toString(),
},
})
}),
);
}
};
@ -372,7 +372,7 @@ export function renderIndexSettingsTab(
new ListConfigModal(settingTab.plugin, {
title: t("Configure Ignore Headings"),
description: t(
"Add headings to ignore. Tasks under these headings will be excluded from indexing. Examples: '## Project', '## Inbox', '# Archive'"
"Add headings to ignore. Tasks under these headings will be excluded from indexing. Examples: '## Project', '## Inbox', '# Archive'",
),
placeholder: t("Enter heading (e.g., ## Inbox)"),
values: getIgnoreHeadings(),
@ -383,9 +383,9 @@ export function renderIndexSettingsTab(
updateButtonText();
new Notice(
t(
"Heading filters updated. Rebuild the task index to apply to existing tasks."
"Heading filters updated. Rebuild the task index to apply to existing tasks.",
),
6000
6000,
);
},
}).open();
@ -396,8 +396,8 @@ export function renderIndexSettingsTab(
.setName(t("Focus all tasks behind heading"))
.setDesc(
t(
"Configure headings to focus on. Only tasks under these headings will be included in indexing."
)
"Configure headings to focus on. Only tasks under these headings will be included in indexing.",
),
)
.addButton((button) => {
const getFocusHeadings = () => {
@ -418,7 +418,7 @@ export function renderIndexSettingsTab(
interpolation: {
count: headings.length.toString(),
},
})
}),
);
}
};
@ -428,7 +428,7 @@ export function renderIndexSettingsTab(
new ListConfigModal(settingTab.plugin, {
title: t("Configure Focus Headings"),
description: t(
"Add headings to focus on. Only tasks under these headings will be included in indexing. Examples: '## Project', '## Inbox', '# Tasks'"
"Add headings to focus on. Only tasks under these headings will be included in indexing. Examples: '## Project', '## Inbox', '# Tasks'",
),
placeholder: t("Enter heading (e.g., ## Tasks)"),
values: getFocusHeadings(),
@ -439,9 +439,9 @@ export function renderIndexSettingsTab(
updateButtonText();
new Notice(
t(
"Heading filters updated. Rebuild the task index to apply to existing tasks."
"Heading filters updated. Rebuild the task index to apply to existing tasks.",
),
6000
6000,
);
},
}).open();
@ -452,11 +452,13 @@ export function renderIndexSettingsTab(
.setName(t("Use daily note path as date"))
.setDesc(
t(
"If enabled, the daily note path will be used as the date for tasks."
)
"If enabled, the daily note path will be used as the date for tasks.",
),
)
.addToggle((toggle) => {
toggle.setValue(settingTab.plugin.settings.useDailyNotePathAsDate);
toggle.setValue(
settingTab.plugin.settings.useDailyNotePathAsDate,
);
toggle.onChange((value) => {
settingTab.plugin.settings.useDailyNotePathAsDate = value;
settingTab.applySettingsUpdate();
@ -471,12 +473,12 @@ export function renderIndexSettingsTab(
const descFragment = document.createDocumentFragment();
descFragment.createEl("div", {
text: t(
"Task Genius will use moment.js and also this format to parse the daily note path."
"Task Genius will use moment.js and also this format to parse the daily note path.",
),
});
descFragment.createEl("div", {
text: t(
"You need to set `yyyy` instead of `YYYY` in the format string. And `dd` instead of `DD`."
"You need to set `yyyy` instead of `YYYY` in the format string. And `dd` instead of `DD`.",
),
});
new Setting(containerEl)
@ -497,7 +499,7 @@ export function renderIndexSettingsTab(
new SingleFolderSuggest(
settingTab.app,
text.inputEl,
settingTab.plugin
settingTab.plugin,
);
text.setValue(settingTab.plugin.settings.dailyNotePath);
text.onChange((value) => {
@ -510,8 +512,8 @@ export function renderIndexSettingsTab(
.setName(t("Use as date type"))
.setDesc(
t(
"You can choose due, start, or scheduled as the date type for tasks."
)
"You can choose due, start, or scheduled as the date type for tasks.",
),
)
.addDropdown((dropdown) => {
dropdown
@ -533,7 +535,7 @@ export function renderIndexSettingsTab(
new Setting(containerEl)
.setName(t("File Metadata Inheritance"))
.setDesc(
t("Configure how tasks inherit metadata from file frontmatter")
t("Configure how tasks inherit metadata from file frontmatter"),
)
.setHeading();
@ -541,31 +543,46 @@ export function renderIndexSettingsTab(
.setName(t("Enable file metadata inheritance"))
.setDesc(
t(
"Allow tasks to inherit metadata properties from their file's frontmatter"
)
"Allow tasks to inherit metadata properties from their file's frontmatter",
),
)
.addToggle((toggle) =>
toggle
.setValue(
settingTab.plugin.settings.fileMetadataInheritance.enabled
settingTab.plugin.settings.fileMetadataInheritance
.enabled,
)
.onChange(async (value) => {
settingTab.plugin.settings.fileMetadataInheritance.enabled = value;
settingTab.plugin.settings.fileMetadataInheritance.enabled =
value;
settingTab.applySettingsUpdate();
new ConfirmModal(settingTab.plugin, {
title: t("Reindex"),
message: t("This change affects how tasks inherit metadata from files. Rebuild the index now so changes take effect immediately?"),
message: t(
"This change affects how tasks inherit metadata from files. Rebuild the index now so changes take effect immediately?",
),
confirmText: t("Reindex"),
cancelText: t("Cancel"),
onConfirm: async (confirmed: boolean) => {
if (!confirmed) return;
try {
new Notice(t("Clearing task cache and rebuilding index..."));
await settingTab.plugin.dataflowOrchestrator?.onSettingsChange(["parser"]);
new Notice(t("Task index completely rebuilt"));
new Notice(
t(
"Clearing task cache and rebuilding index...",
),
);
await settingTab.plugin.dataflowOrchestrator?.onSettingsChange(
["parser"],
);
new Notice(
t("Task index completely rebuilt"),
);
} catch (error) {
console.error("Failed to reindex after inheritance setting change:", error);
console.error(
"Failed to reindex after inheritance setting change:",
error,
);
new Notice(t("Failed to reindex tasks"));
}
},
@ -574,7 +591,7 @@ export function renderIndexSettingsTab(
setTimeout(() => {
settingTab.display();
}, 200);
})
}),
);
if (settingTab.plugin.settings.fileMetadataInheritance.enabled) {
@ -582,74 +599,106 @@ export function renderIndexSettingsTab(
.setName(t("Inherit from frontmatter"))
.setDesc(
t(
"Tasks inherit metadata properties like priority, context, etc. from file frontmatter when not explicitly set on the task"
)
"Tasks inherit metadata properties like priority, context, etc. from file frontmatter when not explicitly set on the task",
),
)
.addToggle((toggle) =>
toggle
.setValue(
settingTab.plugin.settings.fileMetadataInheritance
.inheritFromFrontmatter
.inheritFromFrontmatter,
)
.onChange(async (value) => {
settingTab.plugin.settings.fileMetadataInheritance.inheritFromFrontmatter = value;
settingTab.plugin.settings.fileMetadataInheritance.inheritFromFrontmatter =
value;
settingTab.applySettingsUpdate();
new ConfirmModal(settingTab.plugin, {
title: t("Reindex"),
message: t("This change affects how tasks inherit metadata from files. Rebuild the index now so changes take effect immediately?"),
message: t(
"This change affects how tasks inherit metadata from files. Rebuild the index now so changes take effect immediately?",
),
confirmText: t("Reindex"),
cancelText: t("Cancel"),
onConfirm: async (confirmed: boolean) => {
if (!confirmed) return;
try {
new Notice(t("Clearing task cache and rebuilding index..."));
await settingTab.plugin.dataflowOrchestrator?.onSettingsChange(["parser"]);
new Notice(t("Task index completely rebuilt"));
new Notice(
t(
"Clearing task cache and rebuilding index...",
),
);
await settingTab.plugin.dataflowOrchestrator?.onSettingsChange(
["parser"],
);
new Notice(
t("Task index completely rebuilt"),
);
} catch (error) {
console.error("Failed to reindex after inheritance setting change:", error);
new Notice(t("Failed to reindex tasks"));
console.error(
"Failed to reindex after inheritance setting change:",
error,
);
new Notice(
t("Failed to reindex tasks"),
);
}
},
}).open();
})
}),
);
new Setting(containerEl)
.setName(t("Inherit from frontmatter for subtasks"))
.setDesc(
t(
"Allow subtasks to inherit metadata from file frontmatter. When disabled, only top-level tasks inherit file metadata"
)
"Allow subtasks to inherit metadata from file frontmatter. When disabled, only top-level tasks inherit file metadata",
),
)
.addToggle((toggle) =>
toggle
.setValue(
settingTab.plugin.settings.fileMetadataInheritance
.inheritFromFrontmatterForSubtasks
.inheritFromFrontmatterForSubtasks,
)
.onChange(async (value) => {
settingTab.plugin.settings.fileMetadataInheritance.inheritFromFrontmatterForSubtasks = value;
settingTab.plugin.settings.fileMetadataInheritance.inheritFromFrontmatterForSubtasks =
value;
settingTab.applySettingsUpdate();
new ConfirmModal(settingTab.plugin, {
title: t("Reindex"),
message: t("This change affects how tasks inherit metadata from files. Rebuild the index now so changes take effect immediately?"),
message: t(
"This change affects how tasks inherit metadata from files. Rebuild the index now so changes take effect immediately?",
),
confirmText: t("Reindex"),
cancelText: t("Cancel"),
onConfirm: async (confirmed: boolean) => {
if (!confirmed) return;
try {
new Notice(t("Clearing task cache and rebuilding index..."));
await settingTab.plugin.dataflowOrchestrator?.onSettingsChange(["parser"]);
new Notice(t("Task index completely rebuilt"));
new Notice(
t(
"Clearing task cache and rebuilding index...",
),
);
await settingTab.plugin.dataflowOrchestrator?.onSettingsChange(
["parser"],
);
new Notice(
t("Task index completely rebuilt"),
);
} catch (error) {
console.error("Failed to reindex after inheritance setting change:", error);
new Notice(t("Failed to reindex tasks"));
console.error(
"Failed to reindex after inheritance setting change:",
error,
);
new Notice(
t("Failed to reindex tasks"),
);
}
},
}).open();
})
}),
);
}
})();
@ -668,21 +717,21 @@ export function renderIndexSettingsTab(
.setName(t("Enable file tasks"))
.setDesc(
t(
"Allow Task Genius to recognize files as tasks using metadata, tags, or templates."
)
"Allow Task Genius to recognize files as tasks using metadata, tags, or templates.",
),
)
.addToggle((toggle) => {
toggle.setValue(fileTasksEnabled);
toggle.onChange((value) => {
const controls = ensureScopeControls(
settingTab.plugin.settings
settingTab.plugin.settings,
);
controls.fileTasksEnabled = value;
if (!settingTab.plugin.settings.fileSource) {
settingTab.plugin.settings.fileSource = {
enabled: value,
} as any;
} as FileSourceConfiguration;
} else {
settingTab.plugin.settings.fileSource.enabled = value;
}
@ -693,9 +742,9 @@ export function renderIndexSettingsTab(
if (!value) {
new Notice(
t(
"File task recognition disabled. Existing file tasks will be pruned shortly."
"File task recognition disabled. Existing file tasks will be pruned shortly.",
),
6000
6000,
);
}
});
@ -711,30 +760,25 @@ export function renderIndexSettingsTab(
const fileSettingsContainer = fileBodyEl;
new Setting(fileSettingsContainer)
.setName(t("File Task Configuration"))
.setDesc(
t(
"Configure how files can be recognized and treated as tasks with various strategies."
)
)
.setHeading();
const fileSourceContainerEl = fileSettingsContainer.createDiv(
"file-source-container"
"file-source-container",
);
const renderFileSourceSection = () => {
fileSourceContainerEl.empty();
if (settingTab.plugin.settings.fileSource?.enabled) {
createFileSourceSettings(fileSourceContainerEl, settingTab.plugin, {
showEnableToggle: false,
});
createFileSourceSettings(
fileSourceContainerEl,
settingTab.plugin,
{
showEnableToggle: false,
},
);
} else {
fileSourceContainerEl.createDiv({
cls: "setting-item-description",
text: t(
"File tasks are disabled. Enable them to configure recognition strategies."
"File tasks are disabled. Enable them to configure recognition strategies.",
),
});
}
@ -759,10 +803,10 @@ export function renderIndexSettingsTab(
setActiveTaskSourcePanel(initialPanel);
inlineSwitcherButton.addEventListener("click", () =>
setActiveTaskSourcePanel("inline")
setActiveTaskSourcePanel("inline"),
);
fileSwitcherButton.addEventListener("click", () =>
setActiveTaskSourcePanel("file")
setActiveTaskSourcePanel("file"),
);
// ========================================
@ -777,14 +821,14 @@ export function renderIndexSettingsTab(
.setName(t("Enable worker processing"))
.setDesc(
t(
"Use background worker for file parsing to improve performance. Recommended for large vaults."
)
"Use background worker for file parsing to improve performance. Recommended for large vaults.",
),
)
.addToggle((toggle) => {
// Use the new fileSource.performance.enableWorkerProcessing setting
toggle.setValue(
settingTab.plugin.settings.fileSource?.performance
?.enableWorkerProcessing ?? true
?.enableWorkerProcessing ?? true,
);
toggle.onChange((value) => {
// Ensure fileSource and performance objects exist
@ -832,8 +876,8 @@ export function renderIndexSettingsTab(
.setName(t("Rebuild index"))
.setDesc(
t(
"Force a complete rebuild of the task index. Use this if you notice missing or incorrect tasks."
)
"Force a complete rebuild of the task index. Use this if you notice missing or incorrect tasks.",
),
)
.setClass("mod-warning")
.addButton((button) => {
@ -841,7 +885,7 @@ export function renderIndexSettingsTab(
new ConfirmModal(settingTab.plugin, {
title: t("Reindex"),
message: t(
"Are you sure you want to force reindex all tasks?"
"Are you sure you want to force reindex all tasks?",
),
confirmText: t("Reindex"),
cancelText: t("Cancel"),
@ -849,7 +893,9 @@ export function renderIndexSettingsTab(
if (!confirmed) return;
try {
new Notice(
t("Clearing task cache and rebuilding index...")
t(
"Clearing task cache and rebuilding index...",
),
);
if (settingTab.plugin.dataflowOrchestrator) {
await settingTab.plugin.dataflowOrchestrator.rebuild();
@ -858,7 +904,7 @@ export function renderIndexSettingsTab(
} catch (error) {
console.error(
"Failed to force reindex tasks:",
error
error,
);
new Notice(t("Failed to force reindex tasks"));
}
@ -869,7 +915,7 @@ export function renderIndexSettingsTab(
}
function ensureScopeControls(
settings: TaskProgressBarSettings
settings: TaskProgressBarSettings,
): FileFilterScopeControls {
const scopeControls =
settings.fileFilter.scopeControls ??

View file

@ -1,8 +1,7 @@
import { App, Component } from "obsidian";
import { App, Component, debounce } from "obsidian";
import { Task } from "@/types/task";
import { TaskListItemComponent } from "./listItem";
import { TaskTreeItemComponent } from "./treeItem";
import { tasksToTree } from "@/utils/ui/tree-view-utils";
import { t } from "@/translations/helper";
import TaskProgressBarPlugin from "@/index";
@ -22,7 +21,7 @@ export class TaskListRendererComponent extends Component {
private containerEl: HTMLElement, // The HTML element to render tasks into
private plugin: TaskProgressBarPlugin,
private app: App,
private context: string // Context identifier (e.g., "projects", "review")
private context: string, // Context identifier (e.g., "projects", "review")
) {
super();
// Add this renderer as a child of the parent component
@ -43,45 +42,66 @@ export class TaskListRendererComponent extends Component {
isTreeView: boolean,
allTasksMap: Map<string, Task>, // Make it optional but required for tree view
emptyMessage: string = t("No tasks found."),
append: boolean = false
append: boolean = false,
) {
if (!append) {
this.cleanupComponents();
this.containerEl.empty();
}
this.debounceUpdateTasks(
tasks,
isTreeView,
allTasksMap,
emptyMessage,
append,
);
}
if (tasks.length === 0 && !append) {
this.renderEmptyState(emptyMessage);
return;
}
private debounceUpdateTasks = debounce(
(
tasks: Task[],
isTreeView: boolean,
allTasksMap: Map<string, Task>, // Make it optional but required for tree view
emptyMessage: string = t("No tasks found."),
append = false,
) => {
if (!append) {
this.cleanupComponents();
this.containerEl.empty();
}
// Store the map if provided (primarily for tree view)
if (allTasksMap) {
this.allTasksMap = allTasksMap;
} else if (isTreeView) {
// Fallback: if tree view is requested but no map provided, build it from section tasks
// This might lead to incomplete trees if parents are outside the section.
console.warn(
"TaskListRendererComponent: allTasksMap not provided for tree view. Tree may be incomplete."
);
this.allTasksMap = new Map(tasks.map((task) => [task.id, task]));
}
if (isTreeView) {
if (!this.allTasksMap || this.allTasksMap.size === 0) {
console.error(
"TaskListRendererComponent: Cannot render tree view without allTasksMap."
);
this.renderEmptyState(
"Error: Task data unavailable for tree view."
); // Show error
if (tasks.length === 0 && !append) {
this.renderEmptyState(emptyMessage);
return;
}
this.renderTreeView(tasks, this.allTasksMap); // Pass the map
} else {
this.renderListView(tasks);
}
}
// Store the map if provided (primarily for tree view)
if (allTasksMap) {
this.allTasksMap = allTasksMap;
} else if (isTreeView) {
// Fallback: if tree view is requested but no map provided, build it from section tasks
// This might lead to incomplete trees if parents are outside the section.
console.warn(
"TaskListRendererComponent: allTasksMap not provided for tree view. Tree may be incomplete.",
);
this.allTasksMap = new Map(
tasks.map((task) => [task.id, task]),
);
}
if (isTreeView) {
if (!this.allTasksMap || this.allTasksMap.size === 0) {
console.error(
"TaskListRendererComponent: Cannot render tree view without allTasksMap.",
);
this.renderEmptyState(
"Error: Task data unavailable for tree view.",
); // Show error
return;
}
this.renderTreeView(tasks, this.allTasksMap); // Pass the map
} else {
this.renderListView(tasks);
}
},
1000,
);
private renderListView(tasks: Task[]) {
const fragment = document.createDocumentFragment();
@ -90,7 +110,7 @@ export class TaskListRendererComponent extends Component {
task,
this.context,
this.app,
this.plugin
this.plugin,
);
// Set up event handlers
@ -109,13 +129,13 @@ export class TaskListRendererComponent extends Component {
"TaskListRendererComponent onTaskUpdate",
this.onTaskUpdate,
originalTask.content,
updatedTask.content
updatedTask.content,
);
if (this.onTaskUpdate) {
console.log(
"TaskListRendererComponent onTaskUpdate",
originalTask.content,
updatedTask.content
updatedTask.content,
);
await this.onTaskUpdate(originalTask, updatedTask);
}
@ -141,7 +161,7 @@ export class TaskListRendererComponent extends Component {
private renderTreeView(
sectionTasks: Task[],
allTasksMap: Map<string, Task>
allTasksMap: Map<string, Task>,
) {
const fragment = document.createDocumentFragment();
const sectionTaskIds = new Set(sectionTasks.map((t) => t.id)); // IDs of tasks belonging to this section
@ -151,7 +171,7 @@ export class TaskListRendererComponent extends Component {
const markSubtreeAsProcessed = (
rootTask: Task,
sectionTaskIds: Set<string>,
processedTaskIds: Set<string>
processedTaskIds: Set<string>,
) => {
if (sectionTaskIds.has(rootTask.id)) {
processedTaskIds.add(rootTask.id);
@ -164,7 +184,7 @@ export class TaskListRendererComponent extends Component {
markSubtreeAsProcessed(
childTask,
sectionTaskIds,
processedTaskIds
processedTaskIds,
);
}
});
@ -197,11 +217,11 @@ export class TaskListRendererComponent extends Component {
!sectionTaskIds.has(currentTask.metadata.parent)
) {
const parentTask = allTasksMap.get(
currentTask.metadata.parent
currentTask.metadata.parent,
);
if (!parentTask) {
console.warn(
`Parent task ${currentTask.metadata.parent} not found in allTasksMap.`
`Parent task ${currentTask.metadata.parent} not found in allTasksMap.`,
);
break;
}
@ -219,7 +239,7 @@ export class TaskListRendererComponent extends Component {
markSubtreeAsProcessed(
actualRoot,
sectionTaskIds,
processedTaskIds
processedTaskIds,
);
}
}
@ -238,7 +258,7 @@ export class TaskListRendererComponent extends Component {
directChildren.push(childTask);
} else {
console.warn(
`Child task ${childId} (parent: ${rootTask.id}) not found in allTasksMap.`
`Child task ${childId} (parent: ${rootTask.id}) not found in allTasksMap.`,
);
}
});
@ -253,7 +273,7 @@ export class TaskListRendererComponent extends Component {
0, // Root level is 0
directChildren, // Pass the actual children from the full map
allTasksMap, // Pass the full map for recursive building
this.plugin
this.plugin,
);
// Set up event handlers
@ -302,7 +322,7 @@ export class TaskListRendererComponent extends Component {
// Try updating in list view components
const listItemComponent = this.taskComponents.find(
(c) => c.getTask().id === updatedTask.id
(c) => c.getTask().id === updatedTask.id,
);
if (listItemComponent) {
listItemComponent.updateTask(updatedTask);

View file

@ -6,6 +6,7 @@ import {
Keymap,
Platform,
Workspace,
debounce,
} from "obsidian";
import { Task } from "@/types/task";
import { MarkdownRendererComponent } from "@/components/ui/renderers/MarkdownRenderer";
@ -212,7 +213,7 @@ export class TaskListItemComponent extends Component {
}
});
this.renderTaskItem();
this.debounceUpdateTaskItem();
this.updateSelectionVisualState();
}
@ -280,8 +281,6 @@ export class TaskListItemComponent extends Component {
// Priority indicator if available
if (this.task.metadata.priority) {
console.log("priority", this.task.metadata.priority);
// Convert priority to numeric value
let numericPriority: number;
if (typeof this.task.metadata.priority === "string") {
@ -929,9 +928,13 @@ export class TaskListItemComponent extends Component {
});
}
private debounceUpdateTaskItem = debounce(() => {
this.renderTaskItem();
}, 200);
private updateTaskDisplay() {
// Re-render the entire task item
this.renderTaskItem();
this.debounceUpdateTaskItem();
}
public getTask(): Task {

View file

@ -1377,12 +1377,9 @@ export class DataflowOrchestrator {
/**
* Process multiple files in batch using workers for parallel processing
*/
async processBatch(
files: TFile[],
useWorkers: boolean = true,
): Promise<void> {
async processBatch(files: TFile[], useWorkers = true): Promise<void> {
const updates = new Map<string, Task[]>();
let skippedCount = 0;
const skippedCount = 0;
// Decide whether to use workers based on batch size and configuration
const shouldUseWorkers = useWorkers && files.length > 5; // Use workers for batches > 5 files

View file

@ -108,6 +108,13 @@ export class Repository {
console.log(
`[Repository] Loaded ${this.icsEvents.length} ICS events from storage`
);
// Load file tasks from storage
console.log("[Repository] Loading file tasks from storage...");
this.fileTasks = await this.storage.loadFileTasks();
console.log(
`[Repository] Loaded ${this.fileTasks.size} file tasks from storage`
);
} catch (error) {
console.error("[Repository] Error during initialization:", error);
// Continue with empty index on error
@ -459,6 +466,9 @@ export class Repository {
async persist(): Promise<void> {
const snapshot = await this.indexer.getIndexSnapshot();
await this.storage.storeConsolidated(snapshot);
// Also persist file tasks
await this.storage.storeFileTasks(this.fileTasks);
}
/**

View file

@ -48,6 +48,7 @@ export const Keys = {
augmented: (path: string) => `tasks.augmented:${path}`,
consolidated: () => `consolidated:taskIndex`,
icsEvents: () => `ics:events`,
fileTasks: () => `file:tasks`,
meta: {
version: () => `meta:version`,
schemaVersion: () => `meta:schemaVersion`,
@ -286,6 +287,45 @@ export class Storage {
}
}
/**
* Store file tasks (from FileSource)
*/
async storeFileTasks(tasks: Map<string, Task>): Promise<void> {
const record = {
time: Date.now(),
version: this.currentVersion,
schema: this.schemaVersion,
data: Array.from(tasks.entries()),
};
await this.cache.storeFile(Keys.fileTasks(), record);
console.log(`[Storage] Stored ${tasks.size} file tasks`);
}
/**
* Load file tasks (from FileSource)
*/
async loadFileTasks(): Promise<Map<string, Task>> {
try {
const cached = await this.cache.loadFile<any>(Keys.fileTasks());
if (!cached || !cached.data) {
return new Map();
}
// Check version compatibility
if (!this.isVersionValid(cached.data)) {
await this.cache.removeFile(Keys.fileTasks());
return new Map();
}
const entries = cached.data.data || [];
return new Map(entries);
} catch (error) {
console.error("[Storage] Error loading file tasks:", error);
return new Map();
}
}
/**
* Load consolidated task index
*/

View file

@ -15,6 +15,7 @@ import type {
RecognitionStrategy,
PathRecognitionConfig,
TemplateRecognitionConfig,
MetadataMappingConfig,
} from "@/types/file-source";
import { Events, emit, Seq, on } from "../events/Events";
@ -809,9 +810,13 @@ export class FileSource {
): Partial<FileSourceTaskMetadata> {
const config = this.config.getConfig();
const frontmatter = fileCache?.frontmatter || {};
const resolvedFrontmatter = this.applyMetadataMappings(
frontmatter,
config.metadataMappings,
);
// Derive status from frontmatter and eagerly map textual metadata to a symbol
const rawStatus = frontmatter.status ?? "";
const rawStatus = resolvedFrontmatter.status ?? "";
const toSymbol = (val: string): string => {
if (!val) return config.fileTaskProperties.defaultStatus;
// Already a single-character mark
@ -855,20 +860,32 @@ export class FileSource {
// Extract standard task metadata
const metadata: Partial<FileSourceTaskMetadata> = {
dueDate: this.parseDate(frontmatter.dueDate || frontmatter.due),
dueDate: this.parseDate(
resolvedFrontmatter.dueDate ?? resolvedFrontmatter.due,
),
startDate: this.parseDate(
frontmatter.startDate || frontmatter.start,
resolvedFrontmatter.startDate ?? resolvedFrontmatter.start,
),
scheduledDate: this.parseDate(
frontmatter.scheduledDate || frontmatter.scheduled,
resolvedFrontmatter.scheduledDate ??
resolvedFrontmatter.scheduled,
),
completedDate: this.parseDate(resolvedFrontmatter.completedDate),
createdDate: this.parseDate(resolvedFrontmatter.createdDate),
recurrence:
typeof resolvedFrontmatter.recurrence === "string"
? resolvedFrontmatter.recurrence
: undefined,
priority:
frontmatter.priority ||
this.parsePriority(resolvedFrontmatter.priority) ??
config.fileTaskProperties.defaultPriority,
project: frontmatter.project,
context: frontmatter.context,
area: frontmatter.area,
tags: fileCache?.tags?.map((tag) => tag.tag) || [],
project: resolvedFrontmatter.project,
context: resolvedFrontmatter.context,
area: resolvedFrontmatter.area,
tags: this.mergeTags(
fileCache?.tags?.map((tag) => tag.tag) || [],
resolvedFrontmatter.tags,
),
status: status,
children: [],
};
@ -891,6 +908,84 @@ export class FileSource {
return this.config.mapSymbolToMetadata(symbol);
}
/**
* Apply configured metadata mappings to normalize frontmatter keys
*/
private applyMetadataMappings(
frontmatter: Record<string, any>,
mappings: MetadataMappingConfig[],
): Record<string, any> {
if (!Array.isArray(mappings) || mappings.length === 0) {
return { ...frontmatter };
}
const result = { ...frontmatter };
for (const mapping of mappings) {
if (!mapping?.enabled) continue;
const sourceKey = mapping.sourceKey;
const targetKey = mapping.targetKey;
if (!sourceKey || !targetKey) continue;
const value = frontmatter[sourceKey];
if (value !== undefined) {
result[targetKey] = value;
}
}
return result;
}
/**
* Merge vault tags with mapped frontmatter tag values
*/
private mergeTags(existing: string[], extra: unknown): string[] {
const tagSet = new Set(existing);
if (Array.isArray(extra)) {
for (const value of extra) {
if (typeof value !== "string") continue;
const normalized = this.normalizeTag(value);
if (normalized) tagSet.add(normalized);
}
} else if (typeof extra === "string") {
const segments = extra
.split(/[,\s]+/)
.map((segment) => this.normalizeTag(segment))
.filter((segment): segment is string => Boolean(segment));
for (const segment of segments) {
tagSet.add(segment);
}
}
return Array.from(tagSet);
}
private normalizeTag(value: string): string | null {
const trimmed = value.trim();
if (!trimmed) return null;
return trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
}
private parsePriority(value: unknown): number | undefined {
if (value === undefined || value === null) {
return undefined;
}
if (typeof value === "number" && !Number.isNaN(value)) {
return value;
}
if (typeof value === "string") {
const trimmed = value.trim();
if (!trimmed) return undefined;
const parsed = Number(trimmed);
if (!Number.isNaN(parsed)) {
return parsed;
}
}
return undefined;
}
/**
* Parse date from various formats
*/

View file

@ -14,7 +14,8 @@ import type {
FileTaskPropertiesConfig,
RelationshipsConfig,
PerformanceConfig,
StatusMappingConfig
StatusMappingConfig,
MetadataMappingConfig
} from "../../types/file-source";
/** Default configuration for metadata-based recognition */
@ -139,6 +140,9 @@ export const DEFAULT_STATUS_MAPPING_CONFIG: StatusMappingConfig = {
caseSensitive: false
};
/** Default metadata mappings */
export const DEFAULT_METADATA_MAPPINGS: MetadataMappingConfig[] = [];
/** Complete default FileSource configuration */
export const DEFAULT_FILE_SOURCE_CONFIG: FileSourceConfiguration = {
enabled: false, // Disabled by default for backward compatibility
@ -148,6 +152,7 @@ export const DEFAULT_FILE_SOURCE_CONFIG: FileSourceConfiguration = {
templates: DEFAULT_TEMPLATE_CONFIG,
paths: DEFAULT_PATH_CONFIG
},
metadataMappings: [...DEFAULT_METADATA_MAPPINGS],
fileTaskProperties: DEFAULT_FILE_TASK_PROPERTIES,
relationships: DEFAULT_RELATIONSHIPS_CONFIG,
performance: DEFAULT_PERFORMANCE_CONFIG,
@ -177,8 +182,8 @@ export class FileSourceConfig {
*/
updateConfig(updates: Partial<FileSourceConfiguration>): void {
const newConfig = this.mergeWithDefaults(updates);
const hasChanged = JSON.stringify(newConfig) !== JSON.stringify(this.config);
const hasChanged = !this.deepEqual(newConfig, this.config);
if (hasChanged) {
this.config = newConfig;
this.notifyListeners();
@ -279,6 +284,26 @@ export class FileSourceConfig {
}
}
if (config.metadataMappings) {
config.metadataMappings.forEach((mapping, index) => {
const sourceKey =
typeof mapping?.sourceKey === "string"
? mapping.sourceKey.trim()
: "";
const targetKey =
typeof mapping?.targetKey === "string"
? mapping.targetKey.trim()
: "";
if (!sourceKey) {
errors.push(`Metadata mapping ${index + 1} requires a source key`);
}
if (!targetKey) {
errors.push(`Metadata mapping ${index + 1} requires a target key`);
}
});
}
// Validate status mapping config
if (config.statusMapping?.enabled) {
const mapping = config.statusMapping;
@ -304,40 +329,114 @@ export class FileSourceConfig {
recognitionStrategies: {
metadata: {
...DEFAULT_METADATA_CONFIG,
...partial.recognitionStrategies?.metadata
...partial.recognitionStrategies?.metadata,
},
tags: {
...DEFAULT_TAG_CONFIG,
...partial.recognitionStrategies?.tags
...partial.recognitionStrategies?.tags,
},
templates: {
...DEFAULT_TEMPLATE_CONFIG,
...partial.recognitionStrategies?.templates
...partial.recognitionStrategies?.templates,
},
paths: {
...DEFAULT_PATH_CONFIG,
...partial.recognitionStrategies?.paths
}
...partial.recognitionStrategies?.paths,
},
},
metadataMappings: this.normalizeMetadataMappings(
partial.metadataMappings ?? DEFAULT_METADATA_MAPPINGS,
),
fileTaskProperties: {
...DEFAULT_FILE_TASK_PROPERTIES,
...partial.fileTaskProperties
...partial.fileTaskProperties,
},
relationships: {
...DEFAULT_RELATIONSHIPS_CONFIG,
...partial.relationships
...partial.relationships,
},
performance: {
...DEFAULT_PERFORMANCE_CONFIG,
...partial.performance
...partial.performance,
},
statusMapping: {
...DEFAULT_STATUS_MAPPING_CONFIG,
...partial.statusMapping
}
...partial.statusMapping,
},
};
}
/**
* Normalize metadata mappings: trim keys, drop invalid entries
*/
private normalizeMetadataMappings(
mappings?: MetadataMappingConfig[] | null,
): MetadataMappingConfig[] {
if (!Array.isArray(mappings)) {
return [];
}
const normalized: MetadataMappingConfig[] = [];
for (const mapping of mappings) {
if (!mapping) continue;
const sourceKey =
typeof mapping.sourceKey === "string" ? mapping.sourceKey.trim() : "";
const targetKey =
typeof mapping.targetKey === "string" ? mapping.targetKey.trim() : "";
if (!sourceKey || !targetKey) {
continue;
}
normalized.push({
sourceKey,
targetKey,
enabled: mapping.enabled !== false,
});
}
return normalized;
}
/**
* Deep equality check for configuration objects
* @param obj1 First object to compare
* @param obj2 Second object to compare
* @returns True if objects are deeply equal
*/
private deepEqual(obj1: any, obj2: any): boolean {
// Handle primitive types and null
if (obj1 === obj2) return true;
if (obj1 == null || obj2 == null) return false;
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return false;
// Handle arrays
if (Array.isArray(obj1) && Array.isArray(obj2)) {
if (obj1.length !== obj2.length) return false;
for (let i = 0; i < obj1.length; i++) {
if (!this.deepEqual(obj1[i], obj2[i])) return false;
}
return true;
}
// One is array, the other is not
if (Array.isArray(obj1) !== Array.isArray(obj2)) return false;
// Handle objects
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (const key of keys1) {
if (!keys2.includes(key)) return false;
if (!this.deepEqual(obj1[key], obj2[key])) return false;
}
return true;
}
/**
* Notify all listeners of configuration changes
*/
@ -549,4 +648,4 @@ export class FileSourceConfig {
return {};
}
}
}
}

View file

@ -915,67 +915,67 @@ export default class TaskProgressBarPlugin extends Plugin {
});
if (this.settings.enableIndexer) {
// Add command to refresh the task index
this.addCommand({
id: "refresh-task-index",
name: t("Refresh task index"),
callback: async () => {
try {
new Notice(t("Refreshing task index..."));
// // Add command to refresh the task index
// this.addCommand({
// id: "refresh-task-index",
// name: t("Refresh task index"),
// callback: async () => {
// try {
// new Notice(t("Refreshing task index..."));
// Check if dataflow is enabled
if (
this.settings?.enableIndexer &&
this.dataflowOrchestrator
) {
// Use dataflow orchestrator for refresh
console.log(
"[Command] Refreshing task index via dataflow",
);
// // Check if dataflow is enabled
// if (
// this.settings?.enableIndexer &&
// this.dataflowOrchestrator
// ) {
// // Use dataflow orchestrator for refresh
// console.log(
// "[Command] Refreshing task index via dataflow",
// );
// Re-scan all files to refresh the index
const files = this.app.vault.getMarkdownFiles();
const canvasFiles = this.app.vault
.getFiles()
.filter((f) => f.extension === "canvas");
const allFiles = [...files, ...canvasFiles];
// // Re-scan all files to refresh the index
// const files = this.app.vault.getMarkdownFiles();
// const canvasFiles = this.app.vault
// .getFiles()
// .filter((f) => f.extension === "canvas");
// const allFiles = [...files, ...canvasFiles];
// Process files in batches
const batchSize = 50;
for (
let i = 0;
i < allFiles.length;
i += batchSize
) {
const batch = allFiles.slice(i, i + batchSize);
await Promise.all(
batch.map((file) =>
(
this.dataflowOrchestrator as any
).processFileImmediate(file),
),
);
}
// // Process files in batches
// const batchSize = 50;
// for (
// let i = 0;
// i < allFiles.length;
// i += batchSize
// ) {
// const batch = allFiles.slice(i, i + batchSize);
// await Promise.all(
// batch.map((file) =>
// (
// this.dataflowOrchestrator as any
// ).processFileImmediate(file),
// ),
// );
// }
// Refresh ICS events if available
const icsSource = (this.dataflowOrchestrator as any)
.icsSource;
if (icsSource) {
await icsSource.refresh();
}
}
// else {
// // Use legacy task manager
// await this.taskManager.initialize();
// }
// // Refresh ICS events if available
// const icsSource = (this.dataflowOrchestrator as any)
// .icsSource;
// if (icsSource) {
// await icsSource.refresh();
// }
// }
// // else {
// // // Use legacy task manager
// // await this.taskManager.initialize();
// // }
new Notice(t("Task index refreshed"));
} catch (error) {
console.error("Failed to refresh task index:", error);
new Notice(t("Failed to refresh task index"));
}
},
});
// new Notice(t("Task index refreshed"));
// } catch (error) {
// console.error("Failed to refresh task index:", error);
// new Notice(t("Failed to refresh task index"));
// }
// },
// });
// Add command to force reindex all tasks by clearing cache
this.addCommand({
@ -1825,10 +1825,7 @@ export default class TaskProgressBarPlugin extends Plugin {
await this.saveSettings();
}
} catch (error) {
console.error(
"[TG] Failed to check migration onboarding:",
error,
);
console.error("[TG] Failed to check migration onboarding:", error);
}
}

View file

@ -818,7 +818,7 @@ export class FluentTaskView extends ItemView {
this.actionHandlers.toggleTaskCompletion(task);
},
onTaskEdit: (task) => {
this.actionHandlers.handleTaskSelection(task);
void this.actionHandlers.handleTaskEditInFile(task);
},
onTaskUpdate: async (originalTask, updatedTask) => {
await this.actionHandlers.handleTaskUpdate(

View file

@ -929,7 +929,7 @@ button.fluent-new-task-btn:hover {
}
@container (width < 800px) {
button.fluent-view-tab.clickable-icon span {
.tg-fluent-main-container button.fluent-view-tab.clickable-icon span {
display: none;
}
}

View file

@ -82,6 +82,16 @@ export interface RecognitionStrategiesConfig {
paths: PathRecognitionConfig;
}
/** Metadata mapping configuration for normalizing frontmatter keys */
export interface MetadataMappingConfig {
/** Source frontmatter key */
sourceKey: string;
/** Target standard metadata field */
targetKey: string;
/** Whether this mapping is active */
enabled: boolean;
}
/** File task properties configuration */
export interface FileTaskPropertiesConfig {
/** Default task content source */
@ -147,6 +157,9 @@ export interface FileSourceConfiguration {
/** Recognition strategies */
recognitionStrategies: RecognitionStrategiesConfig;
/** Metadata mappings for normalizing custom frontmatter fields */
metadataMappings: MetadataMappingConfig[];
/** File task properties */
fileTaskProperties: FileTaskPropertiesConfig;
@ -212,4 +225,4 @@ export interface FileTaskCache {
childTaskIds: Set<string>;
/** Last updated timestamp */
lastUpdated: number;
}
}

View file

@ -268,6 +268,7 @@ function createDefaultFileSourceConfig(): FileSourceConfiguration {
matchMode: "prefix"
}
},
metadataMappings: [],
fileTaskProperties: {
contentSource: "filename",
stripExtension: true,

File diff suppressed because one or more lines are too long