Merge pull request #4718 from MaximilianKrambach/sortTreeFilter
more sorting tree view filters
This commit is contained in:
commit
d9c5ff68b2
@ -103,6 +103,8 @@ export class SortingTreeComponent<T extends Identifiable & Displayable> implemen
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Function that will be used for filtering the nodes.
|
* Function that will be used for filtering the nodes.
|
||||||
|
* Should return false for an item is to be visible
|
||||||
|
* TODO this inverts all other 'sorting' conventions elsewhere!
|
||||||
*/
|
*/
|
||||||
private activeFilter: (node: T) => boolean;
|
private activeFilter: (node: T) => boolean;
|
||||||
|
|
||||||
|
@ -31,8 +31,8 @@
|
|||||||
<span class="sort-grid">
|
<span class="sort-grid">
|
||||||
<div class="hint">{{ 'Visibility' | translate }}</div>
|
<div class="hint">{{ 'Visibility' | translate }}</div>
|
||||||
<div>
|
<div>
|
||||||
<mat-checkbox *ngFor="let option of filterOptions" [(ngModel)]="option.state" (change)="onFilterChange(option.name)">
|
<mat-checkbox *ngFor="let option of filterOptions" [(ngModel)]="option.state" (change)="onFilterChange(option.id)">
|
||||||
<mat-icon matTooltip="{{ option.name | translate }}">{{ getIcon(option.name) }}</mat-icon> {{ option.name | translate }}
|
<mat-icon matTooltip="{{ option.label | translate }}">{{ getIcon(option.label) }}</mat-icon> {{ option.label | translate }}
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
import { Component, ViewChild, EventEmitter, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { MatSnackBar } from '@angular/material';
|
import { MatSnackBar } from '@angular/material';
|
||||||
|
|
||||||
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { Observable, BehaviorSubject } from 'rxjs';
|
|
||||||
|
|
||||||
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
|
|
||||||
import { BaseViewComponent } from '../../../base/base-view';
|
|
||||||
import { SortingTreeComponent } from 'app/shared/components/sorting-tree/sorting-tree.component';
|
|
||||||
import { ViewItem } from '../../models/view-item';
|
|
||||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
|
||||||
import { CanComponentDeactivate } from 'app/shared/utils/watch-sorting-tree.guard';
|
|
||||||
import { itemVisibilityChoices } from 'app/shared/models/agenda/item';
|
import { itemVisibilityChoices } from 'app/shared/models/agenda/item';
|
||||||
|
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
|
||||||
|
import { SortTreeViewComponent, SortTreeFilterOption } from 'app/site/base/sort-tree.component';
|
||||||
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
|
import { ViewItem } from '../../models/view-item';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort view for the agenda.
|
* Sort view for the agenda.
|
||||||
@ -21,58 +19,25 @@ import { itemVisibilityChoices } from 'app/shared/models/agenda/item';
|
|||||||
templateUrl: './agenda-sort.component.html',
|
templateUrl: './agenda-sort.component.html',
|
||||||
styleUrls: ['./agenda-sort.component.scss']
|
styleUrls: ['./agenda-sort.component.scss']
|
||||||
})
|
})
|
||||||
export class AgendaSortComponent extends BaseViewComponent implements CanComponentDeactivate, OnInit {
|
export class AgendaSortComponent extends SortTreeViewComponent<ViewItem> implements OnInit {
|
||||||
/**
|
/**
|
||||||
* Reference to the view child
|
* All agendaItems sorted by their weight {@link ViewItem.weight}
|
||||||
*/
|
*/
|
||||||
@ViewChild('osSortedTree')
|
public itemsObservable: Observable<ViewItem[]>;
|
||||||
public osSortTree: SortingTreeComponent<ViewItem>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitter to emit if the nodes should expand or collapse.
|
|
||||||
*/
|
|
||||||
public readonly changeState: EventEmitter<Boolean> = new EventEmitter<Boolean>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitter who emits the filters to the sorting tree.
|
|
||||||
*/
|
|
||||||
public readonly changeFilter: EventEmitter<(item: ViewItem) => boolean> = new EventEmitter<
|
|
||||||
(item: ViewItem) => boolean
|
|
||||||
>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* These are the available options for filtering the nodes.
|
* These are the available options for filtering the nodes.
|
||||||
* Adds the property `state` to identify if the option is marked as active.
|
* Adds the property `state` to identify if the option is marked as active.
|
||||||
* When reset the filters, the option `state` will be set to `false`.
|
* When reset the filters, the option `state` will be set to `false`.
|
||||||
*/
|
*/
|
||||||
public filterOptions = itemVisibilityChoices.map(item => {
|
public filterOptions: SortTreeFilterOption[] = itemVisibilityChoices.map(item => {
|
||||||
return { ...item, state: false };
|
return { label: item.name, id: item.key, state: false };
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BehaviourSubject to get informed every time the filters change.
|
* BehaviourSubject to get informed every time the filters change.
|
||||||
*/
|
*/
|
||||||
private activeFilters: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]);
|
protected activeFilters: BehaviorSubject<number[]> = new BehaviorSubject<number[]>([]);
|
||||||
|
|
||||||
/**
|
|
||||||
* Boolean to check if changes has been made.
|
|
||||||
*/
|
|
||||||
public hasChanged = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Boolean to check if filters are active, so they could be removed.
|
|
||||||
*/
|
|
||||||
public hasActiveFilter = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Array, that holds the number of visible nodes and amount of available nodes.
|
|
||||||
*/
|
|
||||||
public seenNodes: [number, number] = [0, 0];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All agendaItems sorted by their weight {@link ViewItem.weight}
|
|
||||||
*/
|
|
||||||
public itemsObservable: Observable<ViewItem[]>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the incoming/changing agenda items.
|
* Updates the incoming/changing agenda items.
|
||||||
@ -87,12 +52,30 @@ export class AgendaSortComponent extends BaseViewComponent implements CanCompone
|
|||||||
translate: TranslateService,
|
translate: TranslateService,
|
||||||
matSnackBar: MatSnackBar,
|
matSnackBar: MatSnackBar,
|
||||||
private agendaRepo: ItemRepositoryService,
|
private agendaRepo: ItemRepositoryService,
|
||||||
private promptService: PromptService
|
promptService: PromptService
|
||||||
) {
|
) {
|
||||||
super(title, translate, matSnackBar);
|
super(title, translate, matSnackBar, promptService);
|
||||||
this.itemsObservable = this.agendaRepo.getViewModelListObservable();
|
this.itemsObservable = this.agendaRepo.getViewModelListObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to emit the active filters.
|
||||||
|
* Filters will be stored in an array to prevent duplicated options.
|
||||||
|
* Furthermore if the option is already included in this array, then it will be deleted.
|
||||||
|
* This array will be emitted.
|
||||||
|
*
|
||||||
|
* @param filter Is the filter that was activated by the user.
|
||||||
|
*/
|
||||||
|
public onFilterChange(filter: number): void {
|
||||||
|
const value = this.activeFilters.value;
|
||||||
|
if (!value.includes(filter)) {
|
||||||
|
value.push(filter);
|
||||||
|
} else {
|
||||||
|
value.splice(value.indexOf(filter), 1);
|
||||||
|
}
|
||||||
|
this.activeFilters.next(value);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OnInit method
|
* OnInit method
|
||||||
*/
|
*/
|
||||||
@ -100,64 +83,17 @@ export class AgendaSortComponent extends BaseViewComponent implements CanCompone
|
|||||||
/**
|
/**
|
||||||
* Passes the active filters as an array to the subject.
|
* Passes the active filters as an array to the subject.
|
||||||
*/
|
*/
|
||||||
const filter = this.activeFilters.subscribe((value: string[]) => {
|
const filter = this.activeFilters.subscribe((value: number[]) => {
|
||||||
this.hasActiveFilter = value.length === 0 ? false : true;
|
this.hasActiveFilter = value.length === 0 ? false : true;
|
||||||
this.changeFilter.emit(
|
this.changeFilter.emit(
|
||||||
(item: ViewItem): boolean => {
|
(item: ViewItem): boolean => {
|
||||||
return !(value.includes(item.verboseType) || value.length === 0);
|
return !(value.includes(item.type) || value.length === 0);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
this.subscriptions.push(filter);
|
this.subscriptions.push(filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to save the tree by click.
|
|
||||||
*/
|
|
||||||
public async onSave(): Promise<void> {
|
|
||||||
await this.agendaRepo
|
|
||||||
.sortItems(this.osSortTree.getTreeData())
|
|
||||||
.then(() => this.osSortTree.setSubscription(), this.raiseError);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to restore the old state.
|
|
||||||
*/
|
|
||||||
public async onCancel(): Promise<void> {
|
|
||||||
if (await this.canDeactivate()) {
|
|
||||||
this.osSortTree.setSubscription();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to get an info if changes has been made.
|
|
||||||
*
|
|
||||||
* @param hasChanged Boolean received from the tree to see that changes has been made.
|
|
||||||
*/
|
|
||||||
public receiveChanges(hasChanged: boolean): void {
|
|
||||||
this.hasChanged = hasChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to receive the new number of visible nodes when the filter has changed.
|
|
||||||
*
|
|
||||||
* @param nextNumberOfSeenNodes is an array with two indices:
|
|
||||||
* The first gives the number of currently shown nodes.
|
|
||||||
* The second tells how many nodes available.
|
|
||||||
*/
|
|
||||||
public onChangeAmountOfItems(nextNumberOfSeenNodes: [number, number]): void {
|
|
||||||
this.seenNodes = nextNumberOfSeenNodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to emit if the nodes should be expanded or collapsed.
|
|
||||||
*
|
|
||||||
* @param nextState Is the next state, expanded or collapsed, the nodes should be.
|
|
||||||
*/
|
|
||||||
public onStateChange(nextState: boolean): void {
|
|
||||||
this.changeState.emit(nextState);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to set the active filters to null.
|
* Function to set the active filters to null.
|
||||||
*/
|
*/
|
||||||
@ -169,36 +105,21 @@ export class AgendaSortComponent extends BaseViewComponent implements CanCompone
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to emit the active filters.
|
* Function to save the tree by click.
|
||||||
* Filters will be stored in an array to prevent duplicated options.
|
|
||||||
* Furthermore if the option is already included in this array, then it will be deleted.
|
|
||||||
* This array will be emitted.
|
|
||||||
*
|
|
||||||
* @param filter Is the filter that was activated by the user.
|
|
||||||
*/
|
*/
|
||||||
public onFilterChange(filter: string): void {
|
public async onSave(): Promise<void> {
|
||||||
const value = this.activeFilters.value;
|
await this.agendaRepo
|
||||||
if (!value.includes(filter)) {
|
.sortItems(this.osSortTree.getTreeData())
|
||||||
value.push(filter);
|
.then(() => this.osSortTree.setSubscription(), this.raiseError);
|
||||||
} else {
|
|
||||||
value.splice(value.indexOf(filter), 1);
|
|
||||||
}
|
|
||||||
this.activeFilters.next(value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to open a prompt dialog,
|
* Function to emit if the nodes should be expanded or collapsed.
|
||||||
* so the user will be warned if he has made changes and not saved them.
|
|
||||||
*
|
*
|
||||||
* @returns The result from the prompt dialog.
|
* @param nextState Is the next state, expanded or collapsed, the nodes should be.
|
||||||
*/
|
*/
|
||||||
public async canDeactivate(): Promise<boolean> {
|
public onStateChange(nextState: boolean): void {
|
||||||
if (this.hasChanged) {
|
this.changeState.emit(nextState);
|
||||||
const title = this.translate.instant('Do you really want to exit this page?');
|
|
||||||
const content = this.translate.instant('You made changes.');
|
|
||||||
return await this.promptService.open(title, content);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
127
client/src/app/site/base/sort-tree.component.ts
Normal file
127
client/src/app/site/base/sort-tree.component.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { ViewChild, EventEmitter } from '@angular/core';
|
||||||
|
import { MatSnackBar } from '@angular/material';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { BaseViewModel } from './base-view-model';
|
||||||
|
import { BaseViewComponent } from './base-view';
|
||||||
|
import { CanComponentDeactivate } from 'app/shared/utils/watch-sorting-tree.guard';
|
||||||
|
import { Identifiable } from 'app/shared/models/base/identifiable';
|
||||||
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
|
import { SortingTreeComponent } from 'app/shared/components/sorting-tree/sorting-tree.component';
|
||||||
|
|
||||||
|
export interface SortTreeFilterOption extends Identifiable {
|
||||||
|
label: string;
|
||||||
|
id: number;
|
||||||
|
state: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract Sort view for hierarchic item trees
|
||||||
|
*/
|
||||||
|
export abstract class SortTreeViewComponent<V extends BaseViewModel> extends BaseViewComponent
|
||||||
|
implements CanComponentDeactivate {
|
||||||
|
/**
|
||||||
|
* Reference to the view child
|
||||||
|
*/
|
||||||
|
@ViewChild('osSortedTree')
|
||||||
|
public osSortTree: SortingTreeComponent<V>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitter to emit if the nodes should expand or collapse.
|
||||||
|
*/
|
||||||
|
public readonly changeState: EventEmitter<Boolean> = new EventEmitter<Boolean>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitter that emits the filters to the sorting tree.
|
||||||
|
* TODO note that the boolean function currently requires false if the item
|
||||||
|
* is to be visible!
|
||||||
|
*/
|
||||||
|
public readonly changeFilter: EventEmitter<(item: V) => boolean> = new EventEmitter<(item: V) => boolean>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boolean to check if changes has been made.
|
||||||
|
*/
|
||||||
|
public hasChanged = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boolean to check if filters are active, so they could be removed.
|
||||||
|
*/
|
||||||
|
public hasActiveFilter = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array that holds the number of visible nodes(0) and amount of available
|
||||||
|
* nodes(1).
|
||||||
|
*/
|
||||||
|
public seenNodes: [number, number] = [0, 0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the incoming/changing agenda items.
|
||||||
|
* @param title
|
||||||
|
* @param translate
|
||||||
|
* @param matSnackBar
|
||||||
|
* @param promptService
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
title: Title,
|
||||||
|
public translate: TranslateService,
|
||||||
|
matSnackBar: MatSnackBar,
|
||||||
|
protected promptService: PromptService
|
||||||
|
) {
|
||||||
|
super(title, translate, matSnackBar);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to restore the old state.
|
||||||
|
*/
|
||||||
|
public async onCancel(): Promise<void> {
|
||||||
|
if (await this.canDeactivate()) {
|
||||||
|
this.osSortTree.setSubscription();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to set an info if changes has been made.
|
||||||
|
*
|
||||||
|
* @param hasChanged Boolean received from the tree to see that changes has been made.
|
||||||
|
*/
|
||||||
|
public receiveChanges(hasChanged: boolean): void {
|
||||||
|
this.hasChanged = hasChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to receive the new number of visible nodes when the filter has changed.
|
||||||
|
*
|
||||||
|
* @param nextNumberOfSeenNodes is an array with two indices:
|
||||||
|
* The first gives the number of currently shown nodes.
|
||||||
|
* The second tells how many nodes available.
|
||||||
|
*/
|
||||||
|
public onChangeAmountOfItems(nextNumberOfSeenNodes: [number, number]): void {
|
||||||
|
this.seenNodes = nextNumberOfSeenNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to emit if the nodes should be expanded or collapsed.
|
||||||
|
*
|
||||||
|
* @param nextState Is the next state, expanded or collapsed, the nodes should be.
|
||||||
|
*/
|
||||||
|
public onStateChange(nextState: boolean): void {
|
||||||
|
this.changeState.emit(nextState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to open a prompt dialog, so the user will be warned if they have
|
||||||
|
* made changes and not saved them.
|
||||||
|
*
|
||||||
|
* @returns The result from the prompt dialog.
|
||||||
|
*/
|
||||||
|
public async canDeactivate(): Promise<boolean> {
|
||||||
|
if (this.hasChanged) {
|
||||||
|
const title = this.translate.instant('Do you really want to exit this page?');
|
||||||
|
const content = this.translate.instant('You made changes.');
|
||||||
|
return await this.promptService.open(title, content);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,4 @@
|
|||||||
<os-head-bar
|
<os-head-bar prevUrl="../.." [nav]="false" [editMode]="hasChanged" (mainEvent)="onCancel()" (saveEvent)="onSave()">
|
||||||
prevUrl="../.."
|
|
||||||
[nav]="false"
|
|
||||||
[editMode]="hasChanged"
|
|
||||||
(mainEvent)="onCancel()"
|
|
||||||
(saveEvent)="onSave()">
|
|
||||||
|
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div class="title-slot">
|
<div class="title-slot">
|
||||||
<h2 translate>Call list</h2>
|
<h2 translate>Call list</h2>
|
||||||
@ -12,19 +6,83 @@
|
|||||||
|
|
||||||
<!-- Export menu -->
|
<!-- Export menu -->
|
||||||
<div class="menu-slot">
|
<div class="menu-slot">
|
||||||
<button type="button" mat-icon-button [matMenuTriggerFor]="downloadMenu">
|
<button type="button" mat-icon-button [matMenuTriggerFor]="mainMenu">
|
||||||
<mat-icon>more_vert</mat-icon>
|
<mat-icon>more_vert</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
|
||||||
|
<div class="custom-table-header flex-spaced on-transition-fade">
|
||||||
|
<div class="filter-count">
|
||||||
|
<span> {{ seenNodes[0] }} </span><span translate>of</span>
|
||||||
|
<span> {{ seenNodes[1] }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="current-filters" *ngIf="hasActiveFilter">
|
||||||
|
<div><span translate>Active filters</span>: </div>
|
||||||
|
<div>
|
||||||
|
<button mat-button (click)="resetFilters()">
|
||||||
|
<mat-icon inline>cancel</mat-icon>
|
||||||
|
<span translate>Clear all</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-spaced margin-right-5">
|
||||||
|
<div *ngIf="categoryFilterOptions.length">
|
||||||
|
<button mat-button [matMenuTriggerFor]="catFilterMenu">
|
||||||
|
<span
|
||||||
|
class="upper"
|
||||||
|
[matBadge]="activeCatFilterCount > 0 ? activeCatFilterCount : null"
|
||||||
|
matBadgeColor="accent"
|
||||||
|
[matBadgeOverlap]="false"
|
||||||
|
translate
|
||||||
|
>
|
||||||
|
Categories
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="tagFilterOptions.length">
|
||||||
|
<button mat-button [matMenuTriggerFor]="tagFilterMenu">
|
||||||
|
<span
|
||||||
|
class="upper"
|
||||||
|
[matBadge]="activeTagFilterCount > 0 ? activeTagFilterCount : null"
|
||||||
|
matBadgeColor="accent"
|
||||||
|
[matBadgeOverlap]="false"
|
||||||
|
translate
|
||||||
|
>
|
||||||
|
Tags
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-menu #catFilterMenu="matMenu">
|
||||||
|
<div *ngFor="let catOption of categoryFilterOptions">
|
||||||
|
<button mat-menu-item (click)="onFilterChange('category', catOption.id)">
|
||||||
|
<mat-icon *ngIf="catOption.state">checked</mat-icon>
|
||||||
|
<span>{{ catOption.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</mat-menu>
|
||||||
|
<mat-menu #tagFilterMenu="matMenu">
|
||||||
|
<div *ngFor="let tagOption of tagFilterOptions">
|
||||||
|
<button mat-menu-item (click)="onFilterChange('tag', tagOption.id)">
|
||||||
|
<mat-icon *ngIf="tagOption.state">checked</mat-icon>
|
||||||
|
<span>{{ tagOption.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</mat-menu>
|
||||||
|
|
||||||
<mat-card>
|
<mat-card>
|
||||||
<os-sorting-tree
|
<os-sorting-tree
|
||||||
#osSortedTree
|
#osSortedTree
|
||||||
parentKey="sort_parent_id"
|
parentKey="sort_parent_id"
|
||||||
weightKey="weight"
|
weightKey="weight"
|
||||||
|
(visibleNodes)="onChangeAmountOfItems($event)"
|
||||||
(hasChanged)="receiveChanges($event)"
|
(hasChanged)="receiveChanges($event)"
|
||||||
[model]="motionsObservable">
|
[model]="motionsObservable"
|
||||||
|
[filterChange]="changeFilter"
|
||||||
|
>
|
||||||
<ng-template #innerNode let-item="item">
|
<ng-template #innerNode let-item="item">
|
||||||
<div class="line">
|
<div class="line">
|
||||||
<div class="left">
|
<div class="left">
|
||||||
@ -41,7 +99,7 @@
|
|||||||
</os-sorting-tree>
|
</os-sorting-tree>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|
||||||
<mat-menu #downloadMenu="matMenu">
|
<mat-menu #mainMenu="matMenu">
|
||||||
<button mat-menu-item (click)="pdfExportCallList()">
|
<button mat-menu-item (click)="pdfExportCallList()">
|
||||||
<mat-icon>picture_as_pdf</mat-icon>
|
<mat-icon>picture_as_pdf</mat-icon>
|
||||||
<span translate>Export as PDF</span>
|
<span translate>Export as PDF</span>
|
||||||
|
@ -20,3 +20,6 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.margin-right-5 {
|
||||||
|
margin-right: 5px !important;
|
||||||
|
}
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import { Component, ViewChild } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { MatSnackBar } from '@angular/material';
|
import { MatSnackBar } from '@angular/material';
|
||||||
|
|
||||||
|
import { Observable, BehaviorSubject } from 'rxjs';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
|
import { CategoryRepositoryService } from 'app/core/repositories/motions/category-repository.service';
|
||||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
|
||||||
import { CanComponentDeactivate } from 'app/shared/utils/watch-sorting-tree.guard';
|
|
||||||
import { FlatNode } from 'app/core/ui-services/tree.service';
|
import { FlatNode } from 'app/core/ui-services/tree.service';
|
||||||
import { MotionCsvExportService } from 'app/site/motions/services/motion-csv-export.service';
|
import { MotionCsvExportService } from 'app/site/motions/services/motion-csv-export.service';
|
||||||
import { MotionPdfExportService } from 'app/site/motions/services/motion-pdf-export.service';
|
import { MotionPdfExportService } from 'app/site/motions/services/motion-pdf-export.service';
|
||||||
|
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
|
||||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
import { SortingTreeComponent } from 'app/shared/components/sorting-tree/sorting-tree.component';
|
import { SortTreeViewComponent, SortTreeFilterOption } from 'app/site/base/sort-tree.component';
|
||||||
|
import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service';
|
||||||
import { ViewMotion } from 'app/site/motions/models/view-motion';
|
import { ViewMotion } from 'app/site/motions/models/view-motion';
|
||||||
import { ViewTag } from 'app/site/tags/models/view-tag';
|
import { ViewTag } from 'app/site/tags/models/view-tag';
|
||||||
|
|
||||||
@ -22,15 +22,12 @@ import { ViewTag } from 'app/site/tags/models/view-tag';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'os-call-list',
|
selector: 'os-call-list',
|
||||||
templateUrl: './call-list.component.html',
|
templateUrl: './call-list.component.html',
|
||||||
styleUrls: ['./call-list.component.scss']
|
styleUrls: [
|
||||||
|
'./call-list.component.scss',
|
||||||
|
'../../../../shared/components/sort-filter-bar/sort-filter-bar.component.scss'
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class CallListComponent extends BaseViewComponent implements CanComponentDeactivate {
|
export class CallListComponent extends SortTreeViewComponent<ViewMotion> implements OnInit {
|
||||||
/**
|
|
||||||
* Reference to the sorting tree.
|
|
||||||
*/
|
|
||||||
@ViewChild('osSortedTree')
|
|
||||||
private osSortTree: SortingTreeComponent<ViewMotion>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All motions sorted first by weight, then by id.
|
* All motions sorted first by weight, then by id.
|
||||||
*/
|
*/
|
||||||
@ -47,11 +44,46 @@ export class CallListComponent extends BaseViewComponent implements CanComponent
|
|||||||
public hasChanged = false;
|
public hasChanged = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the motions member, and sorts it.
|
* A (dynamically updating) list of available and active filters by tag
|
||||||
|
*/
|
||||||
|
public tagFilterOptions: SortTreeFilterOption[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current amount of tag filters active
|
||||||
|
*/
|
||||||
|
public activeTagFilterCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A (dynamically updating) list of available and active filters by category
|
||||||
|
*/
|
||||||
|
public categoryFilterOptions: SortTreeFilterOption[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current amount of category filters active
|
||||||
|
*/
|
||||||
|
public activeCatFilterCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BehaviorSubject to get informed every time the tag filter changes.
|
||||||
|
*/
|
||||||
|
protected activeTagFilters: BehaviorSubject<number[]> = new BehaviorSubject<number[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BehaviourSubject to get informed every time the category filter changes.
|
||||||
|
*/
|
||||||
|
protected activeCatFilters: BehaviorSubject<number[]> = new BehaviorSubject<number[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* constructor. Subscribes to the motions
|
||||||
|
*
|
||||||
* @param title
|
* @param title
|
||||||
* @param translate
|
* @param translate
|
||||||
* @param matSnackBar
|
* @param matSnackBar
|
||||||
* @param motionRepo
|
* @param motionRepo
|
||||||
|
* @param motionCsvExport
|
||||||
|
* @param motionPdfExport
|
||||||
|
* @param tagRepo
|
||||||
|
* @param categoryRepo
|
||||||
* @param promptService
|
* @param promptService
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -61,9 +93,11 @@ export class CallListComponent extends BaseViewComponent implements CanComponent
|
|||||||
private motionRepo: MotionRepositoryService,
|
private motionRepo: MotionRepositoryService,
|
||||||
private motionCsvExport: MotionCsvExportService,
|
private motionCsvExport: MotionCsvExportService,
|
||||||
private motionPdfExport: MotionPdfExportService,
|
private motionPdfExport: MotionPdfExportService,
|
||||||
private promptService: PromptService
|
private tagRepo: TagRepositoryService,
|
||||||
|
private categoryRepo: CategoryRepositoryService,
|
||||||
|
promptService: PromptService
|
||||||
) {
|
) {
|
||||||
super(title, translate, matSnackBar);
|
super(title, translate, matSnackBar, promptService);
|
||||||
|
|
||||||
this.motionsObservable = this.motionRepo.getViewModelListObservable();
|
this.motionsObservable = this.motionRepo.getViewModelListObservable();
|
||||||
this.motionsObservable.subscribe(motions => {
|
this.motionsObservable.subscribe(motions => {
|
||||||
@ -72,6 +106,75 @@ export class CallListComponent extends BaseViewComponent implements CanComponent
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes filters and filter subscriptions
|
||||||
|
*/
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.activeTagFilters.subscribe((value: number[]) => this.onSubscribedFilterChange('tag', value))
|
||||||
|
);
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.activeCatFilters.subscribe((value: number[]) => this.onSubscribedFilterChange('category', value))
|
||||||
|
);
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.tagRepo.getViewModelListBehaviorSubject().subscribe(tags => {
|
||||||
|
if (tags && tags.length) {
|
||||||
|
this.tagFilterOptions = tags.map(tag => {
|
||||||
|
return {
|
||||||
|
label: tag.name,
|
||||||
|
id: tag.id,
|
||||||
|
state:
|
||||||
|
this.tagFilterOptions &&
|
||||||
|
this.tagFilterOptions.some(tagfilter => {
|
||||||
|
return tagfilter.id === tag.id && tagfilter.state === true;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this.tagFilterOptions.push({
|
||||||
|
label: this.translate.instant('No tags'),
|
||||||
|
id: 0,
|
||||||
|
state:
|
||||||
|
this.tagFilterOptions &&
|
||||||
|
this.tagFilterOptions.some(tagfilter => {
|
||||||
|
return tagfilter.id === 0 && tagfilter.state === true;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.tagFilterOptions = [];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.categoryRepo.getViewModelListBehaviorSubject().subscribe(categories => {
|
||||||
|
if (categories && categories.length) {
|
||||||
|
this.categoryFilterOptions = categories.map(cat => {
|
||||||
|
return {
|
||||||
|
label: cat.prefixedName,
|
||||||
|
id: cat.id,
|
||||||
|
state:
|
||||||
|
this.categoryFilterOptions &&
|
||||||
|
this.categoryFilterOptions.some(catfilter => {
|
||||||
|
return catfilter.id === cat.id && catfilter.state === true;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this.categoryFilterOptions.push({
|
||||||
|
label: this.translate.instant('No category'),
|
||||||
|
id: 0,
|
||||||
|
state:
|
||||||
|
this.categoryFilterOptions &&
|
||||||
|
this.categoryFilterOptions.some(catfilter => {
|
||||||
|
return catfilter.id === 0 && catfilter.state === true;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.categoryFilterOptions = [];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to save changes on click.
|
* Function to save changes on click.
|
||||||
*/
|
*/
|
||||||
@ -91,7 +194,7 @@ export class CallListComponent extends BaseViewComponent implements CanComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to get an info if changes has been made.
|
* Function to register the info that changes has been made.
|
||||||
*
|
*
|
||||||
* @param hasChanged Boolean received from the tree to see that changes has been made.
|
* @param hasChanged Boolean received from the tree to see that changes has been made.
|
||||||
*/
|
*/
|
||||||
@ -113,21 +216,6 @@ export class CallListComponent extends BaseViewComponent implements CanComponent
|
|||||||
this.motionPdfExport.exportPdfCallList(this.motions);
|
this.motionPdfExport.exportPdfCallList(this.motions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to open a prompt dialog,
|
|
||||||
* so the user will be warned if he has made changes and not saved them.
|
|
||||||
*
|
|
||||||
* @returns The result from the prompt dialog.
|
|
||||||
*/
|
|
||||||
public async canDeactivate(): Promise<boolean> {
|
|
||||||
if (this.hasChanged) {
|
|
||||||
const title = this.translate.instant('Do you really want to exit this page?');
|
|
||||||
const content = this.translate.instant('You made changes.');
|
|
||||||
return await this.promptService.open(title, content);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the tags associated with the motion of a sorting item
|
* Get the tags associated with the motion of a sorting item
|
||||||
*
|
*
|
||||||
@ -138,4 +226,77 @@ export class CallListComponent extends BaseViewComponent implements CanComponent
|
|||||||
const motion = this.motionRepo.getViewModel(item.id);
|
const motion = this.motionRepo.getViewModel(item.id);
|
||||||
return motion ? motion.tags : [];
|
return motion ? motion.tags : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles a filter. An array with the filter ids array will be emitted
|
||||||
|
* as active/model/Filters
|
||||||
|
*
|
||||||
|
* @param model either 'tag' or 'category' for the filter that changed
|
||||||
|
* @param filter the filter that is supposed to be toggled.
|
||||||
|
*/
|
||||||
|
public onFilterChange(model: 'tag' | 'category', filter: number): void {
|
||||||
|
const value = model === 'tag' ? this.activeTagFilters.value : this.activeCatFilters.value;
|
||||||
|
if (!value.includes(filter)) {
|
||||||
|
value.push(filter);
|
||||||
|
} else {
|
||||||
|
value.splice(value.indexOf(filter), 1);
|
||||||
|
}
|
||||||
|
if (model === 'tag') {
|
||||||
|
this.activeTagFilters.next(value);
|
||||||
|
} else {
|
||||||
|
this.activeCatFilters.next(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to unset all active filters.
|
||||||
|
*/
|
||||||
|
public resetFilters(): void {
|
||||||
|
this.activeTagFilters.next([]);
|
||||||
|
this.activeCatFilters.next([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to trigger an update of the filter itself and the information about
|
||||||
|
* the state of filters
|
||||||
|
*
|
||||||
|
* @param model the property/model the filter is for
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
private onSubscribedFilterChange(model: 'tag' | 'category', value: number[]): void {
|
||||||
|
if (model === 'tag') {
|
||||||
|
this.activeTagFilterCount = value.length;
|
||||||
|
this.tagFilterOptions.forEach(filterOption => {
|
||||||
|
filterOption.state = value && value.some(v => v === filterOption.id);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.activeCatFilterCount = value.length;
|
||||||
|
this.categoryFilterOptions.forEach(filterOption => {
|
||||||
|
filterOption.state = value && value.some(v => v === filterOption.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.hasActiveFilter = this.activeCatFilterCount > 0 || this.activeTagFilterCount > 0;
|
||||||
|
|
||||||
|
const currentTagFilters = this.tagFilterOptions.filter(option => option.state === true);
|
||||||
|
const currentCategoryFilters = this.categoryFilterOptions.filter(option => option.state === true);
|
||||||
|
this.changeFilter.emit(
|
||||||
|
// TODO this is ugly and potentially reversed
|
||||||
|
(item: ViewMotion): boolean => {
|
||||||
|
if (currentTagFilters.length) {
|
||||||
|
if (!item.tags || !item.tags.length) {
|
||||||
|
if (!currentTagFilters.some(filter => filter.id === 0)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (!item.tags.some(tag => currentTagFilters.some(filter => filter.id === tag.id))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentCategoryFilters.length) {
|
||||||
|
const category_id = item.category_id || 0;
|
||||||
|
return !currentCategoryFilters.some(filter => filter.id === category_id);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user