]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Implement relative date querying
authorMichael Shamoon <4887959+shamoon@users.noreply.github.com>
Tue, 25 Oct 2022 19:45:15 +0000 (12:45 -0700)
committerMichael Shamoon <4887959+shamoon@users.noreply.github.com>
Tue, 25 Oct 2022 19:45:15 +0000 (12:45 -0700)
src-ui/src/app/components/common/date-dropdown/date-dropdown.component.html
src-ui/src/app/components/common/date-dropdown/date-dropdown.component.scss
src-ui/src/app/components/common/date-dropdown/date-dropdown.component.ts
src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html
src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts

index 6e5a7d92842004d08bf1e8574439823bf9fa0df7..0327ee963112d87ba148fa7621652dfb8e53dc2f 100644 (file)
@@ -1,10 +1,18 @@
   <div class="btn-group w-100" ngbDropdown role="group">
   <button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="dateBefore || dateAfter ? 'btn-primary' : 'btn-outline-primary'">
     {{title}}
+    <div *ngIf="isActive" class="position-absolute top-0 start-100 p-2 translate-middle badge bg-secondary border border-light rounded-circle">
+      <span class="visually-hidden">selected</span>
+    </div>
   </button>
   <div class="dropdown-menu date-dropdown shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
     <div class="list-group list-group-flush">
-        <button *ngFor="let qf of quickFilters" class="list-group-item small list-goup list-group-item-action d-flex p-2 ps-3" role="menuitem" (click)="setDateQuickFilter(qf.id)">
+        <button *ngFor="let qf of quickFilters" class="list-group-item small list-goup list-group-item-action d-flex p-2" role="menuitem" (click)="setDateQuickFilter(qf.id)">
+          <div _ngcontent-hga-c166="" class="selected-icon me-1">
+            <svg *ngIf="quickFilter === qf.id" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
+              <path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
+            </svg>
+          </div>
           {{qf.name}}
         </button>
         <div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
index e6045629ba27727d54157eaf9ba2ac43adc0a093..8573ab57541ee6c5501c7307d398b0b8071a3ff6 100644 (file)
@@ -5,3 +5,8 @@
     line-height: 1;
   }
 }
+
+.selected-icon {
+  min-width: 1em;
+  min-height: 1em;
+}
index f9d90ae7a48a891386ed0dcc266d56bedec57fac..6eaa14b97a21132f3ed781795f9916797bbd6cca 100644 (file)
@@ -16,6 +16,13 @@ import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
 export interface DateSelection {
   before?: string
   after?: string
+  dateQuery?: string
+}
+
+interface QuickFilter {
+  id: number
+  name: string
+  dateQuery: string
 }
 
 const LAST_7_DAYS = 0
@@ -34,11 +41,23 @@ export class DateDropdownComponent implements OnInit, OnDestroy {
     this.datePlaceHolder = settings.getLocalizedDateInputFormat()
   }
 
-  quickFilters = [
-    { id: LAST_7_DAYS, name: $localize`Last 7 days` },
-    { id: LAST_MONTH, name: $localize`Last month` },
-    { id: LAST_3_MONTHS, name: $localize`Last 3 months` },
-    { id: LAST_YEAR, name: $localize`Last year` },
+  quickFilters: Array<QuickFilter> = [
+    {
+      id: LAST_7_DAYS,
+      name: $localize`Last 7 days`,
+      dateQuery: '-1 week to now',
+    },
+    {
+      id: LAST_MONTH,
+      name: $localize`Last month`,
+      dateQuery: '-1 month to now',
+    },
+    {
+      id: LAST_3_MONTHS,
+      name: $localize`Last 3 months`,
+      dateQuery: '-3 month to now',
+    },
+    { id: LAST_YEAR, name: $localize`Last year`, dateQuery: '-1 year to now' },
   ]
 
   datePlaceHolder: string
@@ -55,12 +74,36 @@ export class DateDropdownComponent implements OnInit, OnDestroy {
   @Output()
   dateAfterChange = new EventEmitter<string>()
 
+  quickFilter: number
+
+  @Input()
+  set dateQuery(query: string) {
+    this.quickFilter = this.quickFilters.find((qf) => qf.dateQuery == query)?.id
+  }
+
+  get dateQuery(): string {
+    return (
+      this.quickFilters.find((qf) => qf.id == this.quickFilter)?.dateQuery ?? ''
+    )
+  }
+
+  @Output()
+  dateQueryChange = new EventEmitter<string>()
+
   @Input()
   title: string
 
   @Output()
   datesSet = new EventEmitter<DateSelection>()
 
+  get isActive(): boolean {
+    return (
+      this.quickFilter > -1 ||
+      this.dateAfter?.length > 0 ||
+      this.dateBefore?.length > 0
+    )
+  }
+
   private datesSetDebounce$ = new Subject()
 
   private sub: Subscription
@@ -79,35 +122,28 @@ export class DateDropdownComponent implements OnInit, OnDestroy {
 
   setDateQuickFilter(qf: number) {
     this.dateBefore = null
-    let date = new Date()
-    switch (qf) {
-      case LAST_7_DAYS:
-        date.setDate(date.getDate() - 7)
-        break
-
-      case LAST_MONTH:
-        date.setMonth(date.getMonth() - 1)
-        break
-
-      case LAST_3_MONTHS:
-        date.setMonth(date.getMonth() - 3)
-        break
-
-      case LAST_YEAR:
-        date.setFullYear(date.getFullYear() - 1)
-        break
-    }
-    this.dateAfter = formatDate(date, 'yyyy-MM-dd', 'en-us', 'UTC')
+    this.dateAfter = null
+    this.quickFilter = this.quickFilter == qf ? null : qf
     this.onChange()
   }
 
+  qfIsSelected(qf: number) {
+    return this.quickFilter == qf
+  }
+
   onChange() {
-    this.dateAfterChange.emit(this.dateAfter)
     this.dateBeforeChange.emit(this.dateBefore)
-    this.datesSet.emit({ after: this.dateAfter, before: this.dateBefore })
+    this.dateAfterChange.emit(this.dateAfter)
+    this.dateQueryChange.emit(this.dateQuery)
+    this.datesSet.emit({
+      after: this.dateAfter,
+      before: this.dateBefore,
+      dateQuery: this.dateQuery,
+    })
   }
 
   onChangeDebounce() {
+    this.dateQuery = null
     this.datesSetDebounce$.next({
       after: this.dateAfter,
       before: this.dateBefore,
index 3ba0623dd48c1be142ac67eb6fde1c17ea009092..fd6c945de2ed92a7782c91033b989e3a1f64d4c7 100644 (file)
             title="Created" i18n-title
             (datesSet)="updateRules()"
             [(dateBefore)]="dateCreatedBefore"
-            [(dateAfter)]="dateCreatedAfter"></app-date-dropdown>
+            [(dateAfter)]="dateCreatedAfter"
+            [(dateQuery)]="dateCreatedQuery"></app-date-dropdown>
           <app-date-dropdown class="mb-2 mb-xl-0"
+            title="Added" i18n-title
+            (datesSet)="updateRules()"
             [(dateBefore)]="dateAddedBefore"
             [(dateAfter)]="dateAddedAfter"
-            title="Added" i18n-title
-            (datesSet)="updateRules()"></app-date-dropdown>
+            [(dateQuery)]="dateAddedQuery"></app-date-dropdown>
         </div>
      </div>
    </div>
index d5295f69752e50e7b220f75f1cb7c923052ca70b..734242be135585a1ee268821afddb02af72f82d5 100644 (file)
@@ -57,6 +57,9 @@ const TEXT_FILTER_MODIFIER_NOTNULL = 'not null'
 const TEXT_FILTER_MODIFIER_GT = 'greater'
 const TEXT_FILTER_MODIFIER_LT = 'less'
 
+const RELATIVE_DATE_QUERY_REGEXP_CREATED = /created:\[([^\]]+)\]/g
+const RELATIVE_DATE_QUERY_REGEXP_ADDED = /added:\[([^\]]+)\]/g
+
 @Component({
   selector: 'app-filter-editor',
   templateUrl: './filter-editor.component.html',
@@ -197,6 +200,8 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
   dateCreatedAfter: string
   dateAddedBefore: string
   dateAddedAfter: string
+  dateCreatedQuery: string
+  dateAddedQuery: string
 
   _unmodifiedFilterRules: FilterRule[] = []
   _filterRules: FilterRule[] = []
@@ -228,6 +233,8 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
     this.dateAddedAfter = null
     this.dateCreatedBefore = null
     this.dateCreatedAfter = null
+    this.dateCreatedQuery = null
+    this.dateAddedQuery = null
     this.textFilterModifier = TEXT_FILTER_MODIFIER_EQUALS
 
     value.forEach((rule) => {
@@ -245,7 +252,30 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
           this.textFilterTarget = TEXT_FILTER_TARGET_ASN
           break
         case FILTER_FULLTEXT_QUERY:
-          this._textFilter = rule.value
+          let queryArgs = rule.value.split(',')
+          queryArgs.forEach((arg) => {
+            if (arg.match(RELATIVE_DATE_QUERY_REGEXP_CREATED)) {
+              ;[...arg.matchAll(RELATIVE_DATE_QUERY_REGEXP_CREATED)].forEach(
+                (match) => {
+                  if (match[1]?.length) {
+                    this.dateCreatedQuery = match[1]
+                  }
+                }
+              )
+              queryArgs.splice(queryArgs.indexOf(arg), 1)
+            }
+            if (arg.match(RELATIVE_DATE_QUERY_REGEXP_ADDED)) {
+              ;[...arg.matchAll(RELATIVE_DATE_QUERY_REGEXP_ADDED)].forEach(
+                (match) => {
+                  if (match[1]?.length) {
+                    this.dateAddedQuery = match[1]
+                  }
+                }
+              )
+              queryArgs.splice(queryArgs.indexOf(arg), 1)
+            }
+          })
+          this._textFilter = queryArgs.join(',')
           this.textFilterTarget = TEXT_FILTER_TARGET_FULLTEXT_QUERY
           break
         case FILTER_FULLTEXT_MORELIKE:
@@ -471,6 +501,52 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
         value: this.dateAddedAfter,
       })
     }
+    if (this.dateAddedQuery || this.dateCreatedQuery) {
+      let queryArgs: Array<string> = []
+      if (this.dateCreatedQuery)
+        queryArgs.push(`created:[${this.dateCreatedQuery}]`)
+      if (this.dateAddedQuery) queryArgs.push(`added:[${this.dateAddedQuery}]`)
+      const existingRule = filterRules.find(
+        (fr) => fr.rule_type == FILTER_FULLTEXT_QUERY
+      )
+      if (existingRule) {
+        let existingRuleArgs = existingRule.value.split(',')
+        if (this.dateCreatedQuery) {
+          queryArgs = existingRuleArgs
+            .filter((arg) => !arg.includes('created:'))
+            .concat(queryArgs)
+        }
+        if (this.dateAddedQuery) {
+          queryArgs = existingRuleArgs
+            .filter((arg) => !arg.includes('added:'))
+            .concat(queryArgs)
+        }
+        existingRule.value = queryArgs.join(',')
+      } else {
+        filterRules.push({
+          rule_type: FILTER_FULLTEXT_QUERY,
+          value: queryArgs.join(','),
+        })
+      }
+    }
+    if (!this.dateAddedQuery && !this.dateCreatedQuery) {
+      const existingRule = filterRules.find(
+        (fr) => fr.rule_type == FILTER_FULLTEXT_QUERY
+      )
+      if (
+        existingRule?.value.includes('created:') ||
+        existingRule?.value.includes('added:')
+      ) {
+        // remove any existing date query
+        existingRule.value = existingRule.value
+          .replace(RELATIVE_DATE_QUERY_REGEXP_CREATED, '')
+          .replace(RELATIVE_DATE_QUERY_REGEXP_ADDED, '')
+        if (existingRule.value.replace(',', '').trim() === '') {
+          // if its empty now, remove it entirely
+          filterRules.splice(filterRules.indexOf(existingRule), 1)
+        }
+      }
+    }
     return filterRules
   }
 
@@ -584,6 +660,8 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
       target != TEXT_FILTER_TARGET_FULLTEXT_MORELIKE
     ) {
       this._textFilter = ''
+      this.dateAddedQuery = ''
+      this.dateCreatedQuery = ''
     }
     this.textFilterTarget = target
     this.textFilterInput.nativeElement.focus()