Merge branch 'fix/issue-401' into feature/any-all-filtering

This commit is contained in:
Michael Shamoon
2021-01-21 14:29:31 -08:00
20 changed files with 215 additions and 150 deletions

View File

@@ -29,15 +29,22 @@
</div>
<div *ngIf="selectionModel.items" class="items">
<ng-container *ngFor="let item of selectionModel.itemsSorted | filter: filterText">
<app-toggleable-dropdown-button *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" (toggle)="selectionModel.toggle(item.id)"></app-toggleable-dropdown-button>
<app-toggleable-dropdown-button *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)"></app-toggleable-dropdown-button>
</ng-container>
</div>
<button *ngIf="editing" class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!selectionModel.isDirty()">
<small class="ml-1" [ngClass]="{'font-weight-bold': selectionModel.isDirty()}" i18n>Apply</small>
<small class="ml-2" [ngClass]="{'font-weight-bold': selectionModel.isDirty()}" i18n>Apply</small>
<svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
</svg>
</button>
<div *ngIf="!editing" class="list-group-item list-group-item-note pt-1 pb-2">
<small i18n>Use
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-option" viewBox="0 0 16 16">
<use xlink:href="assets/bootstrap-icons.svg#option" />
</svg>
+ click to exclude items.</small>
</div>
</div>
</div>
</div>

View File

@@ -35,3 +35,15 @@
.btn-group > label.disabled {
filter: brightness(0.5);
}
small > svg {
margin-top: -2px;
}
.list-group-item-note {
line-height: 1;
small {
font-size: 70%;
}
}

View File

@@ -45,6 +45,10 @@ export class FilterableDropdownSelectionModel {
return this.items.filter(i => this.temporarySelectionStates.get(i.id) == ToggleableItemState.Selected)
}
getExcludedItems() {
return this.items.filter(i => this.temporarySelectionStates.get(i.id) == ToggleableItemState.Excluded)
}
set(id: number, state: ToggleableItemState, fireEvent = true) {
if (state == ToggleableItemState.NotSelected) {
this.temporarySelectionStates.delete(id)
@@ -58,9 +62,9 @@ export class FilterableDropdownSelectionModel {
toggle(id: number, fireEvent = true) {
let state = this.temporarySelectionStates.get(id)
if (state == null || state != ToggleableItemState.Selected) {
if (state == null || (state != ToggleableItemState.Selected && state != ToggleableItemState.Excluded)) {
this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
} else if (state == ToggleableItemState.Selected) {
} else if (state == ToggleableItemState.Selected || state == ToggleableItemState.Excluded) {
this.temporarySelectionStates.delete(id)
}
@@ -85,7 +89,27 @@ export class FilterableDropdownSelectionModel {
if (fireEvent) {
this.changed.next(this)
}
}
exclude(id: number, fireEvent:boolean = true) {
let state = this.temporarySelectionStates.get(id)
if (state == null || state != ToggleableItemState.Excluded) {
this.temporarySelectionStates.set(id, ToggleableItemState.Excluded)
} else if (state == ToggleableItemState.Excluded) {
this.temporarySelectionStates.delete(id)
}
if (!this.multiple) {
for (let key of this.temporarySelectionStates.keys()) {
if (key != id) {
this.temporarySelectionStates.delete(key)
}
}
}
if (fireEvent) {
this.changed.next(this)
}
}
private getNonTemporary(id: number) {
@@ -287,4 +311,12 @@ export class FilterableDropdownComponent {
}
}
}
excludeClicked(itemID: number) {
if (this.editing) {
this.selectionModel.toggle(itemID)
} else {
this.selectionModel.exclude(itemID)
}
}
}

View File

@@ -1,4 +1,4 @@
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-left-0 border-right-0 border-bottom" role="menuitem" (click)="toggleItem()">
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-left-0 border-right-0 border-bottom" role="menuitem" (click)="toggleItem($event)">
<div class="selected-icon mr-1">
<ng-container *ngIf="isChecked()">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
@@ -10,10 +10,14 @@
<path d="M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8z"/>
</svg>
</ng-container>
<ng-container *ngIf="isExcluded()">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-x" viewBox="0 0 16 16">
<path 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>
</ng-container>
</div>
<div class="mr-1">
<app-tag *ngIf="isTag; else displayName" [tag]="item" [clickable]="true" linkTitle="Filter by tag"></app-tag>
<app-tag *ngIf="isTag; else displayName" [tag]="item" [clickable]="false"></app-tag>
<ng-template #displayName><small>{{item.name}}</small></ng-template>
</div>
<div class="badge badge-light rounded-pill ml-auto mr-1">{{item.document_count}}</div>

View File

@@ -4,7 +4,8 @@ import { MatchingModel } from 'src/app/data/matching-model';
export enum ToggleableItemState {
NotSelected = 0,
Selected = 1,
PartiallySelected = 2
PartiallySelected = 2,
Excluded = 3
}
@Component({
@@ -26,12 +27,19 @@ export class ToggleableDropdownButtonComponent {
@Output()
toggle = new EventEmitter()
@Output()
exclude = new EventEmitter()
get isTag(): boolean {
return 'is_inbox_tag' in this.item
}
toggleItem(): void {
this.toggle.emit()
toggleItem(event: MouseEvent): void {
if (event.altKey) {
this.exclude.emit()
} else {
this.toggle.emit()
}
}
isChecked() {
@@ -42,4 +50,7 @@ export class ToggleableDropdownButtonComponent {
return this.state == ToggleableItemState.PartiallySelected
}
isExcluded() {
return this.state == ToggleableItemState.Excluded
}
}

View File

@@ -1,6 +1,6 @@
<div class="card mb-3 shadow-sm" [class.card-selected]="selected" [class.document-card]="selectable" (click)="this.toggleSelected.emit($event)">
<div class="card mb-3 shadow-sm" [class.card-selected]="selected" [class.document-card]="selectable">
<div class="row no-gutters">
<div class="col-md-2 d-none d-lg-block doc-img-background rounded-left" [class.doc-img-background-selected]="selected">
<div class="col-md-2 d-none d-lg-block doc-img-background rounded-left" [class.doc-img-background-selected]="selected" (click)="this.toggleSelected.emit($event)">
<img [src]="getThumbUrl()" class="card-img doc-img border-right rounded-left">
<div style="top: 0; left: 0" class="position-absolute border-right border-bottom bg-light p-1" [class.document-card-check]="!selected">

View File

@@ -1,6 +1,6 @@
<div class="col p-2 h-100">
<div class="card h-100 shadow-sm document-card" [class.card-selected]="selected" (click)="this.toggleSelected.emit($event)">
<div class="border-bottom doc-img-container" [class.doc-img-background-selected]="selected">
<div class="card h-100 shadow-sm document-card" [class.card-selected]="selected">
<div class="border-bottom doc-img-container" [class.doc-img-background-selected]="selected" (click)="this.toggleSelected.emit($event)">
<img class="card-img doc-img rounded-top" [src]="getThumbUrl()">
<div class="border-right border-bottom bg-light p-1 rounded document-card-check">

View File

@@ -152,7 +152,7 @@
</td>
<td>
<a routerLink="/documents/{{d.id}}" title="Edit document" style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
<app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ml-1" clickable="true" linkTitle="Filter by tag" (click)="clickTag(t.id)"></app-tag>
<app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ml-1" clickable="true" linkTitle="Filter by tag" (click)="clickTag(t.id);$event.stopPropagation()"></app-tag>
</td>
<td class="d-none d-xl-table-cell">
<ng-container *ngIf="d.document_type">

View File

@@ -8,7 +8,7 @@ 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_ANY_TAG, FILTER_HAS_TAGS_ALL, FILTER_HAS_TAGS_ANY, FILTER_TITLE } from 'src/app/data/filter-rule-type';
import { FILTER_ADDED_AFTER, FILTER_ADDED_BEFORE, FILTER_CORRESPONDENT, FILTER_CREATED_AFTER, FILTER_CREATED_BEFORE, FILTER_DOCUMENT_TYPE, FILTER_HAS_ANY_TAG, FILTER_HAS_TAGS_ALL, FILTER_HAS_TAGS_ANY, FILTER_DOES_NOT_HAVE_TAG, FILTER_TITLE } from 'src/app/data/filter-rule-type';
import { FilterableDropdownSelectionModel } from '../../common/filterable-dropdown/filterable-dropdown.component';
import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component';
@@ -111,6 +111,9 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
case FILTER_HAS_ANY_TAG:
this.tagSelectionModel.set(null, ToggleableItemState.Selected, false)
break
case FILTER_DOES_NOT_HAVE_TAG:
this.tagSelectionModel.set(rule.value ? +rule.value : null, ToggleableItemState.Excluded, false)
break
case FILTER_CORRESPONDENT:
this.correspondentSelectionModel.set(rule.value ? +rule.value : null, ToggleableItemState.Selected, false)
break
@@ -133,6 +136,9 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
this.tagSelectionModel.getSelectedItems().filter(tag => tag.id).forEach(tag => {
filterRules.push({rule_type: tagFilterType, value: tag.id?.toString()})
})
this.tagSelectionModel.getExcludedItems().filter(tag => tag.id).forEach(tag => {
filterRules.push({rule_type: FILTER_DOES_NOT_HAVE_TAG, value: tag.id?.toString()})
})
}
this.correspondentSelectionModel.getSelectedItems().forEach(correspondent => {
filterRules.push({rule_type: FILTER_CORRESPONDENT, value: correspondent.id?.toString()})