mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Implement relative date querying
This commit is contained in:
		| @@ -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"> | ||||
|   | ||||
| @@ -5,3 +5,8 @@ | ||||
|     line-height: 1; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .selected-icon { | ||||
|   min-width: 1em; | ||||
|   min-height: 1em; | ||||
| } | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -54,12 +54,14 @@ | ||||
|             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> | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Michael Shamoon
					Michael Shamoon