mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-02 13:45:10 -05:00
Implement relative date querying
This commit is contained in:
parent
3357fa19f3
commit
6a00d5e08a
@ -1,10 +1,18 @@
|
|||||||
<div class="btn-group w-100" ngbDropdown role="group">
|
<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'">
|
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="dateBefore || dateAfter ? 'btn-primary' : 'btn-outline-primary'">
|
||||||
{{title}}
|
{{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>
|
</button>
|
||||||
<div class="dropdown-menu date-dropdown shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
<div class="dropdown-menu date-dropdown shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
||||||
<div class="list-group list-group-flush">
|
<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}}
|
{{qf.name}}
|
||||||
</button>
|
</button>
|
||||||
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
|
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
|
||||||
|
@ -5,3 +5,8 @@
|
|||||||
line-height: 1;
|
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 {
|
export interface DateSelection {
|
||||||
before?: string
|
before?: string
|
||||||
after?: string
|
after?: string
|
||||||
|
dateQuery?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QuickFilter {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
dateQuery: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const LAST_7_DAYS = 0
|
const LAST_7_DAYS = 0
|
||||||
@ -34,11 +41,23 @@ export class DateDropdownComponent implements OnInit, OnDestroy {
|
|||||||
this.datePlaceHolder = settings.getLocalizedDateInputFormat()
|
this.datePlaceHolder = settings.getLocalizedDateInputFormat()
|
||||||
}
|
}
|
||||||
|
|
||||||
quickFilters = [
|
quickFilters: Array<QuickFilter> = [
|
||||||
{ id: LAST_7_DAYS, name: $localize`Last 7 days` },
|
{
|
||||||
{ id: LAST_MONTH, name: $localize`Last month` },
|
id: LAST_7_DAYS,
|
||||||
{ id: LAST_3_MONTHS, name: $localize`Last 3 months` },
|
name: $localize`Last 7 days`,
|
||||||
{ id: LAST_YEAR, name: $localize`Last year` },
|
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
|
datePlaceHolder: string
|
||||||
@ -55,12 +74,36 @@ export class DateDropdownComponent implements OnInit, OnDestroy {
|
|||||||
@Output()
|
@Output()
|
||||||
dateAfterChange = new EventEmitter<string>()
|
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()
|
@Input()
|
||||||
title: string
|
title: string
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
datesSet = new EventEmitter<DateSelection>()
|
datesSet = new EventEmitter<DateSelection>()
|
||||||
|
|
||||||
|
get isActive(): boolean {
|
||||||
|
return (
|
||||||
|
this.quickFilter > -1 ||
|
||||||
|
this.dateAfter?.length > 0 ||
|
||||||
|
this.dateBefore?.length > 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private datesSetDebounce$ = new Subject()
|
private datesSetDebounce$ = new Subject()
|
||||||
|
|
||||||
private sub: Subscription
|
private sub: Subscription
|
||||||
@ -79,35 +122,28 @@ export class DateDropdownComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
setDateQuickFilter(qf: number) {
|
setDateQuickFilter(qf: number) {
|
||||||
this.dateBefore = null
|
this.dateBefore = null
|
||||||
let date = new Date()
|
this.dateAfter = null
|
||||||
switch (qf) {
|
this.quickFilter = this.quickFilter == qf ? null : 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.onChange()
|
this.onChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
qfIsSelected(qf: number) {
|
||||||
|
return this.quickFilter == qf
|
||||||
|
}
|
||||||
|
|
||||||
onChange() {
|
onChange() {
|
||||||
this.dateAfterChange.emit(this.dateAfter)
|
|
||||||
this.dateBeforeChange.emit(this.dateBefore)
|
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() {
|
onChangeDebounce() {
|
||||||
|
this.dateQuery = null
|
||||||
this.datesSetDebounce$.next({
|
this.datesSetDebounce$.next({
|
||||||
after: this.dateAfter,
|
after: this.dateAfter,
|
||||||
before: this.dateBefore,
|
before: this.dateBefore,
|
||||||
|
@ -54,12 +54,14 @@
|
|||||||
title="Created" i18n-title
|
title="Created" i18n-title
|
||||||
(datesSet)="updateRules()"
|
(datesSet)="updateRules()"
|
||||||
[(dateBefore)]="dateCreatedBefore"
|
[(dateBefore)]="dateCreatedBefore"
|
||||||
[(dateAfter)]="dateCreatedAfter"></app-date-dropdown>
|
[(dateAfter)]="dateCreatedAfter"
|
||||||
|
[(dateQuery)]="dateCreatedQuery"></app-date-dropdown>
|
||||||
<app-date-dropdown class="mb-2 mb-xl-0"
|
<app-date-dropdown class="mb-2 mb-xl-0"
|
||||||
|
title="Added" i18n-title
|
||||||
|
(datesSet)="updateRules()"
|
||||||
[(dateBefore)]="dateAddedBefore"
|
[(dateBefore)]="dateAddedBefore"
|
||||||
[(dateAfter)]="dateAddedAfter"
|
[(dateAfter)]="dateAddedAfter"
|
||||||
title="Added" i18n-title
|
[(dateQuery)]="dateAddedQuery"></app-date-dropdown>
|
||||||
(datesSet)="updateRules()"></app-date-dropdown>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -57,6 +57,9 @@ const TEXT_FILTER_MODIFIER_NOTNULL = 'not null'
|
|||||||
const TEXT_FILTER_MODIFIER_GT = 'greater'
|
const TEXT_FILTER_MODIFIER_GT = 'greater'
|
||||||
const TEXT_FILTER_MODIFIER_LT = 'less'
|
const TEXT_FILTER_MODIFIER_LT = 'less'
|
||||||
|
|
||||||
|
const RELATIVE_DATE_QUERY_REGEXP_CREATED = /created:\[([^\]]+)\]/g
|
||||||
|
const RELATIVE_DATE_QUERY_REGEXP_ADDED = /added:\[([^\]]+)\]/g
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-filter-editor',
|
selector: 'app-filter-editor',
|
||||||
templateUrl: './filter-editor.component.html',
|
templateUrl: './filter-editor.component.html',
|
||||||
@ -197,6 +200,8 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
|||||||
dateCreatedAfter: string
|
dateCreatedAfter: string
|
||||||
dateAddedBefore: string
|
dateAddedBefore: string
|
||||||
dateAddedAfter: string
|
dateAddedAfter: string
|
||||||
|
dateCreatedQuery: string
|
||||||
|
dateAddedQuery: string
|
||||||
|
|
||||||
_unmodifiedFilterRules: FilterRule[] = []
|
_unmodifiedFilterRules: FilterRule[] = []
|
||||||
_filterRules: FilterRule[] = []
|
_filterRules: FilterRule[] = []
|
||||||
@ -228,6 +233,8 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
|||||||
this.dateAddedAfter = null
|
this.dateAddedAfter = null
|
||||||
this.dateCreatedBefore = null
|
this.dateCreatedBefore = null
|
||||||
this.dateCreatedAfter = null
|
this.dateCreatedAfter = null
|
||||||
|
this.dateCreatedQuery = null
|
||||||
|
this.dateAddedQuery = null
|
||||||
this.textFilterModifier = TEXT_FILTER_MODIFIER_EQUALS
|
this.textFilterModifier = TEXT_FILTER_MODIFIER_EQUALS
|
||||||
|
|
||||||
value.forEach((rule) => {
|
value.forEach((rule) => {
|
||||||
@ -245,7 +252,30 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
|||||||
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
|
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
|
||||||
break
|
break
|
||||||
case FILTER_FULLTEXT_QUERY:
|
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
|
this.textFilterTarget = TEXT_FILTER_TARGET_FULLTEXT_QUERY
|
||||||
break
|
break
|
||||||
case FILTER_FULLTEXT_MORELIKE:
|
case FILTER_FULLTEXT_MORELIKE:
|
||||||
@ -471,6 +501,52 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
|||||||
value: this.dateAddedAfter,
|
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
|
return filterRules
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -584,6 +660,8 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
|||||||
target != TEXT_FILTER_TARGET_FULLTEXT_MORELIKE
|
target != TEXT_FILTER_TARGET_FULLTEXT_MORELIKE
|
||||||
) {
|
) {
|
||||||
this._textFilter = ''
|
this._textFilter = ''
|
||||||
|
this.dateAddedQuery = ''
|
||||||
|
this.dateCreatedQuery = ''
|
||||||
}
|
}
|
||||||
this.textFilterTarget = target
|
this.textFilterTarget = target
|
||||||
this.textFilterInput.nativeElement.focus()
|
this.textFilterInput.nativeElement.focus()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user