Refactor editor and filterable dropdowns to be common components in anticipation of bulk editor

This commit is contained in:
Michael Shamoon
2020-12-18 16:03:52 -08:00
parent d3b63c1bc3
commit bbf87f11f4
20 changed files with 105 additions and 105 deletions

View File

@@ -11,7 +11,7 @@ import { DocumentService, DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/doc
import { TagService } from 'src/app/services/rest/tag.service';
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
import { Toast, ToastService } from 'src/app/services/toast.service';
import { FilterEditorComponent } from '../filter-editor/filter-editor.component';
import { FilterEditorComponent } from './filter-editor/filter-editor.component';
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component';
import { SelectDialogComponent } from '../common/select-dialog/select-dialog.component';
import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component';

View File

@@ -0,0 +1,27 @@
<div class="row">
<div class="col mb-2 mb-xl-0">
<div class="form-inline d-flex">
<label class="text-muted mr-2">Filter by:</label>
<input class="form-control form-control-sm flex-grow-1" type="text" [(ngModel)]="titleFilter" placeholder="Title">
</div>
</div>
<div class="w-100 d-xl-none"></div>
<div class="col col-xl-auto mb-2 mb-xl-0">
<div class="d-flex">
<app-filterable-dropdown class="mr-2 mr-md-3" [items]="tags" [itemsSelected]="selectedTags" title="Tags" icon="tag-fill" (toggle)="toggleTag($event.id)"></app-filterable-dropdown>
<app-filterable-dropdown class="mr-2 mr-md-3" [items]="correspondents" [itemsSelected]="selectedCorrespondents" title="Correspondents" icon="person-fill" (toggle)="toggleCorrespondent($event.id)"></app-filterable-dropdown>
<app-filterable-dropdown class="mr-2 mr-md-3" [items]="documentTypes" [itemsSelected]="selectedDocumentTypes" title="Document types" icon="file-earmark-fill" (toggle)="toggleDocumentType($event.id)"></app-filterable-dropdown>
<app-date-dropdown class="mr-2 mr-md-3" [dateBefore]="dateCreatedBefore" [dateAfter]="dateCreatedAfter" title="Created" (datesSet)="onDatesCreatedSet($event)"></app-date-dropdown>
<app-date-dropdown [dateBefore]="dateAddedBefore" [dateAfter]="dateAddedAfter" title="Added" (datesSet)="onDatesAddedSet($event)"></app-date-dropdown>
</div>
</div>
<div class="w-100 d-xl-none"></div>
<div class="col col-xl-auto mb-2 mb-xl-0">
<button class="btn btn-link btn-sm px-0 mx-0 ml-xl-n4" [disabled]="!hasFilters()" (click)="clearSelected()">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
</svg>
Clear all filters
</button>
</div>
</div>

View File

@@ -0,0 +1,10 @@
.quick-filter {
min-width: 250px;
max-height: 400px;
overflow-y: scroll;
.selected-icon {
min-width: 1em;
min-height: 1em;
}
}

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FilterEditorComponent } from './filter-editor.component';
describe('FilterEditorComponent', () => {
let component: FilterEditorComponent;
let fixture: ComponentFixture<FilterEditorComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FilterEditorComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FilterEditorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,239 @@
import { Component, EventEmitter, Input, Output, OnInit, OnDestroy } from '@angular/core';
import { PaperlessTag } from 'src/app/data/paperless-tag';
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { NgbDateParserFormatter, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
import { TagService } from 'src/app/services/rest/tag.service';
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
import { FilterRule } from 'src/app/data/filter-rule';
import { FILTER_ADDED_AFTER, FILTER_ADDED_BEFORE, FILTER_CORRESPONDENT, FILTER_CREATED_AFTER, FILTER_CREATED_BEFORE, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_RULE_TYPES, FILTER_TITLE } from 'src/app/data/filter-rule-type';
import { DateSelection } from 'src/app/components/common/date-dropdown/date-dropdown.component';
@Component({
selector: 'app-filter-editor',
templateUrl: './filter-editor.component.html',
styleUrls: ['./filter-editor.component.scss']
})
export class FilterEditorComponent implements OnInit, OnDestroy {
generateFilterName() {
if (this.filterRules.length == 1) {
let rule = this.filterRules[0]
switch(this.filterRules[0].rule_type) {
case FILTER_CORRESPONDENT:
return `Correspondent: ${this.correspondents.find(c => c.id == +rule.value)?.name}`
case FILTER_DOCUMENT_TYPE:
return `Type: ${this.documentTypes.find(dt => dt.id == +rule.value)?.name}`
case FILTER_HAS_TAG:
return `Tag: ${this.tags.find(t => t.id == +rule.value)?.name}`
}
}
return ""
}
constructor(
private documentTypeService: DocumentTypeService,
private tagService: TagService,
private correspondentService: CorrespondentService,
private dateParser: NgbDateParserFormatter
) { }
tags: PaperlessTag[] = []
correspondents: PaperlessCorrespondent[]
documentTypes: PaperlessDocumentType[] = []
@Input()
filterRules: FilterRule[]
@Output()
filterRulesChange = new EventEmitter<FilterRule[]>()
hasFilters() {
return this.filterRules.length > 0
}
get selectedTags(): PaperlessTag[] {
let tagRules: FilterRule[] = this.filterRules.filter(fr => fr.rule_type == FILTER_HAS_TAG)
return this.tags?.filter(t => tagRules.find(tr => +tr.value == t.id))
}
get selectedCorrespondents(): PaperlessCorrespondent[] {
let correspondentRules: FilterRule[] = this.filterRules.filter(fr => fr.rule_type == FILTER_CORRESPONDENT)
return this.correspondents?.filter(c => correspondentRules.find(cr => +cr.value == c.id))
}
get selectedDocumentTypes(): PaperlessDocumentType[] {
let documentTypeRules: FilterRule[] = this.filterRules.filter(fr => fr.rule_type == FILTER_DOCUMENT_TYPE)
return this.documentTypes?.filter(dt => documentTypeRules.find(dtr => +dtr.value == dt.id))
}
get titleFilter() {
let existingRule = this.filterRules.find(rule => rule.rule_type == FILTER_TITLE)
return existingRule ? existingRule.value : ''
}
set titleFilter(value) {
this.titleFilterDebounce.next(value)
}
titleFilterDebounce: Subject<string>
subscription: Subscription
ngOnInit() {
this.tagService.listAll().subscribe(result => this.tags = result.results)
this.correspondentService.listAll().subscribe(result => this.correspondents = result.results)
this.documentTypeService.listAll().subscribe(result => this.documentTypes = result.results)
this.titleFilterDebounce = new Subject<string>()
this.subscription = this.titleFilterDebounce.pipe(
debounceTime(400),
distinctUntilChanged()
).subscribe(title => {
this.setTitleRule(title)
})
}
ngOnDestroy() {
this.titleFilterDebounce.complete()
// TODO: not sure if both is necessary
this.subscription.unsubscribe()
}
applyFilters() {
this.filterRulesChange.next(this.filterRules)
}
clearSelected() {
this.filterRules = []
this.applyFilters()
}
private toggleFilterRule(filterRuleTypeID: number, value: number) {
let filterRuleType = FILTER_RULE_TYPES.find(t => t.id == filterRuleTypeID)
let existingRule = this.filterRules.find(rule => rule.rule_type == filterRuleTypeID && rule.value == value?.toString())
let existingRuleOfSameType = this.filterRules.find(rule => rule.rule_type == filterRuleTypeID)
if (existingRule) {
// if this exact rule already exists, remove it in all cases.
this.filterRules.splice(this.filterRules.indexOf(existingRule), 1)
} else if (filterRuleType.multi || !existingRuleOfSameType) {
// if we allow multiple rules per type, or no rule of this type already exists, push a new rule.
this.filterRules.push({rule_type: filterRuleTypeID, value: value?.toString()})
} else {
// otherwise (i.e., no multi support AND there's already a rule of this type), update the rule.
existingRuleOfSameType.value = value?.toString()
}
this.applyFilters()
}
private setTitleRule(title: string) {
let existingRule = this.filterRules.find(rule => rule.rule_type == FILTER_TITLE)
if (!existingRule && title) {
this.filterRules.push({rule_type: FILTER_TITLE, value: title})
} else if (existingRule && !title) {
this.filterRules.splice(this.filterRules.findIndex(rule => rule.rule_type == FILTER_TITLE), 1)
} else if (existingRule && title) {
existingRule.value = title
}
this.applyFilters()
}
toggleTag(tagId: number) {
this.toggleFilterRule(FILTER_HAS_TAG, tagId)
}
toggleCorrespondent(correspondentId: number) {
this.toggleFilterRule(FILTER_CORRESPONDENT, correspondentId)
}
toggleDocumentType(documentTypeId: number) {
this.toggleFilterRule(FILTER_DOCUMENT_TYPE, documentTypeId)
}
// Date handling
onDatesCreatedSet(dates: DateSelection) {
this.setDateCreatedBefore(dates.before)
this.setDateCreatedAfter(dates.after)
this.applyFilters()
}
onDatesAddedSet(dates: DateSelection) {
this.setDateAddedBefore(dates.before)
this.setDateAddedAfter(dates.after)
this.applyFilters()
}
get dateCreatedBefore(): string {
let createdBeforeRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_CREATED_BEFORE)
return createdBeforeRule ? createdBeforeRule.value : null
}
get dateCreatedAfter(): string {
let createdAfterRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_CREATED_AFTER)
return createdAfterRule ? createdAfterRule.value : null
}
get dateAddedBefore(): string {
let addedBeforeRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_ADDED_BEFORE)
return addedBeforeRule ? addedBeforeRule.value : null
}
get dateAddedAfter(): string {
let addedAfterRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_ADDED_AFTER)
return addedAfterRule ? addedAfterRule.value : null
}
setDateCreatedBefore(date?: string) {
if (date) this.setDateFilter(date, FILTER_CREATED_BEFORE)
else this.clearDateFilter(FILTER_CREATED_BEFORE)
}
setDateCreatedAfter(date?: string) {
if (date) this.setDateFilter(date, FILTER_CREATED_AFTER)
else this.clearDateFilter(FILTER_CREATED_AFTER)
}
setDateAddedBefore(date?: string) {
if (date) this.setDateFilter(date, FILTER_ADDED_BEFORE)
else this.clearDateFilter(FILTER_ADDED_BEFORE)
}
setDateAddedAfter(date?: string) {
if (date) this.setDateFilter(date, FILTER_ADDED_AFTER)
else this.clearDateFilter(FILTER_ADDED_AFTER)
}
setDateFilter(date: string, dateRuleTypeID: number) {
let existingRule = this.filterRules.find(rule => rule.rule_type == dateRuleTypeID)
if (existingRule) {
existingRule.value = date
} else {
this.filterRules.push({rule_type: dateRuleTypeID, value: date})
}
}
clearDateFilter(dateRuleTypeID: number) {
let ruleIndex = this.filterRules.findIndex(rule => rule.rule_type == dateRuleTypeID)
if (ruleIndex != -1) {
this.filterRules.splice(ruleIndex, 1)
}
}
}