mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-28 18:24:38 -05:00
Allow filtering on multiple correspondents, doctypes, storage paths
Preserve 'Not assigned' option Fix default logical operator Update frontend strings Fix radio button name overlaps Use include / exclude with multi-select for OneToOne objects
This commit is contained in:
@@ -1,21 +1,29 @@
|
||||
<div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown">
|
||||
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
|
||||
<button class="btn btn-sm" id="dropdown_{{name}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
|
||||
</svg>
|
||||
<div class="d-none d-sm-inline"> {{title}}</div>
|
||||
<ng-container *ngIf="!editing && selectionModel.totalCount > 0">
|
||||
<app-clearable-badge [number]="multiple ? selectionModel.totalCount : undefined" [selected]="!multiple && selectionModel.selectionSize() > 0" (cleared)="reset()"></app-clearable-badge>
|
||||
<app-clearable-badge [number]="selectionModel.totalCount" [selected]="selectionModel.selectionSize() > 0" (cleared)="reset()"></app-clearable-badge>
|
||||
</ng-container>
|
||||
</button>
|
||||
<div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
||||
<div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown_{{name}}">
|
||||
<div class="list-group list-group-flush">
|
||||
<div *ngIf="!editing && multiple" class="list-group-item d-flex">
|
||||
<div class="btn-group btn-group-xs flex-fill">
|
||||
<input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!operatorToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorAnd" value="and">
|
||||
<label class="btn btn-outline-primary" for="logicalOperatorAnd" i18n>All</label>
|
||||
<input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!operatorToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorOr" value="or">
|
||||
<label class="btn btn-outline-primary" for="logicalOperatorOr" i18n>Any</label>
|
||||
<div *ngIf="!editing && manyToOne" class="list-group-item d-flex">
|
||||
<div class="btn-group btn-group-xs flex-fill" role="group">
|
||||
<input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorAnd_{{name}}" name="logicalOperatorAnd_{{name}}" value="and">
|
||||
<label class="btn btn-outline-primary" for="logicalOperatorAnd_{{name}}" i18n>All</label>
|
||||
<input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorOr_{{name}}" name="logicalOperatorOr_{{name}}" value="or">
|
||||
<label class="btn btn-outline-primary" for="logicalOperatorOr_{{name}}" i18n>Any</label>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!editing && !manyToOne" class="list-group-item d-flex">
|
||||
<div class="btn-group btn-group-xs flex-fill" role="group">
|
||||
<input [(ngModel)]="selectionModel.intersection" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleIntersection()" type="radio" class="btn-check" id="intersectionInclude_{{name}}" name="intersectionInclude_{{name}}" value="include">
|
||||
<label class="btn btn-outline-primary" for="intersectionInclude_{{name}}" i18n>Include</label>
|
||||
<input [(ngModel)]="selectionModel.intersection" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleIntersection()" type="radio" class="btn-check" id="intersectionExclude_{{name}}" name="intersectionExclude_{{name}}" value="exclude">
|
||||
<label class="btn btn-outline-primary" for="intersectionExclude_{{name}}" i18n>Exclude</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
@@ -34,7 +42,7 @@
|
||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
|
||||
</svg>
|
||||
</button>
|
||||
<div *ngIf="!editing && multiple" class="list-group-item list-group-item-note pt-1 pb-2">
|
||||
<div *ngIf="!editing && manyToOne" class="list-group-item list-group-item-note pt-1 pb-2">
|
||||
<small i18n>Click again to exclude items.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -18,12 +18,25 @@ export interface ChangedItems {
|
||||
itemsToRemove: MatchingModel[]
|
||||
}
|
||||
|
||||
export enum LogicalOperator {
|
||||
And = 'and',
|
||||
Or = 'or',
|
||||
}
|
||||
|
||||
export enum Intersection {
|
||||
Include = 'include',
|
||||
Exclude = 'exclude',
|
||||
}
|
||||
|
||||
export class FilterableDropdownSelectionModel {
|
||||
changed = new Subject<FilterableDropdownSelectionModel>()
|
||||
|
||||
multiple = false
|
||||
private _logicalOperator = 'and'
|
||||
temporaryLogicalOperator = this._logicalOperator
|
||||
manyToOne = false
|
||||
singleSelect = false
|
||||
private _logicalOperator: LogicalOperator = LogicalOperator.And
|
||||
temporaryLogicalOperator: LogicalOperator = this._logicalOperator
|
||||
private _intersection: Intersection = Intersection.Include
|
||||
temporaryIntersection: Intersection = this._intersection
|
||||
|
||||
items: MatchingModel[] = []
|
||||
|
||||
@@ -86,7 +99,30 @@ export class FilterableDropdownSelectionModel {
|
||||
(state != ToggleableItemState.Selected &&
|
||||
state != ToggleableItemState.Excluded)
|
||||
) {
|
||||
this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
|
||||
if (this.manyToOne || this.singleSelect) {
|
||||
this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
|
||||
|
||||
if (this.singleSelect) {
|
||||
for (let key of this.temporarySelectionStates.keys()) {
|
||||
if (key != id) {
|
||||
this.temporarySelectionStates.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let newState =
|
||||
this.intersection == Intersection.Include
|
||||
? ToggleableItemState.Selected
|
||||
: ToggleableItemState.Excluded
|
||||
if (!id) newState = ToggleableItemState.Selected
|
||||
if (
|
||||
state == ToggleableItemState.Excluded &&
|
||||
this.intersection == Intersection.Exclude
|
||||
) {
|
||||
newState = ToggleableItemState.NotSelected
|
||||
}
|
||||
this.temporarySelectionStates.set(id, newState)
|
||||
}
|
||||
} else if (
|
||||
state == ToggleableItemState.Selected ||
|
||||
state == ToggleableItemState.Excluded
|
||||
@@ -94,14 +130,6 @@ export class FilterableDropdownSelectionModel {
|
||||
this.temporarySelectionStates.delete(id)
|
||||
}
|
||||
|
||||
if (!this.multiple) {
|
||||
for (let key of this.temporarySelectionStates.keys()) {
|
||||
if (key != id) {
|
||||
this.temporarySelectionStates.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
for (let key of this.temporarySelectionStates.keys()) {
|
||||
if (key) {
|
||||
@@ -119,19 +147,36 @@ export class FilterableDropdownSelectionModel {
|
||||
|
||||
exclude(id: number, fireEvent: boolean = true) {
|
||||
let state = this.temporarySelectionStates.get(id)
|
||||
if (state == null || state != ToggleableItemState.Excluded) {
|
||||
this.temporarySelectionStates.set(id, ToggleableItemState.Excluded)
|
||||
this.temporaryLogicalOperator = this._logicalOperator = 'and'
|
||||
} else if (state == ToggleableItemState.Excluded) {
|
||||
this.temporarySelectionStates.delete(id)
|
||||
}
|
||||
if (id && (state == null || state != ToggleableItemState.Excluded)) {
|
||||
this.temporaryLogicalOperator = this._logicalOperator = this.manyToOne
|
||||
? LogicalOperator.And
|
||||
: LogicalOperator.Or
|
||||
|
||||
if (!this.multiple) {
|
||||
for (let key of this.temporarySelectionStates.keys()) {
|
||||
if (key != id) {
|
||||
this.temporarySelectionStates.delete(key)
|
||||
if (this.manyToOne || this.singleSelect) {
|
||||
this.temporarySelectionStates.set(id, ToggleableItemState.Excluded)
|
||||
|
||||
if (this.singleSelect) {
|
||||
for (let key of this.temporarySelectionStates.keys()) {
|
||||
if (key != id) {
|
||||
this.temporarySelectionStates.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let newState =
|
||||
this.intersection == Intersection.Include
|
||||
? ToggleableItemState.Selected
|
||||
: ToggleableItemState.Excluded
|
||||
if (
|
||||
state == ToggleableItemState.Selected &&
|
||||
this.intersection == Intersection.Include
|
||||
) {
|
||||
newState = ToggleableItemState.NotSelected
|
||||
}
|
||||
this.temporarySelectionStates.set(id, newState)
|
||||
}
|
||||
} else if (!id || state == ToggleableItemState.Excluded) {
|
||||
this.temporarySelectionStates.delete(id)
|
||||
}
|
||||
|
||||
if (fireEvent) {
|
||||
@@ -143,11 +188,11 @@ export class FilterableDropdownSelectionModel {
|
||||
return this.selectionStates.get(id) || ToggleableItemState.NotSelected
|
||||
}
|
||||
|
||||
get logicalOperator(): string {
|
||||
get logicalOperator(): LogicalOperator {
|
||||
return this.temporaryLogicalOperator
|
||||
}
|
||||
|
||||
set logicalOperator(operator: string) {
|
||||
set logicalOperator(operator: LogicalOperator) {
|
||||
this.temporaryLogicalOperator = operator
|
||||
}
|
||||
|
||||
@@ -155,6 +200,26 @@ export class FilterableDropdownSelectionModel {
|
||||
this.changed.next(this)
|
||||
}
|
||||
|
||||
get intersection(): Intersection {
|
||||
return this.temporaryIntersection
|
||||
}
|
||||
|
||||
set intersection(intersection: Intersection) {
|
||||
this.temporaryIntersection = intersection
|
||||
}
|
||||
|
||||
toggleIntersection() {
|
||||
if (this.temporarySelectionStates.size === 0) return
|
||||
let newState =
|
||||
this.intersection == Intersection.Include
|
||||
? ToggleableItemState.Selected
|
||||
: ToggleableItemState.Excluded
|
||||
this.temporarySelectionStates.forEach((state, key) => {
|
||||
this.temporarySelectionStates.set(key, newState)
|
||||
})
|
||||
this.changed.next(this)
|
||||
}
|
||||
|
||||
get(id: number) {
|
||||
return (
|
||||
this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected
|
||||
@@ -171,7 +236,8 @@ export class FilterableDropdownSelectionModel {
|
||||
|
||||
clear(fireEvent = true) {
|
||||
this.temporarySelectionStates.clear()
|
||||
this.temporaryLogicalOperator = this._logicalOperator = 'and'
|
||||
this.temporaryLogicalOperator = this._logicalOperator = LogicalOperator.And
|
||||
this.temporaryIntersection = this._intersection = Intersection.Include
|
||||
if (fireEvent) {
|
||||
this.changed.next(this)
|
||||
}
|
||||
@@ -194,6 +260,8 @@ export class FilterableDropdownSelectionModel {
|
||||
return true
|
||||
} else if (this.temporaryLogicalOperator !== this._logicalOperator) {
|
||||
return true
|
||||
} else if (this.temporaryIntersection !== this._intersection) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
@@ -217,13 +285,18 @@ export class FilterableDropdownSelectionModel {
|
||||
this.selectionStates.set(key, value)
|
||||
})
|
||||
this._logicalOperator = this.temporaryLogicalOperator
|
||||
this._intersection = this.temporaryIntersection
|
||||
}
|
||||
|
||||
reset() {
|
||||
reset(complete: boolean = false) {
|
||||
this.temporarySelectionStates.clear()
|
||||
this.selectionStates.forEach((value, key) => {
|
||||
this.temporarySelectionStates.set(key, value)
|
||||
})
|
||||
if (complete) {
|
||||
this.selectionStates.clear()
|
||||
} else {
|
||||
this.selectionStates.forEach((value, key) => {
|
||||
this.temporarySelectionStates.set(key, value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
diff(): ChangedItems {
|
||||
@@ -269,14 +342,16 @@ export class FilterableDropdownComponent {
|
||||
return this._selectionModel.items
|
||||
}
|
||||
|
||||
_selectionModel = new FilterableDropdownSelectionModel()
|
||||
_selectionModel: FilterableDropdownSelectionModel =
|
||||
new FilterableDropdownSelectionModel()
|
||||
|
||||
@Input()
|
||||
set selectionModel(model: FilterableDropdownSelectionModel) {
|
||||
if (this.selectionModel) {
|
||||
this.selectionModel.changed.complete()
|
||||
model.items = this.selectionModel.items
|
||||
model.multiple = this.selectionModel.multiple
|
||||
model.manyToOne = this.selectionModel.manyToOne
|
||||
model.singleSelect = this.editing && !this.selectionModel.manyToOne
|
||||
}
|
||||
model.changed.subscribe((updatedModel) => {
|
||||
this.selectionModelChange.next(updatedModel)
|
||||
@@ -292,12 +367,12 @@ export class FilterableDropdownComponent {
|
||||
selectionModelChange = new EventEmitter<FilterableDropdownSelectionModel>()
|
||||
|
||||
@Input()
|
||||
set multiple(value: boolean) {
|
||||
this.selectionModel.multiple = value
|
||||
set manyToOne(manyToOne: boolean) {
|
||||
this.selectionModel.manyToOne = manyToOne
|
||||
}
|
||||
|
||||
get multiple() {
|
||||
return this.selectionModel.multiple
|
||||
get manyToOne() {
|
||||
return this.selectionModel.manyToOne
|
||||
}
|
||||
|
||||
@Input()
|
||||
@@ -327,16 +402,20 @@ export class FilterableDropdownComponent {
|
||||
@Output()
|
||||
opened = new EventEmitter()
|
||||
|
||||
get operatorToggleEnabled(): boolean {
|
||||
return (
|
||||
this.selectionModel.selectionSize() > 1 &&
|
||||
this.selectionModel.getExcludedItems().length == 0
|
||||
)
|
||||
get modifierToggleEnabled(): boolean {
|
||||
return this.manyToOne
|
||||
? this.selectionModel.selectionSize() > 1 &&
|
||||
this.selectionModel.getExcludedItems().length == 0
|
||||
: !this.selectionModel.isNoneSelected()
|
||||
}
|
||||
|
||||
@Input()
|
||||
documentCounts: SelectionDataItem[]
|
||||
|
||||
get name(): string {
|
||||
return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
|
||||
}
|
||||
|
||||
getUpdatedDocumentCount(id: number) {
|
||||
if (this.documentCounts) {
|
||||
return this.documentCounts.find((c) => c.id === id)?.document_count
|
||||
@@ -346,7 +425,6 @@ export class FilterableDropdownComponent {
|
||||
modelIsDirty: boolean = false
|
||||
|
||||
constructor(private filterPipe: FilterPipe) {
|
||||
this.selectionModel = new FilterableDropdownSelectionModel()
|
||||
this.selectionModelChange.subscribe((updatedModel) => {
|
||||
this.modelIsDirty = updatedModel.isDirty()
|
||||
})
|
||||
@@ -400,7 +478,7 @@ export class FilterableDropdownComponent {
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.selectionModel.reset()
|
||||
this.selectionModel.reset(true)
|
||||
this.selectionModelChange.emit(this.selectionModel)
|
||||
}
|
||||
}
|
||||
|
@@ -30,7 +30,7 @@
|
||||
[items]="tags"
|
||||
[disabled]="!userCanEditAll"
|
||||
[editing]="true"
|
||||
[multiple]="true"
|
||||
[manyToOne]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
(opened)="openTagsDropdown()"
|
||||
[(selectionModel)]="tagSelectionModel"
|
||||
|
@@ -14,19 +14,19 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group flex-fill" role="group">
|
||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="details" (ngModelChange)="saveDisplayMode()" id="displayModeDetails">
|
||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="details" (ngModelChange)="saveDisplayMode()" id="displayModeDetails" name="displayModeDetails">
|
||||
<label for="displayModeDetails" class="btn btn-outline-primary btn-sm">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#list-ul" />
|
||||
</svg>
|
||||
</label>
|
||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="smallCards" (ngModelChange)="saveDisplayMode()" id="displayModeSmall">
|
||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="smallCards" (ngModelChange)="saveDisplayMode()" id="displayModeSmall" name="displayModeSmall">
|
||||
<label for="displayModeSmall" class="btn btn-outline-primary btn-sm">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#grid" />
|
||||
</svg>
|
||||
</label>
|
||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="largeCards" (ngModelChange)="saveDisplayMode()" id="displayModeLarge">
|
||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="largeCards" (ngModelChange)="saveDisplayMode()" id="displayModeLarge" name="displayModeLarge">
|
||||
<label for="displayModeLarge" class="btn btn-outline-primary btn-sm">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#hdd-stack" />
|
||||
|
@@ -27,7 +27,7 @@
|
||||
<app-filterable-dropdown class="flex-fill" title="Tags" icon="tag-fill" i18n-title
|
||||
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||
[items]="tags"
|
||||
[multiple]="true"
|
||||
[manyToOne]="true"
|
||||
[(selectionModel)]="tagSelectionModel"
|
||||
(selectionModelChange)="updateRules()"
|
||||
(opened)="onTagsDropdownOpen()"
|
||||
|
@@ -21,10 +21,10 @@ import {
|
||||
FILTER_ADDED_AFTER,
|
||||
FILTER_ADDED_BEFORE,
|
||||
FILTER_ASN,
|
||||
FILTER_CORRESPONDENT,
|
||||
FILTER_HAS_CORRESPONDENT_ANY,
|
||||
FILTER_CREATED_AFTER,
|
||||
FILTER_CREATED_BEFORE,
|
||||
FILTER_DOCUMENT_TYPE,
|
||||
FILTER_HAS_DOCUMENT_TYPE_ANY,
|
||||
FILTER_FULLTEXT_MORELIKE,
|
||||
FILTER_FULLTEXT_QUERY,
|
||||
FILTER_HAS_ANY_TAG,
|
||||
@@ -33,12 +33,22 @@ import {
|
||||
FILTER_DOES_NOT_HAVE_TAG,
|
||||
FILTER_TITLE,
|
||||
FILTER_TITLE_CONTENT,
|
||||
FILTER_STORAGE_PATH,
|
||||
FILTER_HAS_STORAGE_PATH_ANY,
|
||||
FILTER_ASN_ISNULL,
|
||||
FILTER_ASN_GT,
|
||||
FILTER_ASN_LT,
|
||||
FILTER_DOES_NOT_HAVE_CORRESPONDENT,
|
||||
FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
|
||||
FILTER_DOES_NOT_HAVE_STORAGE_PATH,
|
||||
FILTER_DOCUMENT_TYPE,
|
||||
FILTER_CORRESPONDENT,
|
||||
FILTER_STORAGE_PATH,
|
||||
} from 'src/app/data/filter-rule-type'
|
||||
import { FilterableDropdownSelectionModel } from '../../common/filterable-dropdown/filterable-dropdown.component'
|
||||
import {
|
||||
FilterableDropdownSelectionModel,
|
||||
Intersection,
|
||||
LogicalOperator,
|
||||
} from '../../common/filterable-dropdown/filterable-dropdown.component'
|
||||
import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'
|
||||
import {
|
||||
DocumentService,
|
||||
@@ -93,7 +103,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
if (this.filterRules.length == 1) {
|
||||
let rule = this.filterRules[0]
|
||||
switch (this.filterRules[0].rule_type) {
|
||||
case FILTER_CORRESPONDENT:
|
||||
case FILTER_HAS_CORRESPONDENT_ANY:
|
||||
if (rule.value) {
|
||||
return $localize`Correspondent: ${
|
||||
this.correspondents.find((c) => c.id == +rule.value)?.name
|
||||
@@ -102,7 +112,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
return $localize`Without correspondent`
|
||||
}
|
||||
|
||||
case FILTER_DOCUMENT_TYPE:
|
||||
case FILTER_HAS_DOCUMENT_TYPE_ANY:
|
||||
if (rule.value) {
|
||||
return $localize`Type: ${
|
||||
this.documentTypes.find((dt) => dt.id == +rule.value)?.name
|
||||
@@ -335,6 +345,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
this.dateAddedBefore = rule.value
|
||||
break
|
||||
case FILTER_HAS_TAGS_ALL:
|
||||
this.tagSelectionModel.logicalOperator = LogicalOperator.And
|
||||
this.tagSelectionModel.set(
|
||||
rule.value ? +rule.value : null,
|
||||
ToggleableItemState.Selected,
|
||||
@@ -342,7 +353,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
)
|
||||
break
|
||||
case FILTER_HAS_TAGS_ANY:
|
||||
this.tagSelectionModel.logicalOperator = 'or'
|
||||
this.tagSelectionModel.logicalOperator = LogicalOperator.Or
|
||||
this.tagSelectionModel.set(
|
||||
rule.value ? +rule.value : null,
|
||||
ToggleableItemState.Selected,
|
||||
@@ -360,26 +371,59 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
)
|
||||
break
|
||||
case FILTER_CORRESPONDENT:
|
||||
case FILTER_HAS_CORRESPONDENT_ANY:
|
||||
this.correspondentSelectionModel.logicalOperator = LogicalOperator.Or
|
||||
this.correspondentSelectionModel.intersection = Intersection.Include
|
||||
this.correspondentSelectionModel.set(
|
||||
rule.value ? +rule.value : null,
|
||||
ToggleableItemState.Selected,
|
||||
false
|
||||
)
|
||||
break
|
||||
case FILTER_DOES_NOT_HAVE_CORRESPONDENT:
|
||||
this.correspondentSelectionModel.intersection = Intersection.Exclude
|
||||
this.correspondentSelectionModel.set(
|
||||
rule.value ? +rule.value : null,
|
||||
ToggleableItemState.Excluded,
|
||||
false
|
||||
)
|
||||
break
|
||||
case FILTER_DOCUMENT_TYPE:
|
||||
case FILTER_HAS_DOCUMENT_TYPE_ANY:
|
||||
this.documentTypeSelectionModel.logicalOperator = LogicalOperator.Or
|
||||
this.documentTypeSelectionModel.intersection = Intersection.Include
|
||||
this.documentTypeSelectionModel.set(
|
||||
rule.value ? +rule.value : null,
|
||||
ToggleableItemState.Selected,
|
||||
false
|
||||
)
|
||||
break
|
||||
case FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE:
|
||||
this.documentTypeSelectionModel.intersection = Intersection.Exclude
|
||||
this.documentTypeSelectionModel.set(
|
||||
rule.value ? +rule.value : null,
|
||||
ToggleableItemState.Excluded,
|
||||
false
|
||||
)
|
||||
break
|
||||
case FILTER_STORAGE_PATH:
|
||||
case FILTER_HAS_STORAGE_PATH_ANY:
|
||||
this.storagePathSelectionModel.logicalOperator = LogicalOperator.Or
|
||||
this.storagePathSelectionModel.intersection = Intersection.Include
|
||||
this.storagePathSelectionModel.set(
|
||||
rule.value ? +rule.value : null,
|
||||
ToggleableItemState.Selected,
|
||||
false
|
||||
)
|
||||
break
|
||||
case FILTER_DOES_NOT_HAVE_STORAGE_PATH:
|
||||
this.storagePathSelectionModel.intersection = Intersection.Exclude
|
||||
this.storagePathSelectionModel.set(
|
||||
rule.value ? +rule.value : null,
|
||||
ToggleableItemState.Excluded,
|
||||
false
|
||||
)
|
||||
break
|
||||
case FILTER_ASN_ISNULL:
|
||||
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
|
||||
this.textFilterModifier =
|
||||
@@ -469,7 +513,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
filterRules.push({ rule_type: FILTER_HAS_ANY_TAG, value: 'false' })
|
||||
} else {
|
||||
const tagFilterType =
|
||||
this.tagSelectionModel.logicalOperator == 'and'
|
||||
this.tagSelectionModel.logicalOperator == LogicalOperator.And
|
||||
? FILTER_HAS_TAGS_ALL
|
||||
: FILTER_HAS_TAGS_ANY
|
||||
this.tagSelectionModel
|
||||
@@ -491,28 +535,66 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
})
|
||||
})
|
||||
}
|
||||
this.correspondentSelectionModel
|
||||
.getSelectedItems()
|
||||
.forEach((correspondent) => {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_CORRESPONDENT,
|
||||
value: correspondent.id?.toString(),
|
||||
if (this.correspondentSelectionModel.isNoneSelected()) {
|
||||
filterRules.push({ rule_type: FILTER_CORRESPONDENT, value: null })
|
||||
} else {
|
||||
this.correspondentSelectionModel
|
||||
.getSelectedItems()
|
||||
.forEach((correspondent) => {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_HAS_CORRESPONDENT_ANY,
|
||||
value: correspondent.id?.toString(),
|
||||
})
|
||||
})
|
||||
})
|
||||
this.documentTypeSelectionModel
|
||||
.getSelectedItems()
|
||||
.forEach((documentType) => {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_DOCUMENT_TYPE,
|
||||
value: documentType.id?.toString(),
|
||||
this.correspondentSelectionModel
|
||||
.getExcludedItems()
|
||||
.forEach((correspondent) => {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_DOES_NOT_HAVE_CORRESPONDENT,
|
||||
value: correspondent.id?.toString(),
|
||||
})
|
||||
})
|
||||
})
|
||||
this.storagePathSelectionModel.getSelectedItems().forEach((storagePath) => {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_STORAGE_PATH,
|
||||
value: storagePath.id?.toString(),
|
||||
})
|
||||
})
|
||||
}
|
||||
if (this.documentTypeSelectionModel.isNoneSelected()) {
|
||||
filterRules.push({ rule_type: FILTER_DOCUMENT_TYPE, value: null })
|
||||
} else {
|
||||
this.documentTypeSelectionModel
|
||||
.getSelectedItems()
|
||||
.forEach((documentType) => {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_HAS_DOCUMENT_TYPE_ANY,
|
||||
value: documentType.id?.toString(),
|
||||
})
|
||||
})
|
||||
this.documentTypeSelectionModel
|
||||
.getExcludedItems()
|
||||
.forEach((documentType) => {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
|
||||
value: documentType.id?.toString(),
|
||||
})
|
||||
})
|
||||
}
|
||||
if (this.storagePathSelectionModel.isNoneSelected()) {
|
||||
filterRules.push({ rule_type: FILTER_STORAGE_PATH, value: null })
|
||||
} else {
|
||||
this.storagePathSelectionModel
|
||||
.getSelectedItems()
|
||||
.forEach((storagePath) => {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_HAS_STORAGE_PATH_ANY,
|
||||
value: storagePath.id?.toString(),
|
||||
})
|
||||
})
|
||||
this.storagePathSelectionModel
|
||||
.getExcludedItems()
|
||||
.forEach((storagePath) => {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_DOES_NOT_HAVE_STORAGE_PATH,
|
||||
value: storagePath.id?.toString(),
|
||||
})
|
||||
})
|
||||
}
|
||||
if (this.dateCreatedBefore) {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_CREATED_BEFORE,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { FILTER_CORRESPONDENT } from 'src/app/data/filter-rule-type'
|
||||
import { FILTER_HAS_CORRESPONDENT_ANY } from 'src/app/data/filter-rule-type'
|
||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
@@ -35,7 +35,7 @@ export class CorrespondentListComponent extends ManagementListComponent<Paperles
|
||||
toastService,
|
||||
documentListViewService,
|
||||
permissionsService,
|
||||
FILTER_CORRESPONDENT,
|
||||
FILTER_HAS_CORRESPONDENT_ANY,
|
||||
$localize`correspondent`,
|
||||
$localize`correspondents`,
|
||||
PermissionType.Correspondent,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { FILTER_DOCUMENT_TYPE } from 'src/app/data/filter-rule-type'
|
||||
import { FILTER_HAS_DOCUMENT_TYPE_ANY } from 'src/app/data/filter-rule-type'
|
||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import {
|
||||
@@ -32,7 +32,7 @@ export class DocumentTypeListComponent extends ManagementListComponent<Paperless
|
||||
toastService,
|
||||
documentListViewService,
|
||||
permissionsService,
|
||||
FILTER_DOCUMENT_TYPE,
|
||||
FILTER_HAS_DOCUMENT_TYPE_ANY,
|
||||
$localize`document type`,
|
||||
$localize`document types`,
|
||||
PermissionType.DocumentType,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { FILTER_STORAGE_PATH } from 'src/app/data/filter-rule-type'
|
||||
import { FILTER_HAS_STORAGE_PATH_ANY } from 'src/app/data/filter-rule-type'
|
||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import {
|
||||
@@ -32,7 +32,7 @@ export class StoragePathListComponent extends ManagementListComponent<PaperlessS
|
||||
toastService,
|
||||
documentListViewService,
|
||||
permissionsService,
|
||||
FILTER_STORAGE_PATH,
|
||||
FILTER_HAS_STORAGE_PATH_ANY,
|
||||
$localize`storage path`,
|
||||
$localize`storage paths`,
|
||||
PermissionType.StoragePath,
|
||||
|
@@ -8,8 +8,12 @@ export const FILTER_ASN_GT = 23
|
||||
export const FILTER_ASN_LT = 24
|
||||
|
||||
export const FILTER_CORRESPONDENT = 3
|
||||
export const FILTER_HAS_CORRESPONDENT_ANY = 26
|
||||
export const FILTER_DOES_NOT_HAVE_CORRESPONDENT = 27
|
||||
|
||||
export const FILTER_DOCUMENT_TYPE = 4
|
||||
export const FILTER_HAS_DOCUMENT_TYPE_ANY = 28
|
||||
export const FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE = 29
|
||||
|
||||
export const FILTER_IS_IN_INBOX = 5
|
||||
export const FILTER_HAS_TAGS_ALL = 6
|
||||
@@ -18,6 +22,8 @@ export const FILTER_DOES_NOT_HAVE_TAG = 17
|
||||
export const FILTER_HAS_TAGS_ANY = 22
|
||||
|
||||
export const FILTER_STORAGE_PATH = 25
|
||||
export const FILTER_HAS_STORAGE_PATH_ANY = 30
|
||||
export const FILTER_DOES_NOT_HAVE_STORAGE_PATH = 31
|
||||
|
||||
export const FILTER_CREATED_BEFORE = 8
|
||||
export const FILTER_CREATED_AFTER = 9
|
||||
@@ -63,6 +69,18 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
|
||||
datatype: 'correspondent',
|
||||
multi: false,
|
||||
},
|
||||
{
|
||||
id: FILTER_HAS_CORRESPONDENT_ANY,
|
||||
filtervar: 'correspondent__id__in',
|
||||
datatype: 'correspondent',
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
id: FILTER_DOES_NOT_HAVE_CORRESPONDENT,
|
||||
filtervar: 'correspondent__id__none',
|
||||
datatype: 'correspondent',
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
id: FILTER_STORAGE_PATH,
|
||||
filtervar: 'storage_path__id',
|
||||
@@ -70,6 +88,18 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
|
||||
datatype: 'storage_path',
|
||||
multi: false,
|
||||
},
|
||||
{
|
||||
id: FILTER_HAS_STORAGE_PATH_ANY,
|
||||
filtervar: 'storage_path__id__in',
|
||||
datatype: 'storage_path',
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
id: FILTER_DOES_NOT_HAVE_STORAGE_PATH,
|
||||
filtervar: 'storage_path__id__none',
|
||||
datatype: 'storage_path',
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
id: FILTER_DOCUMENT_TYPE,
|
||||
filtervar: 'document_type__id',
|
||||
@@ -77,6 +107,18 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
|
||||
datatype: 'document_type',
|
||||
multi: false,
|
||||
},
|
||||
{
|
||||
id: FILTER_HAS_DOCUMENT_TYPE_ANY,
|
||||
filtervar: 'document_type__id__in',
|
||||
datatype: 'document_type',
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
id: FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
|
||||
filtervar: 'document_type__id__none',
|
||||
datatype: 'document_type',
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
id: FILTER_IS_IN_INBOX,
|
||||
filtervar: 'is_in_inbox',
|
||||
|
@@ -86,12 +86,12 @@ export function queryParamsFromFilterRules(filterRules: FilterRule[]): Params {
|
||||
let params = {}
|
||||
for (let rule of filterRules) {
|
||||
let ruleType = FILTER_RULE_TYPES.find((t) => t.id == rule.rule_type)
|
||||
if (ruleType.multi) {
|
||||
if (ruleType.isnull_filtervar && rule.value == null) {
|
||||
params[ruleType.isnull_filtervar] = 1
|
||||
} else if (ruleType.multi) {
|
||||
params[ruleType.filtervar] = params[ruleType.filtervar]
|
||||
? params[ruleType.filtervar] + ',' + rule.value
|
||||
: rule.value
|
||||
} else if (ruleType.isnull_filtervar && rule.value == null) {
|
||||
params[ruleType.isnull_filtervar] = 1
|
||||
} else {
|
||||
params[ruleType.filtervar] = rule.value
|
||||
if (ruleType.datatype == 'boolean')
|
||||
|
Reference in New Issue
Block a user