mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
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:
parent
35e86c37a5
commit
f7456af12d
18 changed files with 892 additions and 399 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1686,6 +1686,7 @@ export const DEFAULT_SETTINGS: TaskProgressBarSettings = {
|
|||
matchMode: "prefix",
|
||||
},
|
||||
},
|
||||
metadataMappings: [],
|
||||
fileTaskProperties: {
|
||||
contentSource: "filename",
|
||||
stripExtension: true,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 ??
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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 {};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
117
src/index.ts
117
src/index.ts
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
15
src/types/file-source.d.ts
vendored
15
src/types/file-source.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue