fix(project): restore project config settings synchronization

Fixes project configuration not being applied from settings by:
- Restoring project options update in DataflowOrchestrator settings handler
- Passing full project config options when initializing ProjectConfigManager
- Ensuring detection methods and all config flags are properly propagated
This commit is contained in:
Quorafind 2025-09-07 22:51:07 +08:00
parent 39bd038dd3
commit 42d67c630d
3 changed files with 215 additions and 56 deletions

View file

@ -826,6 +826,36 @@ export class DataflowOrchestrator {
);
}
// Update ProjectResolver / ProjectConfigManager options from settings
try {
const pc = settings?.projectConfig;
if (pc) {
this.updateProjectOptions({
configFileName: pc?.configFile?.fileName || "project.md",
searchRecursively:
pc?.configFile?.searchRecursively ?? true,
metadataKey: pc?.metadataConfig?.metadataKey || "project",
pathMappings: pc?.pathMappings || [],
metadataMappings: pc?.metadataMappings || [],
defaultProjectNaming: pc?.defaultProjectNaming || {
strategy: "filename",
stripExtension: true,
enabled: false,
},
enhancedProjectEnabled: pc?.enableEnhancedProject ?? false,
metadataConfigEnabled: pc?.metadataConfig?.enabled ?? false,
configFileEnabled: pc?.configFile?.enabled ?? false,
detectionMethods:
pc?.metadataConfig?.detectionMethods || [],
});
}
} catch (e) {
console.warn(
"[DataflowOrchestrator] Failed to update project config options on settings update",
e
);
}
// Update TimeParsingService configuration
if (settings.timeParsing && this.timeParsingService) {
this.timeParsingService.updateConfig(settings.timeParsing);

View file

@ -390,7 +390,37 @@ export default class TaskProgressBarPlugin extends Plugin {
this.app.metadataCache,
this,
{
// ProjectConfigManagerOptions is narrower; pass only known properties
configFileName:
this.settings.projectConfig?.configFile?.fileName ||
"project.md",
searchRecursively:
this.settings.projectConfig?.configFile
?.searchRecursively ?? true,
metadataKey:
this.settings.projectConfig?.metadataConfig
?.metadataKey || "project",
pathMappings:
this.settings.projectConfig?.pathMappings || [],
metadataMappings:
this.settings.projectConfig?.metadataMappings || [],
defaultProjectNaming: this.settings.projectConfig
?.defaultProjectNaming || {
strategy: "filename",
stripExtension: true,
enabled: false,
},
enhancedProjectEnabled:
this.settings.projectConfig
?.enableEnhancedProject ?? false,
metadataConfigEnabled:
this.settings.projectConfig?.metadataConfig
?.enabled ?? false,
configFileEnabled:
this.settings.projectConfig?.configFile?.enabled ??
false,
detectionMethods:
this.settings.projectConfig?.metadataConfig
?.detectionMethods || [],
}
);
console.timeEnd("[TPB] createDataflow");

View file

@ -187,7 +187,8 @@ export class ProjectConfigManager {
}
const currentTimestamp = (file as TFile).stat.mtime;
const cachedTimestamp = this.fileMetadataTimestampCache.get(filePath);
const cachedTimestamp =
this.fileMetadataTimestampCache.get(filePath);
// Check if cache is valid (file hasn't been modified)
if (
@ -219,16 +220,16 @@ export class ProjectConfigManager {
*/
public normalizeProjectPath(path: string): string {
if (!path) return "";
// Replace backslashes with forward slashes
let normalized = path.replace(/\\/g, "/");
// Remove duplicate slashes
normalized = normalized.replace(/\/+/g, "/");
// Remove leading and trailing slashes
normalized = normalized.replace(/^\/|\/$/g, "");
return normalized;
}
@ -248,7 +249,9 @@ export class ProjectConfigManager {
// Simple path matching - could be enhanced with glob patterns
if (this.matchesPathPattern(filePath, mapping.pathPattern)) {
// Normalize the project name to support nested paths
const normalizedName = this.normalizeProjectPath(mapping.projectName);
const normalizedName = this.normalizeProjectPath(
mapping.projectName
);
return {
type: "path",
name: normalizedName,
@ -264,34 +267,60 @@ export class ProjectConfigManager {
if (file && file instanceof TFile) {
const fileCache = this.metadataCache.getFileCache(file);
const fileMetadata = this.getFileMetadata(filePath);
for (const method of this.detectionMethods) {
if (!method.enabled) continue;
switch (method.type) {
case "metadata":
// Check if the specified metadata property exists
if (fileMetadata && fileMetadata[method.propertyKey]) {
if (
fileMetadata &&
fileMetadata[method.propertyKey]
) {
return {
type: "metadata",
name: String(fileMetadata[method.propertyKey]),
name: String(
fileMetadata[method.propertyKey]
),
source: method.propertyKey,
readonly: true,
};
}
break;
case "tag":
// Check if file has the specified tag
if (fileCache?.tags) {
const targetTag = method.propertyKey.startsWith("#")
? method.propertyKey
// Check if file has the specified tag (consider both inline and frontmatter tags)
{
const targetTag = method.propertyKey.startsWith(
"#"
)
? method.propertyKey
: `#${method.propertyKey}`;
const hasTag = fileCache.tags.some(tagCache =>
tagCache.tag === targetTag
const normalizedTarget =
targetTag.toLowerCase();
const inlineTags = (fileCache?.tags || []).map(
(tc) => tc.tag
);
const fmTagsRaw = fileCache?.frontmatter?.tags;
const fmTagsArr = Array.isArray(fmTagsRaw)
? fmTagsRaw
: fmTagsRaw !== undefined
? [fmTagsRaw]
: [];
const fmTagsNorm = fmTagsArr.map((t: any) => {
const s = String(t || "");
return s.startsWith("#") ? s : `#${s}`;
});
const allTags = [
...inlineTags,
...fmTagsNorm,
].map((t) => String(t || "").toLowerCase());
// For file-level detection: require exact match; do NOT treat hierarchical '#project/xxx' as match unless configured exactly
const hasTag = allTags.some(
(t) => t === normalizedTarget
);
if (hasTag) {
// First try to use title or name from frontmatter as project name
if (fileMetadata?.title) {
@ -311,8 +340,12 @@ export class ProjectConfigManager {
};
}
// Fallback: use the file name (without extension)
const fileName = filePath.split('/').pop() || filePath;
const nameWithoutExt = fileName.replace(/\.md$/i, '');
const fileName =
filePath.split("/").pop() || filePath;
const nameWithoutExt = fileName.replace(
/\.md$/i,
""
);
return {
type: "metadata",
name: nameWithoutExt,
@ -322,21 +355,27 @@ export class ProjectConfigManager {
}
}
break;
case "link":
// Check all links in the file
if (fileCache?.links) {
for (const linkCache of fileCache.links) {
const linkedNote = linkCache.link;
// If there's a filter, check if the link matches
if (method.linkFilter) {
if (linkedNote.includes(method.linkFilter)) {
if (
linkedNote.includes(
method.linkFilter
)
) {
// First try to use title or name from frontmatter as project name
if (fileMetadata?.title) {
return {
type: "metadata",
name: String(fileMetadata.title),
name: String(
fileMetadata.title
),
source: "title (via link)",
readonly: true,
};
@ -344,14 +383,19 @@ export class ProjectConfigManager {
if (fileMetadata?.name) {
return {
type: "metadata",
name: String(fileMetadata.name),
name: String(
fileMetadata.name
),
source: "name (via link)",
readonly: true,
};
}
// Fallback: use the file name (without extension)
const fileName = filePath.split('/').pop() || filePath;
const nameWithoutExt = fileName.replace(/\.md$/i, '');
const fileName =
filePath.split("/").pop() ||
filePath;
const nameWithoutExt =
fileName.replace(/\.md$/i, "");
return {
type: "metadata",
name: nameWithoutExt,
@ -361,15 +405,26 @@ export class ProjectConfigManager {
}
} else if (method.propertyKey) {
// If a property key is specified, only check links in that metadata field
if (fileMetadata && fileMetadata[method.propertyKey]) {
const propValue = String(fileMetadata[method.propertyKey]);
if (
fileMetadata &&
fileMetadata[method.propertyKey]
) {
const propValue = String(
fileMetadata[method.propertyKey]
);
// Check if this link is mentioned in the property
if (propValue.includes(`[[${linkedNote}]]`)) {
if (
propValue.includes(
`[[${linkedNote}]]`
)
) {
// First try to use title or name from frontmatter as project name
if (fileMetadata?.title) {
return {
type: "metadata",
name: String(fileMetadata.title),
name: String(
fileMetadata.title
),
source: "title (via link)",
readonly: true,
};
@ -377,14 +432,22 @@ export class ProjectConfigManager {
if (fileMetadata?.name) {
return {
type: "metadata",
name: String(fileMetadata.name),
name: String(
fileMetadata.name
),
source: "name (via link)",
readonly: true,
};
}
// Fallback: use the file name (without extension)
const fileName = filePath.split('/').pop() || filePath;
const nameWithoutExt = fileName.replace(/\.md$/i, '');
const fileName =
filePath.split("/").pop() ||
filePath;
const nameWithoutExt =
fileName.replace(
/\.md$/i,
""
);
return {
type: "metadata",
name: nameWithoutExt,
@ -445,7 +508,8 @@ export class ProjectConfigManager {
const defaultProject = this.generateDefaultProjectName(filePath);
if (defaultProject) {
// Normalize default project name as well
const normalizedName = this.normalizeProjectPath(defaultProject);
const normalizedName =
this.normalizeProjectPath(defaultProject);
return {
type: "default",
name: normalizedName,
@ -482,7 +546,8 @@ export class ProjectConfigManager {
// Create composite cache key
const cacheKey = `${fileTimestamp}_${configTimestamp}`;
const cachedCacheKey = this.enhancedMetadataTimestampCache.get(filePath);
const cachedCacheKey =
this.enhancedMetadataTimestampCache.get(filePath);
// Check if cache is valid (neither file nor config has been modified)
if (
@ -508,7 +573,10 @@ export class ProjectConfigManager {
return mergedMetadata;
} catch (error) {
console.warn(`Failed to get enhanced metadata for ${filePath}:`, error);
console.warn(
`Failed to get enhanced metadata for ${filePath}:`,
error
);
return {};
}
}
@ -747,7 +815,9 @@ export class ProjectConfigManager {
// Try to convert date string to timestamp for better performance
if (/^\d{4}-\d{2}-\d{2}/.test(value)) {
// Use the same date parsing logic as MarkdownTaskParser
const { parseLocalDate } = require("../utils/date/date-formatter");
const {
parseLocalDate,
} = require("../utils/date/date-formatter");
const timestamp = parseLocalDate(value);
return timestamp !== undefined ? timestamp : value;
}
@ -815,22 +885,29 @@ export class ProjectConfigManager {
case "foldername": {
const normalizedPath = filePath.replace(/\\/g, "/");
const pathParts = normalizedPath.split("/");
// For path-based projects, build nested structure from folder path
// e.g., "Projects/Web/Frontend/file.md" -> "Web/Frontend"
if (pathParts.length > 1) {
// Find if path contains a common project root folder
const projectRootIndex = pathParts.findIndex(part =>
part.toLowerCase() === "projects" ||
part.toLowerCase() === "project"
const projectRootIndex = pathParts.findIndex(
(part) =>
part.toLowerCase() === "projects" ||
part.toLowerCase() === "project"
);
if (projectRootIndex >= 0 && projectRootIndex < pathParts.length - 2) {
if (
projectRootIndex >= 0 &&
projectRootIndex < pathParts.length - 2
) {
// Build project path from folders after the project root
const projectParts = pathParts.slice(projectRootIndex + 1, pathParts.length - 1);
const projectParts = pathParts.slice(
projectRootIndex + 1,
pathParts.length - 1
);
return projectParts.join("/");
}
// Fallback to just parent folder name if no project root found
return pathParts[pathParts.length - 2] || "";
}
@ -886,6 +963,9 @@ export class ProjectConfigManager {
if (options.configFileEnabled !== undefined) {
this.configFileEnabled = options.configFileEnabled;
}
if (options.detectionMethods !== undefined) {
this.detectionMethods = options.detectionMethods || [];
}
// Clear cache when options change
this.clearCache();
@ -912,6 +992,13 @@ export class ProjectConfigManager {
};
}
/**
* Expose detection methods (used to decide if worker can be used)
*/
getDetectionMethods(): ProjectDetectionMethod[] {
return this.detectionMethods || [];
}
/**
* Get project config data for a file (alias for getProjectConfig for compatibility)
*/
@ -943,18 +1030,23 @@ export class ProjectConfigManager {
} {
// Calculate estimated memory usage (rough approximation)
const configCacheSize = Array.from(this.configCache.values())
.map(config => JSON.stringify(config).length)
.map((config) => JSON.stringify(config).length)
.reduce((sum, size) => sum + size, 0);
const fileMetadataCacheSize = Array.from(this.fileMetadataCache.values())
.map(metadata => JSON.stringify(metadata).length)
const fileMetadataCacheSize = Array.from(
this.fileMetadataCache.values()
)
.map((metadata) => JSON.stringify(metadata).length)
.reduce((sum, size) => sum + size, 0);
const enhancedMetadataCacheSize = Array.from(this.enhancedMetadataCache.values())
.map(metadata => JSON.stringify(metadata).length)
const enhancedMetadataCacheSize = Array.from(
this.enhancedMetadataCache.values()
)
.map((metadata) => JSON.stringify(metadata).length)
.reduce((sum, size) => sum + size, 0);
const totalMemoryUsage = configCacheSize + fileMetadataCacheSize + enhancedMetadataCacheSize;
const totalMemoryUsage =
configCacheSize + fileMetadataCacheSize + enhancedMetadataCacheSize;
return {
configCache: {
@ -980,9 +1072,16 @@ export class ProjectConfigManager {
let clearedCount = 0;
// Check file metadata cache for stale entries
for (const [filePath, timestamp] of this.fileMetadataTimestampCache.entries()) {
for (const [
filePath,
timestamp,
] of this.fileMetadataTimestampCache.entries()) {
const file = this.vault.getFileByPath(filePath);
if (!file || !("stat" in file) || (file as TFile).stat.mtime !== timestamp) {
if (
!file ||
!("stat" in file) ||
(file as TFile).stat.mtime !== timestamp
) {
this.fileMetadataCache.delete(filePath);
this.fileMetadataTimestampCache.delete(filePath);
this.enhancedMetadataCache.delete(filePath);