diff --git a/src/app/app.component.html b/src/app/app.component.html
index ec21e8698..0dedc0dfc 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -406,6 +406,7 @@
No Message Selected
diff --git a/src/app/xapian/multiple-search-fields-input/multiple-search-fields-input.component.spec.ts b/src/app/xapian/multiple-search-fields-input/multiple-search-fields-input.component.spec.ts
index a17304417..71fdf7c88 100644
--- a/src/app/xapian/multiple-search-fields-input/multiple-search-fields-input.component.spec.ts
+++ b/src/app/xapian/multiple-search-fields-input/multiple-search-fields-input.component.spec.ts
@@ -29,7 +29,7 @@ import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { firstValueFrom } from 'rxjs';
-import { take } from 'rxjs/operators';
+import { SimpleChange } from '@angular/core';
describe('MultipleSearchFieldsInputComponent', () => {
let component: MultipleSearchFieldsInputComponent;
@@ -71,4 +71,34 @@ describe('MultipleSearchFieldsInputComponent', () => {
console.log(searchExpression);
expect(searchExpression).toBe('folder:"Testfolder" AND subject:"testsubject"');
});
+
+ it('should update fields from a generated search expression', () => {
+ component.currentFolder = 'Inbox';
+ (component as any).searchExpression = 'folder:"Inbox" AND from:"from@example.com" AND to:"to@example.com" AND subject:"Quarterly report" AND (budget) AND flag:attachment AND flag:answered AND flag:flagged AND date:202606 AND NOT flag:seen';
+
+ (component as any).ngOnChanges({
+ searchExpression: new SimpleChange('', (component as any).searchExpression, false)
+ });
+
+ expect(component.formGroup.get('currentfolderonly').value).toBeTrue();
+ expect(component.formGroup.get('from').value).toBe('from@example.com');
+ expect(component.formGroup.get('to').value).toBe('to@example.com');
+ expect(component.formGroup.get('subject').value).toBe('Quarterly report');
+ expect(component.formGroup.get('allfieldsandcontent').value).toBe('budget');
+ expect(component.formGroup.get('hasAttachment').value).toBeTrue();
+ expect(component.formGroup.get('hasReply').value).toBeTrue();
+ expect(component.formGroup.get('hasFlag').value).toBeTrue();
+ expect(component.formGroup.get('date').value).toBe('202606');
+ expect(component.formGroup.get('unreadOnly').value).toBeTrue();
+ });
+
+ it('should show unstructured query text in the all fields input', () => {
+ (component as any).searchExpression = 'monthly budget';
+
+ (component as any).ngOnChanges({
+ searchExpression: new SimpleChange('', (component as any).searchExpression, false)
+ });
+
+ expect(component.formGroup.get('allfieldsandcontent').value).toBe('monthly budget');
+ });
});
diff --git a/src/app/xapian/multiple-search-fields-input/multiple-search-fields-input.component.ts b/src/app/xapian/multiple-search-fields-input/multiple-search-fields-input.component.ts
index d5854fcaf..d13fb159d 100644
--- a/src/app/xapian/multiple-search-fields-input/multiple-search-fields-input.component.ts
+++ b/src/app/xapian/multiple-search-fields-input/multiple-search-fields-input.component.ts
@@ -17,7 +17,7 @@
// along with Runbox 7. If not, see .
// ---------- END RUNBOX LICENSE ----------
-import { Component, OnChanges, Output, EventEmitter, Input } from '@angular/core';
+import { Component, OnChanges, Output, EventEmitter, Input, SimpleChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { distinctUntilChanged } from 'rxjs/operators';
@@ -31,9 +31,12 @@ export class MultipleSearchFieldsInputComponent implements OnChanges {
formGroup;
@Input() currentFolder: string;
+ @Input() searchExpression = '';
@Output() searchexpression = new EventEmitter();
@Output() close = new EventEmitter();
+ private lastEmittedSearchExpression = '';
+
constructor(
formbuilder: FormBuilder
) {
@@ -58,7 +61,12 @@ export class MultipleSearchFieldsInputComponent implements OnChanges {
});
}
- ngOnChanges() {
+ ngOnChanges(changes?: SimpleChanges) {
+ if (changes && changes.searchExpression && this.searchExpression !== this.lastEmittedSearchExpression) {
+ this.updateFormFromSearchExpression(this.searchExpression || '');
+ return;
+ }
+
this.enableDisableUnread();
this.buildSearchExpression();
}
@@ -99,6 +107,84 @@ export class MultipleSearchFieldsInputComponent implements OnChanges {
(fields.date ? (and() + 'date:' + fields.date + '') : '') + // FIXME: This parameter must come last
(fields.unreadOnly ? (and() + 'NOT flag:seen') : ''); // FIXME: This parameter must also come last...
+ this.lastEmittedSearchExpression = searchexpression;
this.searchexpression.emit(searchexpression);
}
+
+ private updateFormFromSearchExpression(searchExpression: string): void {
+ this.formGroup.patchValue(this.parseSearchExpression(searchExpression), { emitEvent: false });
+ this.enableDisableUnread();
+ }
+
+ private parseSearchExpression(searchExpression: string) {
+ const fields = {
+ allfieldsandcontent: '',
+ from: '',
+ to: '',
+ subject: '',
+ date: '',
+ currentfolderonly: false,
+ hasAttachment: false,
+ hasReply: false,
+ hasFlag: false,
+ unreadOnly: false
+ };
+ let remaining = searchExpression.trim();
+
+ remaining = this.consumeQuotedTerm(remaining, 'from', value => fields.from = value);
+ remaining = this.consumeQuotedTerm(remaining, 'to', value => fields.to = value);
+ remaining = this.consumeQuotedTerm(remaining, 'subject', value => fields.subject = value);
+ remaining = remaining.replace(/\bfolder:"([^"]*)"/g, (match, value) => {
+ if (value === this.currentFolder) {
+ fields.currentfolderonly = true;
+ return ' ';
+ }
+ return match;
+ });
+ remaining = this.consumeTerm(remaining, /\bdate:([^\s)]+)/g, value => fields.date = value);
+ remaining = this.consumeFlag(remaining, 'attachment', () => fields.hasAttachment = true);
+ remaining = this.consumeFlag(remaining, 'answered', () => fields.hasReply = true);
+ remaining = this.consumeFlag(remaining, 'flagged', () => fields.hasFlag = true);
+ remaining = this.consumeTerm(remaining, /\bNOT\s+flag:seen\b/g, () => fields.unreadOnly = true);
+
+ remaining = remaining.replace(/\(([^()]*)\)/, (_match, value) => {
+ fields.allfieldsandcontent = value;
+ return ' ';
+ });
+ remaining = this.cleanDanglingAnd(remaining);
+ if (remaining) {
+ fields.allfieldsandcontent = fields.allfieldsandcontent
+ ? `${fields.allfieldsandcontent} ${remaining}`
+ : remaining;
+ }
+
+ return fields;
+ }
+
+ private consumeQuotedTerm(searchExpression: string, term: string, applyValue: (value: string) => void): string {
+ return this.consumeTerm(searchExpression, new RegExp(`\\b${term}:"([^"]*)"`, 'g'), applyValue);
+ }
+
+ private consumeFlag(searchExpression: string, flag: string, applyValue: () => void): string {
+ return this.consumeTerm(searchExpression, new RegExp(`\\bflag:${flag}\\b`, 'g'), applyValue);
+ }
+
+ private consumeTerm(searchExpression: string, pattern: RegExp, applyValue: (value?: string) => void): string {
+ return searchExpression.replace(pattern, (_match, value) => {
+ applyValue(value);
+ return ' ';
+ });
+ }
+
+ private cleanDanglingAnd(searchExpression: string): string {
+ const cleaned = searchExpression
+ .replace(/\s+/g, ' ')
+ .trim()
+ .replace(/^(AND\s+)+/, '')
+ .replace(/(\s+AND)+$/, '')
+ .replace(/\s+AND\s+AND\s+/g, ' AND ')
+ .trim();
+
+ return /^(AND\s*)+$/.test(cleaned) ? '' : cleaned;
+ }
}