mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
fix status trigger value residue
This commit is contained in:
parent
7b17a26bbf
commit
ef04893eba
3 changed files with 96 additions and 1 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
Loading…
Reference in a new issue