fix(file-source): remove project tag when deriving project from tags

When project metadata is derived from tags like #project/Alpha, the tag
is now removed from the final tags array to prevent duplication. This
only applies when the project comes from tag extraction, not from
frontmatter properties.

Also optimizes import to use type-only import for plugin reference.
This commit is contained in:
Quorafind 2025-10-30 15:26:38 +08:00
parent 66e1f6ad88
commit 4f0dc243ed
2 changed files with 63 additions and 15 deletions

View file

@ -360,6 +360,26 @@ describe('FileSource', () => {
expect(task).toBeNull();
});
it('should remove project tag when used to derive project metadata', async () => {
const fileCache = {
frontmatter: {
tags: ['context/foo']
},
tags: [
{ tag: '#project/Alpha' },
{ tag: '#task' }
]
};
mockApp.metadataCache.getFileCache.mockReturnValue(fileCache);
const task = await fileSource.createFileTask('test.md');
expect(task).toBeTruthy();
expect(task!.metadata.project).toBe('Alpha');
expect(task!.metadata.tags).toEqual(expect.arrayContaining(['#task', '#context/foo']));
expect(task!.metadata.tags).not.toContain('#project/Alpha');
});
});
describe('statistics', () => {

View file

@ -6,7 +6,7 @@
*/
import type { App, TFile, EventRef, CachedMetadata } from "obsidian";
import TaskProgressBarPlugin from "@/index";
import type TaskProgressBarPlugin from "@/index";
import type { Task } from "@/types/task";
import type {
FileSourceConfiguration,
@ -869,7 +869,8 @@ export class FileSource {
// Extract project from tags as a fallback (only if not in frontmatter)
// Note: In FileSource mode, context and area come ONLY from frontmatter,
// not from tags like #context/xxx or #area/xxx
const projectFromTags = this.extractProjectFromTags(allTags);
const projectTagExtraction = this.extractProjectFromTags(allTags);
const projectFromTags = projectTagExtraction.project;
// Priority order for project:
// 1. Direct frontmatter field (project: xxx) - highest priority
@ -877,6 +878,16 @@ export class FileSource {
// 3. Tag extraction (#project/xxx) - lowest priority, only if frontmatter has nothing
const projectValue = resolvedFrontmatter.project ?? projectFromTags;
const shouldStripProjectTags =
!resolvedFrontmatter.project &&
Boolean(projectFromTags) &&
projectTagExtraction.matchedTags.length > 0;
const tagsForMetadata = shouldStripProjectTags
? allTags.filter(
(tag) => !projectTagExtraction.matchedTags.includes(tag)
)
: allTags;
// Extract standard task metadata
const metadata: Partial<FileSourceTaskMetadata> = {
dueDate: this.parseDate(
@ -903,7 +914,7 @@ export class FileSource {
// Context and area: ONLY from frontmatter (direct or mapped via metadataMappings)
context: resolvedFrontmatter.context,
area: resolvedFrontmatter.area,
tags: allTags,
tags: tagsForMetadata,
status: status,
children: [],
};
@ -917,9 +928,20 @@ export class FileSource {
* Note: context and area are NOT extracted from tags in FileSource mode,
* they should be defined directly in frontmatter (context: xxx, area: xxx)
*/
private extractProjectFromTags(tags: string[]): string | undefined {
private extractProjectFromTags(tags: string[]): {
project?: string;
matchedTags: string[];
} {
// Get configurable project prefix from plugin settings, with fallback default
const projectPrefix = "project";
const configuredPrefix =
this.plugin?.settings?.projectTagPrefix?.["tasks"];
const projectPrefix =
typeof configuredPrefix === "string" && configuredPrefix.trim()
? configuredPrefix.trim()
: "project";
const matchedTags: string[] = [];
let projectValue: string | undefined;
for (const tag of tags) {
if (!tag || typeof tag !== "string") continue;
@ -933,21 +955,27 @@ export class FileSource {
.toLowerCase()
.startsWith(`${projectPrefix.toLowerCase()}/`)
) {
// Extract the value using the actual prefix length (preserving case in the value)
const slashIndex = tagWithoutHash.indexOf("/");
if (slashIndex !== -1) {
const value = tagWithoutHash.substring(slashIndex + 1);
if (value) {
console.log(
`[FileSource] Extracted project from tag: ${value}`
);
return value;
matchedTags.push(tag);
if (!projectValue) {
// Extract the value using the actual prefix length (preserving case in the value)
const slashIndex = tagWithoutHash.indexOf("/");
if (slashIndex !== -1) {
const value = tagWithoutHash.substring(slashIndex + 1);
if (value) {
projectValue = value;
}
}
}
}
}
return undefined;
if (projectValue) {
console.log(
`[FileSource] Extracted project from tag: ${projectValue}`
);
}
return { project: projectValue, matchedTags };
}
/**