fix status trigger value residue

This commit is contained in:
callumalpass 2026-06-10 19:49:21 +10:00
parent 7b17a26bbf
commit ef04893eba
3 changed files with 96 additions and 1 deletions

View file

@ -31,3 +31,7 @@ When a change has user-facing documentation, include a canonical tasknotes.dev l
```
-->
## Fixed
- (#2007) Fixed a remaining task creation autocomplete case where a status value containing another status label could leave status fragments in the task title. Thanks to @prepare4robots for the follow-up report.

View file

@ -28,6 +28,7 @@ function getDateLocaleFromPlugin(plugin: TaskNotesPlugin): string {
*/
export class NaturalLanguageParser extends NaturalLanguageParserCore {
private readonly taskNotesNlpTriggers?: NLPTriggersConfig;
private readonly taskNotesStatusConfigs: StatusConfig[];
private readonly taskNotesUserFields: UserMappedField[];
private readonly taskNotesPriorityConfigs: PriorityConfig[];
@ -63,17 +64,87 @@ export class NaturalLanguageParser extends NaturalLanguageParserCore {
options
);
this.taskNotesNlpTriggers = nlpTriggers;
this.taskNotesStatusConfigs = statusConfigs;
this.taskNotesUserFields = userFields || [];
this.taskNotesPriorityConfigs = priorityConfigs;
}
parseInput(input: string): ParsedTaskData {
const parsed = super.parseInput(input);
const withPriorityShortcutResidueRemoved = this.removePriorityShortcutResidue(input, parsed);
const withStatusShortcutResidueRemoved = this.applyTriggeredStatusMatch(input, parsed);
const withPriorityShortcutResidueRemoved = this.removePriorityShortcutResidue(
input,
withStatusShortcutResidueRemoved
);
const withLinkedFields = this.extractLinkedUserFields(input, withPriorityShortcutResidueRemoved);
return this.normalizeUserFieldValues(withLinkedFields);
}
private applyTriggeredStatusMatch(input: string, parsed: ParsedTaskData): ParsedTaskData {
const match = this.findTriggeredStatusMatch(input);
if (!match) {
return parsed;
}
parsed.status = match.config.value;
let title = parsed.title;
for (const residue of this.getStatusShortcutResidues(match.token)) {
title = this.removeTokenFragment(title, residue);
}
parsed.title = title || "Untitled Task";
return parsed;
}
private findTriggeredStatusMatch(
input: string
): { config: StatusConfig; token: string } | null {
const trigger = this.getStatusTrigger();
if (!trigger || this.taskNotesStatusConfigs.length === 0) {
return null;
}
const candidates: Array<{ config: StatusConfig; token: string }> = [];
for (const config of this.taskNotesStatusConfigs) {
for (const phrase of this.getStatusPhrases(config)) {
candidates.push({ config, token: `${trigger}${phrase}${trigger}` });
candidates.push({ config, token: `${trigger}${phrase}` });
}
}
candidates.sort((a, b) => b.token.length - a.token.length);
return candidates.find((candidate) => this.containsToken(input, candidate.token)) ?? null;
}
private getStatusShortcutResidues(token: string): string[] {
const residues = new Set<string>([token]);
for (const config of this.taskNotesStatusConfigs) {
for (const phrase of this.getStatusPhrases(config)) {
const residue = this.removeFirstCaseInsensitive(token, phrase);
if (residue && residue !== token) {
this.addStatusShortcutResidue(residues, residue);
}
}
}
return Array.from(residues)
.filter((residue) => residue.trim().length > 0)
.sort((a, b) => b.length - a.length);
}
private addStatusShortcutResidue(residues: Set<string>, residue: string): void {
residues.add(residue);
residues.add(residue.replace(/\s+/g, " ").trim());
}
private getStatusPhrases(config: StatusConfig): string[] {
return [config.value, config.label]
.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
.map((value) => value.trim());
}
private removePriorityShortcutResidue(input: string, parsed: ParsedTaskData): ParsedTaskData {
if (!parsed.priority || this.taskNotesPriorityConfigs.length === 0) {
return parsed;
@ -122,6 +193,13 @@ export class NaturalLanguageParser extends NaturalLanguageParserCore {
return priorityTrigger?.trigger || "";
}
private getStatusTrigger(): string {
const statusTrigger = this.taskNotesNlpTriggers?.triggers.find(
(trigger) => trigger.propertyId === "status"
);
return statusTrigger?.enabled === false ? "" : statusTrigger?.trigger || "*";
}
private containsToken(text: string, token: string): boolean {
const escaped = this.escapeRegexLiteral(token);
return new RegExp(`(^|\\s)${escaped}(?=\\s|$)`, "iu").test(text);

View file

@ -277,6 +277,19 @@ describe('NaturalLanguageParser - Status Extraction', () => {
expect(result.title).toBe('Task');
});
it('prefers the full triggered status value when it contains a shorter status label', () => {
const statusConfigs: StatusConfig[] = [
{ id: 'resource', value: '30 Resource', label: 'Resource', color: '#666666', isCompleted: false, order: 1, autoArchive: false, autoArchiveDelay: 0 },
{ id: 'person', value: '31 Resource - Person', label: 'Person', color: '#0066cc', isCompleted: false, order: 2, autoArchive: false, autoArchiveDelay: 0 }
];
const parser = new NaturalLanguageParser(statusConfigs, priorityConfigs, false);
const result = parser.parseInput('Test *31 Resource - Person*');
expect(result.status).toBe('31 Resource - Person');
expect(result.title).toBe('Test');
});
it('should extract status without trigger (fallback) and remove it', () => {
const statusConfigs: StatusConfig[] = [
{ id: 'done', value: 'done', label: 'Done', color: '#00aa00', isCompleted: true, order: 1, autoArchive: false, autoArchiveDelay: 0 }