Merge pull request #3963 from MaximilianKrambach/os3/sortSearch
Sorting/Filters for list views
This commit is contained in:
commit
405ddaec60
18
client/src/app/core/services/filter-list.service.spec.ts
Normal file
18
client/src/app/core/services/filter-list.service.spec.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FilterListService } from './filter-list.service';
|
||||
import { E2EImportsModule } from '../../../e2e-imports.module';
|
||||
|
||||
describe('FilterListService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [FilterListService]
|
||||
});
|
||||
});
|
||||
|
||||
// TODO testing needs a BaseViewComponent
|
||||
// it('should be created', inject([FilterListService], (service: FilterListService) => {
|
||||
// expect(service).toBeTruthy();
|
||||
// }));
|
||||
});
|
290
client/src/app/core/services/filter-list.service.ts
Normal file
290
client/src/app/core/services/filter-list.service.ts
Normal file
@ -0,0 +1,290 @@
|
||||
import { auditTime } from 'rxjs/operators';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
import { BaseModel } from '../../shared/models/base/base-model';
|
||||
import { BaseViewModel } from '../../site/base/base-view-model';
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
|
||||
/**
|
||||
* Describes the available filters for a listView.
|
||||
* @param isActive: the current state of the filter
|
||||
* @param property: the ViewModel's property or method to filter by
|
||||
* @param label: An optional, different label (if not present, the property will be used)
|
||||
* @param condition: The conditions to be met for a successful display of data. These will
|
||||
* be updated by the {@link filterMenu}
|
||||
* @param options a list of available options for a filter
|
||||
*/
|
||||
export interface OsFilter {
|
||||
property: string;
|
||||
label?: string;
|
||||
options: (OsFilterOption | string )[];
|
||||
count?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a list of available options for a drop down menu of a filter
|
||||
*/
|
||||
export interface OsFilterOption {
|
||||
label: string;
|
||||
condition: string | boolean | number;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Filter for the list view. List views can subscribe to its' dataService (providing filter definitions)
|
||||
* and will receive their filtered data as observable
|
||||
*/
|
||||
|
||||
export abstract class FilterListService<M extends BaseModel, V extends BaseViewModel> {
|
||||
|
||||
/**
|
||||
* stores the currently used raw data to be used for the filter
|
||||
*/
|
||||
private currentRawData: V[];
|
||||
|
||||
/**
|
||||
* The currently used filters.
|
||||
*/
|
||||
public filterDefinitions: OsFilter[];
|
||||
|
||||
/**
|
||||
* The observable output for the filtered data
|
||||
*/
|
||||
public filterDataOutput = new BehaviorSubject<V[]>([]);
|
||||
|
||||
protected filteredData: V[];
|
||||
|
||||
protected name: string;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public constructor(private store: StorageService, private repo: any) {
|
||||
// repo( extends BaseRepository<V, M> ) { // TODO
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the filterService. Returns the filtered data as Observable
|
||||
*/
|
||||
public filter(): Observable<V[]> {
|
||||
this.repo.getViewModelListObservable().pipe(auditTime(100)).subscribe( data => {
|
||||
this.currentRawData = data;
|
||||
this.filteredData = this.filterData(data);
|
||||
this.filterDataOutput.next(this.filteredData);
|
||||
});
|
||||
this.loadStorageDefinition(this.filterDefinitions);
|
||||
return this.filterDataOutput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a newly created filter
|
||||
* @param filter
|
||||
*/
|
||||
public addFilterOption(filterName: string, option: OsFilterOption): void {
|
||||
const filter = this.filterDefinitions.find(f => f.property === filterName );
|
||||
if (filter) {
|
||||
const filterOption = filter.options.find(o =>
|
||||
(typeof o !== 'string') && o.condition === option.condition) as OsFilterOption;
|
||||
if (filterOption && !filterOption.isActive){
|
||||
filterOption.isActive = true;
|
||||
filter.count += 1;
|
||||
}
|
||||
if (filter.count === 1) {
|
||||
this.filteredData = this.filterData(this.filteredData);
|
||||
} else {
|
||||
this.filteredData = this.filterData(this.currentRawData);
|
||||
}
|
||||
this.filterDataOutput.next(this.filteredData);
|
||||
this.setStorageDefinition();
|
||||
}
|
||||
}
|
||||
|
||||
public removeFilterOption(filterName: string, option: OsFilterOption): void {
|
||||
const filter = this.filterDefinitions.find(f => f.property === filterName );
|
||||
if (filter) {
|
||||
const filterOption = filter.options.find(o =>
|
||||
(typeof o !== 'string') && o.condition === option.condition) as OsFilterOption;
|
||||
if (filterOption && filterOption.isActive){
|
||||
filterOption.isActive = false;
|
||||
filter.count -= 1;
|
||||
this.filteredData = this.filterData(this.currentRawData);
|
||||
this.filterDataOutput.next(this.filteredData);
|
||||
this.setStorageDefinition();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles a filter option, to be called after a checkbox state has changed.
|
||||
* @param filterName
|
||||
* @param option
|
||||
*/
|
||||
public toggleFilterOption(filterName: string, option: OsFilterOption): void {
|
||||
option.isActive ? this.removeFilterOption(filterName, option) : this.addFilterOption(filterName, option);
|
||||
}
|
||||
|
||||
public updateFilterDefinitions(filters: OsFilter[]) : void {
|
||||
this.loadStorageDefinition(filters);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the currently saved filter definition from the StorageService,
|
||||
* check their match with current definitions and set the current filter
|
||||
* @param definitions: Currently defined Filter definitions
|
||||
*/
|
||||
private loadStorageDefinition(definitions: OsFilter[]): void {
|
||||
if (!definitions || !definitions.length) {
|
||||
return;
|
||||
}
|
||||
const me = this;
|
||||
this.store.get('filter_' + this.name).then(function(storedData: { name: string, data: OsFilter[] }): void {
|
||||
const storedFilters = (storedData && storedData.data) ? storedData.data : [];
|
||||
definitions.forEach(definedFilter => {
|
||||
const matchingStoreFilter = storedFilters.find(f => f.property === definedFilter.property);
|
||||
let count = 0;
|
||||
definedFilter.options.forEach(option => {
|
||||
if (typeof option === 'string'){
|
||||
return;
|
||||
};
|
||||
if (matchingStoreFilter && matchingStoreFilter.options){
|
||||
const storedOption = matchingStoreFilter.options.find(o =>
|
||||
typeof o !== 'string' && o.condition === option.condition) as OsFilterOption;
|
||||
if (storedOption) {
|
||||
option.isActive = storedOption.isActive;
|
||||
}
|
||||
}
|
||||
if (option.isActive) {
|
||||
count += 1;
|
||||
}
|
||||
});
|
||||
definedFilter.count = count;
|
||||
});
|
||||
me.filterDefinitions = definitions;
|
||||
me.filteredData = me.filterData(me.currentRawData);
|
||||
me.filterDataOutput.next(me.filteredData);
|
||||
}, function(error: any) : void {
|
||||
me.filteredData = me.filterData(me.currentRawData);
|
||||
me.filterDataOutput.next(me.filteredData);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current filter definitions via StorageService
|
||||
*/
|
||||
private setStorageDefinition(): void {
|
||||
this.store.set('filter_' + this.name, {
|
||||
name: 'filter_' + this.name, data: this.filterDefinitions});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Takes an array of data and applies current filters
|
||||
*/
|
||||
private filterData(data: V[]): V[] {
|
||||
const filteredData = [];
|
||||
if (!data) {
|
||||
return filteredData;
|
||||
}
|
||||
if (!this.filterDefinitions || !this.filterDefinitions.length){
|
||||
return data;
|
||||
}
|
||||
data.forEach(newItem => {
|
||||
let excluded = false;
|
||||
for (const filter of this.filterDefinitions) {
|
||||
if (filter.count && !this.checkIncluded(newItem, filter)) {
|
||||
excluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!excluded){
|
||||
filteredData.push(newItem);
|
||||
}
|
||||
});
|
||||
return filteredData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to see if a model instance passes a filter
|
||||
* @param item
|
||||
* @param filter
|
||||
*/
|
||||
private checkIncluded(item: V, filter: OsFilter): boolean {
|
||||
for (const option of filter.options) {
|
||||
if (typeof option === 'string' ){
|
||||
continue;
|
||||
}
|
||||
if (option.isActive) {
|
||||
if (option.condition === null ) {
|
||||
return this.checkIncludedNegative(item, filter);
|
||||
}
|
||||
if (item[filter.property] === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (item[filter.property] instanceof BaseModel ) {
|
||||
if (item[filter.property].id === option.condition){
|
||||
return true;
|
||||
}
|
||||
} else if (item[filter.property] === option.condition){
|
||||
return true;
|
||||
} else if (item[filter.property].toString() === option.condition){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if none of the defined non-null filters apply,
|
||||
* aka 'items that match no filter'
|
||||
* @param item: A viewModel
|
||||
* @param filter
|
||||
*/
|
||||
private checkIncludedNegative(item: V, filter: OsFilter): boolean {
|
||||
if (item[filter.property] === undefined) {
|
||||
return true;
|
||||
}
|
||||
for (const option of filter.options) {
|
||||
if (typeof option === 'string' || option.condition === null) {
|
||||
continue;
|
||||
}
|
||||
if (item[filter.property] === option.condition) {
|
||||
return false;
|
||||
} else if (item[filter.property].toString() === option.condition){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public getFilterName(filter: OsFilter): string {
|
||||
if (filter.label) {
|
||||
return filter.label;
|
||||
} else {
|
||||
const itemProperty = filter.property as string;
|
||||
return itemProperty.charAt(0).toUpperCase() + itemProperty.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
public get hasActiveFilters(): number {
|
||||
if (!this.filterDefinitions || !this.filterDefinitions.length){
|
||||
return 0;
|
||||
}
|
||||
let filters = 0;
|
||||
for (const filter of this.filterDefinitions) {
|
||||
if (filter.count){
|
||||
filters += 1;
|
||||
}
|
||||
};
|
||||
return filters;
|
||||
}
|
||||
|
||||
public hasFilterOptions(): boolean {
|
||||
return (this.filterDefinitions && this.filterDefinitions.length) ? true : false;
|
||||
}
|
||||
|
||||
}
|
18
client/src/app/core/services/sort-list.service.spec.ts
Normal file
18
client/src/app/core/services/sort-list.service.spec.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SortListService } from './sort-list.service';
|
||||
import { E2EImportsModule } from '../../../e2e-imports.module';
|
||||
|
||||
describe('SortListService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [SortListService]
|
||||
});
|
||||
});
|
||||
|
||||
// TODO testing (does not work without injecting a BaseViewComponent)
|
||||
// it('should be created', inject([SortListService], (service: SortListService) => {
|
||||
// expect(service).toBeTruthy();
|
||||
// }));
|
||||
});
|
247
client/src/app/core/services/sort-list.service.ts
Normal file
247
client/src/app/core/services/sort-list.service.ts
Normal file
@ -0,0 +1,247 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { BaseViewModel } from '../../site/base/base-view-model';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
/**
|
||||
* Describes the sorting columns of an associated ListView, and their state.
|
||||
*/
|
||||
export interface OsSortingDefinition<V> {
|
||||
sortProperty: keyof V;
|
||||
sortAscending?: boolean;
|
||||
options: OsSortingItem<V>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A sorting property (data may be a string, a number, a function, or an object
|
||||
* with a toString method) to sort after. Sorting will be done in {@link filterData}
|
||||
*/
|
||||
export interface OsSortingItem<V> {
|
||||
property: keyof V;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export abstract class SortListService<V extends BaseViewModel> {
|
||||
/**
|
||||
* Observable output that submits the newly sorted data each time a sorting has been done
|
||||
*/
|
||||
public sortedData = new BehaviorSubject<V[]>([]);
|
||||
|
||||
/**
|
||||
* The data to be sorted. See also the setter for {@link data}
|
||||
*/
|
||||
private unsortedData: V[];
|
||||
|
||||
/**
|
||||
* The current sorting definitions
|
||||
*/
|
||||
public sortOptions: OsSortingDefinition<V>;
|
||||
|
||||
/**
|
||||
* used for the key in the StorageService to save/load the correct sorting definitions.
|
||||
*/
|
||||
protected name: string;
|
||||
|
||||
/**
|
||||
* The sorting function according to current settings. Set via {@link updateSortFn}.
|
||||
*/
|
||||
private sortFn: (a: V, b: V) => number;
|
||||
|
||||
/**
|
||||
* Constructor. Does nothing. TranslateService is used for localeCompeare.
|
||||
*/
|
||||
public constructor(private translate: TranslateService, private store: StorageService) {}
|
||||
|
||||
/**
|
||||
* Put an array of data that you want sorted.
|
||||
*/
|
||||
public set data(data: V[]) {
|
||||
this.unsortedData = data;
|
||||
this.doAsyncSorting();
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the sorting properties, and returns an observable with sorted data
|
||||
* @param name arbitrary name, used to save/load correct saved settings from StorageService
|
||||
* @param definitions The definitions of the possible options
|
||||
*/
|
||||
public sort(): BehaviorSubject<V[]> {
|
||||
this.loadStorageDefinition();
|
||||
this.updateSortFn();
|
||||
return this.sortedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current sorting order
|
||||
*/
|
||||
public set ascending(ascending: boolean) {
|
||||
this.sortOptions.sortAscending = ascending;
|
||||
this.updateSortFn();
|
||||
this.saveStorageDefinition();
|
||||
this.doAsyncSorting();
|
||||
}
|
||||
|
||||
/**
|
||||
* get the current sorting order
|
||||
*/
|
||||
public get ascending(): boolean {
|
||||
return this.sortOptions.sortAscending;
|
||||
}
|
||||
|
||||
/**
|
||||
* set the property of the viewModel the sorting will be based on.
|
||||
* If the property stays the same, only the sort direction will be toggled,
|
||||
* new sortProperty will result in an ascending order.
|
||||
*/
|
||||
public set sortProperty(property: string) {
|
||||
if (this.sortOptions.sortProperty === (property as keyof V)) {
|
||||
this.ascending = !this.ascending;
|
||||
this.updateSortFn();
|
||||
} else {
|
||||
this.sortOptions.sortProperty = property as keyof V;
|
||||
this.sortOptions.sortAscending = true;
|
||||
this.updateSortFn();
|
||||
this.doAsyncSorting();
|
||||
}
|
||||
this.saveStorageDefinition();
|
||||
}
|
||||
|
||||
/**
|
||||
* get the property of the viewModel the sorting is based on.
|
||||
*/
|
||||
public get sortProperty(): string {
|
||||
return this.sortOptions.sortProperty as string;
|
||||
}
|
||||
|
||||
public get isActive(): boolean {
|
||||
return this.sortOptions && this.sortOptions.options.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the property and the sorting direction at the same time
|
||||
* @param property
|
||||
* @param ascending
|
||||
*/
|
||||
public setSorting(property: string, ascending: boolean): void {
|
||||
this.sortOptions.sortProperty = property as keyof V;
|
||||
this.sortOptions.sortAscending = ascending;
|
||||
this.saveStorageDefinition();
|
||||
this.updateSortFn();
|
||||
this.doAsyncSorting();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the currently active icon for an option.
|
||||
* @param option
|
||||
*/
|
||||
public getSortIcon(option: OsSortingItem<V>): string {
|
||||
if (this.sortProperty !== (option.property as string)) {
|
||||
return '';
|
||||
}
|
||||
return this.ascending ? 'arrow_downward' : 'arrow_upward';
|
||||
}
|
||||
|
||||
public getSortLabel(option: OsSortingItem<V>): string {
|
||||
if (option.label) {
|
||||
return option.label;
|
||||
}
|
||||
const itemProperty = option.property as string;
|
||||
return itemProperty.charAt(0).toUpperCase() + itemProperty.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the currently saved sorting definition from the borwser's
|
||||
* store
|
||||
*/
|
||||
private loadStorageDefinition(): void {
|
||||
const me = this;
|
||||
this.store.get('sorting_' + this.name).then(function(sorting: OsSortingDefinition<V> | null): void {
|
||||
if (sorting) {
|
||||
if (sorting.sortProperty) {
|
||||
me.sortOptions.sortProperty = sorting.sortProperty;
|
||||
if (sorting.sortAscending !== undefined) {
|
||||
me.sortOptions.sortAscending = sorting.sortAscending;
|
||||
}
|
||||
}
|
||||
}
|
||||
me.updateSortFn();
|
||||
me.doAsyncSorting();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* SSaves the current sorting definitions to the local store
|
||||
*/
|
||||
private saveStorageDefinition(): void {
|
||||
this.store.set('sorting_' + this.name, {
|
||||
sortProperty: this.sortProperty,
|
||||
ascending: this.ascending
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* starts sorting, and
|
||||
*/
|
||||
private doAsyncSorting(): Promise<void> {
|
||||
const me = this;
|
||||
return new Promise(function(): void {
|
||||
const data = me.unsortedData.sort(me.sortFn);
|
||||
me.sortedData.next(data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Recreates the sorting function. Is supposed to be called on init and
|
||||
* every time the sorting (property, ascending/descending) or the language changes
|
||||
*/
|
||||
private updateSortFn(): void {
|
||||
const property = this.sortProperty as string;
|
||||
const ascending = this.ascending;
|
||||
const lang = this.translate.currentLang; // TODO: observe and update sorting on change
|
||||
|
||||
this.sortFn = function(itemA: V, itemB: V): number {
|
||||
const firstProperty = ascending ? itemA[property] : itemB[property];
|
||||
const secondProperty = ascending ? itemB[property] : itemA[property];
|
||||
if (typeof firstProperty !== typeof secondProperty) {
|
||||
// undefined/null items should always land at the end
|
||||
if (!firstProperty) {
|
||||
return ascending ? 1 : -1;
|
||||
} else if (!secondProperty) {
|
||||
return ascending ? -1 : 1;
|
||||
} else {
|
||||
throw new TypeError('sorting of items failed because of mismatched types');
|
||||
}
|
||||
} else {
|
||||
switch (typeof firstProperty) {
|
||||
case 'boolean':
|
||||
if (firstProperty === false && secondProperty === true) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
case 'number':
|
||||
return firstProperty > secondProperty ? 1 : -1;
|
||||
case 'string':
|
||||
if (!firstProperty) {
|
||||
return 1;
|
||||
}
|
||||
return firstProperty.localeCompare(secondProperty, lang);
|
||||
case 'function':
|
||||
const a = firstProperty();
|
||||
const b = secondProperty();
|
||||
return a.localeCompare(b, lang);
|
||||
case 'object':
|
||||
return firstProperty.toString().localeCompare(secondProperty.toString(), lang);
|
||||
case 'undefined':
|
||||
return 1;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
<mat-accordion (keyup)=checkKeyEvent($event)>
|
||||
<mat-expansion-panel *ngFor="let filter of service.filterDefinitions">
|
||||
<mat-expansion-panel-header *ngIf="filter.options && filter.options.length">
|
||||
<mat-panel-title>
|
||||
<mat-icon>
|
||||
{{ filter.count ? 'checked' : ''}}
|
||||
</mat-icon>
|
||||
<span translate>{{ service.getFilterName(filter) }}</span>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<div class="indent" *ngIf="filter.options && filter.options.length">
|
||||
<mat-action-list class="filtermenu-expanded">
|
||||
<div *ngFor="let option of filter.options">
|
||||
<div *ngIf="isFilter(option)">
|
||||
<mat-checkbox [checked]="option.isActive" (change)="service.toggleFilterOption(filter.property, option)">
|
||||
{{ option.label |translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<div class="filter-subtitle" *ngIf="!isFilter(option)">
|
||||
<mat-divider *ngIf="option === '-'"></mat-divider>
|
||||
<span *ngIf="option !== '-'" translate> {{option}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</mat-action-list>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
@ -0,0 +1,13 @@
|
||||
div.indent {
|
||||
margin-left: 24px;
|
||||
}
|
||||
mat-divider {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
div.filter-subtitle {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
opacity: 0.9;
|
||||
font-style: italic;
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
// import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
// import { FilterMenuComponent } from './filter-menu.component';
|
||||
|
||||
describe('FilterMenuComponent', () => {
|
||||
// TODO test won't work without a BaseViewModel
|
||||
// let component: FilterMenuComponent<V>;
|
||||
// let fixture: ComponentFixture<FilterMenuComponent<V>>;
|
||||
|
||||
// beforeEach(async(() => {
|
||||
// TestBed.configureTestingModule({
|
||||
// declarations: [FilterMenuComponent]
|
||||
// }).compileComponents();
|
||||
// }));
|
||||
|
||||
// beforeEach(() => {
|
||||
// fixture = TestBed.createComponent(FilterMenuComponent);
|
||||
// component = fixture.componentInstance;
|
||||
// fixture.detectChanges();
|
||||
// });
|
||||
|
||||
// it('should create', () => {
|
||||
// expect(component).toBeTruthy();
|
||||
// });
|
||||
});
|
@ -0,0 +1,64 @@
|
||||
import { Output, Component, OnInit, EventEmitter, Input } from '@angular/core';
|
||||
import { FilterListService, OsFilterOption } from '../../../../core/services/filter-list.service';
|
||||
|
||||
/**
|
||||
* Component for selecting the filters in a filter menu.
|
||||
* It expects to be opened inside a sidenav container,
|
||||
*
|
||||
* ## Examples:
|
||||
*
|
||||
* ### Usage of the selector:
|
||||
* ```html
|
||||
* <os-filter-menu (dismissed)="this.filterMenu.close()">
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-filter-menu',
|
||||
templateUrl: './filter-menu.component.html',
|
||||
styleUrls: ['./filter-menu.component.scss']
|
||||
})
|
||||
export class FilterMenuComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* An event emitter to submit a desire to close this component
|
||||
* TODO: Might be an easier way to do this
|
||||
*/
|
||||
@Output()
|
||||
public dismissed = new EventEmitter<boolean>();
|
||||
|
||||
/**
|
||||
* A filterListService for the listView. There are several Services extending
|
||||
* the FilterListService; unsure about how to get them in any other way.
|
||||
*/
|
||||
@Input()
|
||||
public service: FilterListService<any, any>; // TODO (M, V)
|
||||
|
||||
/**
|
||||
* Constructor. Does nothing.
|
||||
* @param service
|
||||
*/
|
||||
public constructor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Directly closes again if no sorting is available
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
if (!this.service.filterDefinitions) {
|
||||
this.dismissed.next(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests for escape key (to colose the sidebar)
|
||||
* @param event
|
||||
*/
|
||||
public checkKeyEvent(event: KeyboardEvent) : void {
|
||||
if (event.key === 'Escape'){
|
||||
this.dismissed.next(true)
|
||||
}
|
||||
}
|
||||
public isFilter(option: OsFilterOption) : boolean{
|
||||
return (typeof option === 'string') ? false : true;
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
<mat-nav-list class='main-nav'>
|
||||
<mat-list-item *ngFor="let option of this.data.sortOptions.options" (click)="clickedOption(option.property)">
|
||||
<button mat-menu-item>
|
||||
<mat-icon>{{ data.getSortIcon(option) }}</mat-icon>
|
||||
<span translate>{{ data.getSortLabel(option) }}</span>
|
||||
</button>
|
||||
</mat-list-item>
|
||||
</mat-nav-list>
|
@ -0,0 +1,26 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
import { OsSortBottomSheetComponent } from './os-sort-bottom-sheet.component';
|
||||
|
||||
|
||||
describe('OsSortBottomSheetComponent', () => {
|
||||
// let component: OsSortBottomSheetComponent<any>;
|
||||
let fixture: ComponentFixture<OsSortBottomSheetComponent<any>>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(OsSortBottomSheetComponent);
|
||||
// component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
// it('should create', () => {
|
||||
// expect(component).toBeTruthy();
|
||||
// });
|
||||
});
|
@ -0,0 +1,48 @@
|
||||
import { Inject, Component, OnInit } from '@angular/core';
|
||||
import { MatBottomSheetRef, MAT_BOTTOM_SHEET_DATA } from '@angular/material';
|
||||
import { BaseViewModel } from '../../../../site/base/base-view-model';
|
||||
import { SortListService } from '../../../../core/services/sort-list.service';
|
||||
|
||||
/**
|
||||
* A bottom sheet used for setting a list's sorting, used by {@link SortFilterBarComponent}
|
||||
* usage:
|
||||
* ```
|
||||
* @ViewChild('sortBottomSheet')
|
||||
* public sortBottomSheet: OsSortBottomSheetComponent<V>;
|
||||
* ...
|
||||
* this.bottomSheet.open(OsSortBottomSheetComponent, { data: SortService });
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-sort-bottom-sheet',
|
||||
templateUrl: './os-sort-bottom-sheet.component.html',
|
||||
styleUrls: ['./os-sort-bottom-sheet.component.scss']
|
||||
})
|
||||
export class OsSortBottomSheetComponent<V extends BaseViewModel> implements OnInit {
|
||||
|
||||
/**
|
||||
* Constructor. Gets a reference to itself (for closing after interaction)
|
||||
* @param data
|
||||
* @param sheetRef
|
||||
*/
|
||||
public constructor(
|
||||
@Inject(MAT_BOTTOM_SHEET_DATA) public data: SortListService<V>, private sheetRef: MatBottomSheetRef ) {
|
||||
}
|
||||
|
||||
/**
|
||||
* init fucntion. Closes inmediately if no sorting is available.
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
if (!this.data || !this.data.sortOptions || !this.data.sortOptions.options.length){
|
||||
throw new Error('No sorting available for a sorting list');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logic for a toggle of options. Either reverses sorting, or
|
||||
* sorts after a new property.
|
||||
*/
|
||||
public clickedOption(item: string): void {
|
||||
this.sheetRef.dismiss(item);
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
<div class="custom-table-header on-transition-fade">
|
||||
<button mat-button *ngIf="hasFilters" (click)="filterMenu.opened ? filterMenu.close() : filterMenu.open()">
|
||||
<span *ngIf="filterService.hasActiveFilters > 0">{{ filterService.hasActiveFilters }} </span>
|
||||
<span class="upper" translate>Filter</span>
|
||||
</button>
|
||||
<button mat-button *ngIf="vp.isMobile && hasSorting" (click)="openSortDropDown()">
|
||||
<span class="upper" translate>Sort</span>
|
||||
</button>
|
||||
<button mat-button *ngIf="!vp.isMobile && hasSorting" [matMenuTriggerFor]="menu">
|
||||
<span class="upper" translate>Sort</span>
|
||||
</button>
|
||||
<input
|
||||
matInput
|
||||
*ngIf="isSearchBar"
|
||||
(keyup)="applySearch($event, $event.target.value)"
|
||||
osAutofocus
|
||||
placeholder="{{ translate.instant('Search') }}"
|
||||
[ngClass]="vp.isMobile ? 'vp' : ''"
|
||||
/>
|
||||
<button mat-button (click)="toggleSearchBar()">
|
||||
<mat-icon>{{ isSearchBar ? 'keyboard_arrow_right' : 'search' }}</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Header for the filter side bar -->
|
||||
<mat-drawer #filterMenu mode="push" position="end">
|
||||
<div class="custom-table-header filter-menu" (click)="this.filterMenu.toggle()">
|
||||
<span><mat-icon>keyboard_arrow_right</mat-icon></span>
|
||||
<span class="right-with-margin" translate>Filter options:</span>
|
||||
</div>
|
||||
<os-filter-menu *ngIf="filterService" (dismissed)="this.filterMenu.close()" [service]="filterService">
|
||||
</os-filter-menu>
|
||||
</mat-drawer>
|
||||
|
||||
<!-- non-mobile sorting menu -->
|
||||
<mat-menu #menu>
|
||||
<div *ngIf="hasSorting">
|
||||
<mat-list-item
|
||||
*ngFor="let option of sortService.sortOptions.options"
|
||||
(click)="sortService.sortProperty = option.property"
|
||||
>
|
||||
<button mat-menu-item>
|
||||
<mat-icon>{{ sortService.getSortIcon(option) }}</mat-icon>
|
||||
<span translate> {{ sortService.getSortLabel(option) }}</span>
|
||||
</button>
|
||||
</mat-list-item>
|
||||
</div>
|
||||
</mat-menu>
|
@ -0,0 +1,10 @@
|
||||
.filter-menu {
|
||||
margin-left: 5px;
|
||||
justify-content: space-between;
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
span.right-with-margin {
|
||||
margin-right: 25px;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
import { OsSortFilterBarComponent } from './os-sort-filter-bar.component';
|
||||
|
||||
describe('OsSortFilterBarComponent', () => {
|
||||
let component: OsSortFilterBarComponent<any>;
|
||||
let fixture: ComponentFixture<any>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(OsSortFilterBarComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,156 @@
|
||||
import { Input, Output, Component, ViewChild, EventEmitter } from '@angular/core';
|
||||
import { MatBottomSheet } from '@angular/material';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { BaseViewModel } from '../../../site/base/base-view-model';
|
||||
import { OsSortBottomSheetComponent } from './os-sort-bottom-sheet/os-sort-bottom-sheet.component';
|
||||
import { FilterMenuComponent} from './filter-menu/filter-menu.component';
|
||||
import { OsSortingItem } from '../../../core/services/sort-list.service'
|
||||
import { SortListService } from '../../../core/services/sort-list.service';
|
||||
import { ViewportService } from '../../../core/services/viewport.service';
|
||||
|
||||
/**
|
||||
* Reusable bar for list views, offering sorting and filter options.
|
||||
* It will modify the DataSource of the listView to include custom sorting and
|
||||
* filters.
|
||||
*
|
||||
* ## Examples:
|
||||
* ### Usage of the selector:
|
||||
*
|
||||
* ```html
|
||||
* <os-sort-filter-bar [sortService]="sortService" [filterService]="filterService"
|
||||
* (searchFieldChange)="searchFilter($event)">
|
||||
* </os-sort-filter-bar>
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-sort-filter-bar',
|
||||
templateUrl: './os-sort-filter-bar.component.html',
|
||||
styleUrls: ['./os-sort-filter-bar.component.scss']
|
||||
})
|
||||
export class OsSortFilterBarComponent<V extends BaseViewModel> {
|
||||
|
||||
/**
|
||||
* The currently active sorting service for the list view
|
||||
*/
|
||||
@Input()
|
||||
public sortService: SortListService<V>;
|
||||
|
||||
/**
|
||||
* The currently active filter service for the list view. It is supposed to
|
||||
* be a FilterListService extendingFilterListService.
|
||||
*/
|
||||
@Input()
|
||||
public filterService: any; // TODO a FilterListService extendingFilterListService
|
||||
|
||||
@Output()
|
||||
public searchFieldChange = new EventEmitter<string>();
|
||||
/**
|
||||
* The filter side drawer
|
||||
*/
|
||||
@ViewChild('filterMenu')
|
||||
public filterMenu: FilterMenuComponent;
|
||||
|
||||
/**
|
||||
* The bottom sheet used to alter sorting in mobile view
|
||||
*/
|
||||
@ViewChild('sortBottomSheet')
|
||||
public sortBottomSheet: OsSortBottomSheetComponent<V>;
|
||||
|
||||
/**
|
||||
* The 'opened/active' state of the fulltext filter input field
|
||||
*/
|
||||
public isSearchBar = false;
|
||||
|
||||
/**
|
||||
* Constructor. Also creates a filtermenu component and a bottomSheet
|
||||
* @param translate
|
||||
* @param vp
|
||||
* @param bottomSheet
|
||||
*/
|
||||
public constructor(public translate: TranslateService, public vp: ViewportService, private bottomSheet: MatBottomSheet) {
|
||||
this.filterMenu = new FilterMenuComponent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the sorting menu/bottom sheet (depending on state of mobile/desktop)
|
||||
*/
|
||||
public openSortDropDown(): void {
|
||||
if (this.vp.isMobile) {
|
||||
const bottomSheetRef = this.bottomSheet.open(OsSortBottomSheetComponent,
|
||||
{ data: this.sortService }
|
||||
);
|
||||
bottomSheetRef.afterDismissed().subscribe(result => {
|
||||
if (result) {
|
||||
this.sortService.sortProperty = result;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to keypresses on the quick-search input
|
||||
*/
|
||||
public applySearch(event: KeyboardEvent, value?: string): void {
|
||||
if (event.key === 'Escape' ) {
|
||||
this.searchFieldChange.emit('');
|
||||
this.isSearchBar = false;
|
||||
} else {
|
||||
this.searchFieldChange.emit(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there is an active SortService present
|
||||
*/
|
||||
public get hasSorting(): boolean {
|
||||
return (this.sortService && this.sortService.isActive);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there is an active FilterService present
|
||||
*/
|
||||
public get hasFilters(): boolean {
|
||||
if (this.filterService && this.filterService.hasFilterOptions()){
|
||||
return true;
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the currently active icon for an option.
|
||||
* @param option
|
||||
*/
|
||||
public getSortIcon(option: OsSortingItem<V>): string {
|
||||
if (this.sortService.sortProperty !== option.property){
|
||||
return '';
|
||||
}
|
||||
return this.sortService.ascending ? 'arrow_downward' : 'arrow_upward';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the label for anoption. If no label is defined, a capitalized version of
|
||||
* the property is used.
|
||||
* @param option
|
||||
*/
|
||||
public getSortLabel(option: OsSortingItem<V>) : string {
|
||||
if (option.label) {
|
||||
return option.label;
|
||||
}
|
||||
const itemProperty = option.property as string;
|
||||
return itemProperty.charAt(0).toUpperCase() + itemProperty.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open/closes the 'quick search input'. When closing, also removes the filter
|
||||
* that input applied
|
||||
*/
|
||||
public toggleSearchBar(): void {
|
||||
if (!this.isSearchBar){
|
||||
this.isSearchBar = true;
|
||||
} else {
|
||||
this.searchFieldChange.emit('');
|
||||
this.isSearchBar = false;
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,13 @@ import { Poll } from './poll';
|
||||
import { AgendaBaseModel } from '../base/agenda-base-model';
|
||||
import { SearchRepresentation } from '../../../core/services/search.service';
|
||||
|
||||
|
||||
export const assignmentPhase = [
|
||||
{key: 0, name: 'Searching for candidates'},
|
||||
{key: 1, name: 'Voting'},
|
||||
{key: 2, name: 'Finished'}
|
||||
];
|
||||
|
||||
/**
|
||||
* Representation of an assignment.
|
||||
* @ignore
|
||||
|
@ -57,4 +57,17 @@ export class WorkflowState extends Deserializer {
|
||||
public toString = (): string => {
|
||||
return this.name;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a workflowstate has no 'next state' left, and is final
|
||||
*/
|
||||
public get isFinalState(): boolean {
|
||||
if (!this.next_states_id || !this.next_states_id.length ){
|
||||
return true;
|
||||
}
|
||||
if (this.next_states_id.length === 1 && this.next_states_id[0] === 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,8 @@ import {
|
||||
MatIconModule,
|
||||
MatButtonToggleModule,
|
||||
MatBadgeModule,
|
||||
MatStepperModule
|
||||
MatStepperModule,
|
||||
MatBottomSheetModule
|
||||
} from '@angular/material';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatChipsModule } from '@angular/material';
|
||||
@ -67,6 +68,9 @@ import { SortingListComponent } from './components/sorting-list/sorting-list.com
|
||||
import { SpeakerListComponent } from 'app/site/agenda/components/speaker-list/speaker-list.component';
|
||||
import { SortingTreeComponent } from './components/sorting-tree/sorting-tree.component';
|
||||
import { ChoiceDialogComponent } from './components/choice-dialog/choice-dialog.component';
|
||||
import { OsSortFilterBarComponent } from './components/os-sort-filter-bar/os-sort-filter-bar.component';
|
||||
import { OsSortBottomSheetComponent } from './components/os-sort-filter-bar/os-sort-bottom-sheet/os-sort-bottom-sheet.component';
|
||||
import { FilterMenuComponent } from './components/os-sort-filter-bar/filter-menu/filter-menu.component';
|
||||
|
||||
/**
|
||||
* Share Module for all "dumb" components and pipes.
|
||||
@ -104,6 +108,7 @@ import { ChoiceDialogComponent } from './components/choice-dialog/choice-dialog.
|
||||
MatDialogModule,
|
||||
MatSnackBarModule,
|
||||
MatChipsModule,
|
||||
MatBottomSheetModule,
|
||||
MatTooltipModule,
|
||||
MatBadgeModule,
|
||||
// TODO: there is an error with missing icons
|
||||
@ -167,7 +172,8 @@ import { ChoiceDialogComponent } from './components/choice-dialog/choice-dialog.
|
||||
SortingListComponent,
|
||||
EditorModule,
|
||||
SortingTreeComponent,
|
||||
TreeModule
|
||||
TreeModule,
|
||||
OsSortFilterBarComponent
|
||||
],
|
||||
declarations: [
|
||||
PermsDirective,
|
||||
@ -182,13 +188,19 @@ import { ChoiceDialogComponent } from './components/choice-dialog/choice-dialog.
|
||||
SortingListComponent,
|
||||
SpeakerListComponent,
|
||||
SortingTreeComponent,
|
||||
ChoiceDialogComponent
|
||||
ChoiceDialogComponent,
|
||||
OsSortFilterBarComponent,
|
||||
OsSortBottomSheetComponent,
|
||||
FilterMenuComponent
|
||||
],
|
||||
providers: [
|
||||
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter },
|
||||
SearchValueSelectorComponent,
|
||||
SortingListComponent,
|
||||
SortingTreeComponent
|
||||
]
|
||||
SortingTreeComponent,
|
||||
OsSortFilterBarComponent,
|
||||
OsSortBottomSheetComponent
|
||||
],
|
||||
entryComponents: [OsSortBottomSheetComponent]
|
||||
})
|
||||
export class SharedModule {}
|
||||
|
@ -13,6 +13,7 @@
|
||||
</div>
|
||||
|
||||
</os-head-bar>
|
||||
<os-sort-filter-bar [filterService] = "filterService" (searchFieldChange)="searchFilter($event)"></os-sort-filter-bar>
|
||||
|
||||
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
|
||||
<!-- selector column -->
|
||||
|
@ -4,10 +4,12 @@ import { Title } from '@angular/platform-browser';
|
||||
import { MatSnackBar, MatDialog } from '@angular/material';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { ViewItem } from '../../models/view-item';
|
||||
import { ListViewBaseComponent } from 'app/site/base/list-view-base';
|
||||
import { AgendaFilterListService } from '../../services/agenda-filter-list.service';
|
||||
import { AgendaRepositoryService } from '../../services/agenda-repository.service';
|
||||
import { ListViewBaseComponent } from 'app/site/base/list-view-base';
|
||||
import { PromptService } from '../../../../core/services/prompt.service';
|
||||
import { ViewItem } from '../../models/view-item';
|
||||
|
||||
|
||||
import { AgendaCsvExportService } from '../../services/agenda-csv-export.service';
|
||||
import { ItemInfoDialogComponent } from '../item-info-dialog/item-info-dialog.component';
|
||||
@ -50,6 +52,9 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
|
||||
* @param vp determine the viewport
|
||||
* @param durationService Converts numbers to readable duration strings
|
||||
* @param csvExport Handles the exporting into csv
|
||||
* @param repo the agenda repository
|
||||
* @param promptService
|
||||
* @param filterService: service for filtering data
|
||||
*/
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
@ -63,7 +68,8 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
|
||||
private config: ConfigService,
|
||||
public vp: ViewportService,
|
||||
public durationService: DurationService,
|
||||
private csvExport: AgendaCsvExportService
|
||||
private csvExport: AgendaCsvExportService,
|
||||
public filterService: AgendaFilterListService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar);
|
||||
|
||||
@ -73,16 +79,15 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
|
||||
|
||||
/**
|
||||
* Init function.
|
||||
* Sets the title, initializes the table and calls the repository.
|
||||
* Sets the title, initializes the table and filter options, subscribes to filter service.
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
super.setTitle('Agenda');
|
||||
this.initTable();
|
||||
this.repo.getViewModelListObservable().subscribe(newAgendaItem => {
|
||||
this.filterService.filter().subscribe(newAgendaItem => {
|
||||
this.dataSource.data = newAgendaItem;
|
||||
this.checkSelection();
|
||||
});
|
||||
|
||||
this.config
|
||||
.get('agenda_enable_numbering')
|
||||
.subscribe(autoNumbering => (this.isNumberingAllowed = autoNumbering));
|
||||
|
@ -0,0 +1,51 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { FilterListService, OsFilter, OsFilterOption } from "../../../core/services/filter-list.service";
|
||||
import { Item, itemVisibilityChoices } from "../../../shared/models/agenda/item";
|
||||
import { ViewItem } from "../models/view-item";
|
||||
import { StorageService } from "app/core/services/storage.service";
|
||||
import { AgendaRepositoryService } from "./agenda-repository.service";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AgendaFilterListService extends FilterListService<Item, ViewItem> {
|
||||
|
||||
protected name = 'Agenda';
|
||||
|
||||
public filterOptions: OsFilter[] = [];
|
||||
|
||||
/**
|
||||
* Constructor. Also creates the dynamic filter options
|
||||
* @param store
|
||||
* @param repo
|
||||
*/
|
||||
public constructor(store: StorageService, repo: AgendaRepositoryService) {
|
||||
super(store, repo);
|
||||
this.filterOptions = [{
|
||||
label: 'Visibility',
|
||||
property: 'type',
|
||||
options: this.createVisibilityFilterOptions()
|
||||
}, {
|
||||
label: 'Hidden Status',
|
||||
property: 'done',
|
||||
options: [
|
||||
{label: 'Open', condition: false},
|
||||
{label: 'Closed', condition: true}
|
||||
]
|
||||
}];
|
||||
}
|
||||
|
||||
private createVisibilityFilterOptions(): OsFilterOption[] {
|
||||
const options = [];
|
||||
itemVisibilityChoices.forEach(choice => {
|
||||
options.push({
|
||||
condition: choice.key as number,
|
||||
label: choice.name
|
||||
});
|
||||
});
|
||||
return options;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -15,6 +15,10 @@
|
||||
</div>
|
||||
</os-head-bar>
|
||||
|
||||
<mat-drawer-container class="on-transition-fade">
|
||||
<os-sort-filter-bar [filterService]="filterService" [sortService]="sortService"
|
||||
(searchFieldChange)="searchFilter($event)">
|
||||
</os-sort-filter-bar>
|
||||
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
|
||||
<!-- slector column -->
|
||||
<ng-container matColumnDef="selector">
|
||||
@ -90,3 +94,4 @@
|
||||
</button>
|
||||
</div>
|
||||
</mat-menu>
|
||||
</mat-drawer-container>
|
||||
|
@ -1,11 +1,16 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { ViewAssignment } from '../models/view-assignment';
|
||||
import { ListViewBaseComponent } from '../../base/list-view-base';
|
||||
|
||||
import { AssignmentFilterListService } from '../services/assignment-filter.service';
|
||||
import { AssignmentRepositoryService } from '../services/assignment-repository.service';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { ListViewBaseComponent } from '../../base/list-view-base';
|
||||
import { PromptService } from '../../../core/services/prompt.service';
|
||||
import { ViewAssignment } from '../models/view-assignment';
|
||||
import { AssignmentSortListService } from '../services/assignment-sort-list.service';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Listview for the assignments
|
||||
@ -17,21 +22,25 @@ import { PromptService } from '../../../core/services/prompt.service';
|
||||
styleUrls: ['./assignment-list.component.scss']
|
||||
})
|
||||
export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignment> implements OnInit {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param titleService
|
||||
* @param translate
|
||||
* @param matSnackBar
|
||||
* @param repo the repository
|
||||
* @param promptService
|
||||
* @param filterService: A service to supply the filtered datasource
|
||||
* @param sortService: Service to sort the filtered dataSource
|
||||
*/
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
translate: TranslateService,
|
||||
matSnackBar: MatSnackBar,
|
||||
private repo: AssignmentRepositoryService,
|
||||
private promptService: PromptService
|
||||
public repo: AssignmentRepositoryService,
|
||||
private promptService: PromptService,
|
||||
public filterService: AssignmentFilterListService,
|
||||
public sortService: AssignmentSortListService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar);
|
||||
// activate multiSelect mode for this listview
|
||||
@ -40,13 +49,18 @@ export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignmen
|
||||
|
||||
/**
|
||||
* Init function.
|
||||
* Sets the title, inits the table and calls the repo.
|
||||
* Sets the title, inits the table, sets sorting and filter definitions, subscribes to filtered
|
||||
* data and sorting service
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
super.setTitle('Assignments');
|
||||
this.initTable();
|
||||
this.repo.getViewModelListObservable().subscribe(newAssignments => {
|
||||
this.dataSource.data = newAssignments;
|
||||
|
||||
this.filterService.filter().subscribe(filteredData => {
|
||||
this.sortService.data = filteredData;
|
||||
});
|
||||
this.sortService.sort().subscribe(sortedData => {
|
||||
this.dataSource.data = sortedData;
|
||||
this.checkSelection();
|
||||
});
|
||||
}
|
||||
|
@ -0,0 +1,43 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { AssignmentRepositoryService } from "./assignment-repository.service";
|
||||
import { Assignment, assignmentPhase } from "../../../shared/models/assignments/assignment";
|
||||
import { FilterListService, OsFilter, OsFilterOption } from "../../../core/services/filter-list.service";
|
||||
import { StorageService } from "app/core/services/storage.service";
|
||||
import { ViewAssignment } from "../models/view-assignment";
|
||||
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AssignmentFilterListService extends FilterListService<Assignment, ViewAssignment> {
|
||||
|
||||
protected name = 'Assignment';
|
||||
|
||||
public filterOptions: OsFilter[];
|
||||
|
||||
public constructor(store: StorageService, assignmentRepo: AssignmentRepositoryService) {
|
||||
super(store, assignmentRepo);
|
||||
this.filterOptions = [{
|
||||
property: 'phase',
|
||||
options: this.createPhaseOptions()
|
||||
}];
|
||||
}
|
||||
|
||||
private createPhaseOptions(): OsFilterOption[] {
|
||||
const options = [];
|
||||
assignmentPhase.forEach(phase => {
|
||||
options.push({
|
||||
label: phase.name,
|
||||
condition: phase.key,
|
||||
isActive: false
|
||||
});
|
||||
});
|
||||
options.push('-');
|
||||
options.push({
|
||||
label: 'Other',
|
||||
condition: null
|
||||
});
|
||||
return options;
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SortListService, OsSortingDefinition } from '../../../core/services/sort-list.service';
|
||||
import { ViewAssignment } from '../models/view-assignment';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AssignmentSortListService extends SortListService<ViewAssignment> {
|
||||
|
||||
public sortOptions: OsSortingDefinition<ViewAssignment> = {
|
||||
sortProperty: 'assignment',
|
||||
sortAscending: true,
|
||||
options: [
|
||||
{ property: 'agendaItem', label: 'agenda Item' },
|
||||
{ property: 'assignment' },
|
||||
{ property: 'phase' },
|
||||
{ property: 'candidateAmount', label: 'Number of candidates' }
|
||||
]
|
||||
};
|
||||
protected name = 'Assignment';
|
||||
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
import { ViewChild } from '@angular/core';
|
||||
import { MatTableDataSource, MatTable, MatSort, MatPaginator, MatSnackBar } from '@angular/material';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { MatTableDataSource, MatTable, MatSort, MatPaginator, MatSnackBar } from '@angular/material';
|
||||
import { BaseViewModel } from './base-view-model';
|
||||
import { ViewChild } from '@angular/core';
|
||||
|
||||
import { BaseViewComponent } from './base-view';
|
||||
import { BaseViewModel } from './base-view-model';
|
||||
|
||||
export abstract class ListViewBaseComponent<V extends BaseViewModel> extends BaseViewComponent {
|
||||
/**
|
||||
@ -64,7 +65,28 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel> extends Bas
|
||||
public initTable(): void {
|
||||
this.dataSource = new MatTableDataSource();
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
|
||||
public onSortButton(itemProperty: string): void {
|
||||
let newOrder: 'asc' | 'desc' = 'asc';
|
||||
if (itemProperty === this.sort.active) {
|
||||
newOrder = this.sort.direction === 'asc' ? 'desc' : 'asc';
|
||||
}
|
||||
const newSort = {
|
||||
disableClear: true,
|
||||
id: itemProperty,
|
||||
start: newOrder
|
||||
};
|
||||
this.sort.sort(newSort);
|
||||
}
|
||||
|
||||
public onFilterData(filteredDataSource: MatTableDataSource<V>) : void {
|
||||
this.dataSource = filteredDataSource;
|
||||
this.dataSource.paginator = this.paginator;
|
||||
}
|
||||
|
||||
public searchFilter(event: string): void {
|
||||
this.dataSource.filter = event;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -48,6 +48,10 @@
|
||||
|
||||
</os-head-bar>
|
||||
|
||||
<mat-drawer-container class="on-transition-fade">
|
||||
<os-sort-filter-bar [sortService]="sortService" [filterService]="filterService"
|
||||
(searchFieldChange)="searchFilter($event)">
|
||||
</os-sort-filter-bar>
|
||||
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
|
||||
<!-- Selector Column -->
|
||||
<ng-container matColumnDef="selector">
|
||||
@ -172,3 +176,4 @@
|
||||
</button>
|
||||
</div>
|
||||
</mat-menu>
|
||||
</mat-drawer-container>
|
||||
|
@ -12,8 +12,13 @@ import { MediafileRepositoryService } from '../../services/mediafile-repository.
|
||||
import { MediaManageService } from '../../services/media-manage.service';
|
||||
import { PromptService } from 'app/core/services/prompt.service';
|
||||
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
|
||||
import { MediafileFilterListService } from '../../services/mediafile-filter.service';
|
||||
import { MediafilesSortListService } from '../../services/mediafiles-sort-list.service';
|
||||
import { ViewportService } from 'app/core/services/viewport.service';
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Lists all the uploaded files.
|
||||
*/
|
||||
@ -81,11 +86,13 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile>
|
||||
private repo: MediafileRepositoryService,
|
||||
private mediaManage: MediaManageService,
|
||||
private promptService: PromptService,
|
||||
public vp: ViewportService
|
||||
public vp: ViewportService,
|
||||
public filterService: MediafileFilterListService,
|
||||
public sortService: MediafilesSortListService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar);
|
||||
|
||||
// emables multiSelection for this listView
|
||||
// embles multiSelection for this listView
|
||||
this.canMultiSelect = true;
|
||||
}
|
||||
|
||||
@ -102,8 +109,12 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile>
|
||||
hidden: new FormControl(),
|
||||
});
|
||||
|
||||
this.repo.getViewModelListObservable().subscribe(newFiles => {
|
||||
this.dataSource.data = newFiles;
|
||||
this.filterService.filter().subscribe(filteredData => {
|
||||
this.sortService.data = filteredData;
|
||||
});
|
||||
|
||||
this.sortService.sort().subscribe(sortedData => {
|
||||
this.dataSource.data = sortedData;
|
||||
});
|
||||
|
||||
// Observe the logo actions
|
||||
|
@ -118,4 +118,9 @@ export class ViewMediafile extends BaseViewModel {
|
||||
this._mediafile = update;
|
||||
}
|
||||
}
|
||||
|
||||
public is_hidden(): boolean {
|
||||
return this._mediafile.hidden;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { FilterListService } from "../../../core/services/filter-list.service";
|
||||
import { Mediafile } from "../../../shared/models/mediafiles/mediafile";
|
||||
import { ViewMediafile } from "../models/view-mediafile";
|
||||
import { StorageService } from "app/core/services/storage.service";
|
||||
import { MediafileRepositoryService } from "./mediafile-repository.service";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MediafileFilterListService extends FilterListService<Mediafile, ViewMediafile> {
|
||||
|
||||
protected name = 'Mediafile';
|
||||
|
||||
public filterOptions = [{
|
||||
property: 'is_hidden', label: 'Hidden',
|
||||
options: [
|
||||
{ condition: true, label: 'is hidden' },
|
||||
{ condition: false, label: 'is not hidden', isActive: true }
|
||||
]
|
||||
}
|
||||
// , { TODO: is_pdf is not yet implemented on mediafile side
|
||||
// property: 'is_pdf', isActive: false, label: 'PDF',
|
||||
// options: [
|
||||
// {condition: true, label: 'is a PDF'},
|
||||
// {condition: false, label: 'is not a PDF'}
|
||||
// ]
|
||||
// }
|
||||
];
|
||||
|
||||
public constructor(store: StorageService, repo: MediafileRepositoryService){
|
||||
super(store, repo);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SortListService, OsSortingDefinition } from '../../../core/services/sort-list.service';
|
||||
import { ViewMediafile } from '../models/view-mediafile';
|
||||
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MediafilesSortListService extends SortListService<ViewMediafile> {
|
||||
|
||||
public sortOptions: OsSortingDefinition<ViewMediafile> = {
|
||||
sortProperty: 'title',
|
||||
sortAscending: true,
|
||||
options: [
|
||||
{ property: 'title' },
|
||||
{ property: 'type' },
|
||||
{ property: 'size' },
|
||||
// { property: 'upload_date' }
|
||||
{ property: 'uploader' }
|
||||
]
|
||||
};
|
||||
protected name = 'Mediafile';
|
||||
}
|
@ -1,13 +1,3 @@
|
||||
.custom-table-header {
|
||||
// display: none;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
text-align: right;
|
||||
background: white;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
|
@ -16,9 +16,10 @@
|
||||
</div>
|
||||
</os-head-bar>
|
||||
|
||||
<div class="custom-table-header on-transition-fade">
|
||||
<button mat-button><span translate>SORT</span></button> <button mat-button><span translate>FILTER</span></button>
|
||||
</div>
|
||||
<mat-drawer-container class="on-transition-fade">
|
||||
<os-sort-filter-bar [filterService]="filterService" [sortService]="sortService"
|
||||
(searchFieldChange)="searchFilter($event)">
|
||||
</os-sort-filter-bar>
|
||||
|
||||
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
|
||||
<!-- Selector column -->
|
||||
@ -219,3 +220,4 @@
|
||||
</button>
|
||||
</div>
|
||||
</mat-menu>
|
||||
</mat-drawer-container>
|
||||
|
@ -7,19 +7,21 @@ import { ConfigService } from '../../../../core/services/config.service';
|
||||
import { MotionCsvExportService } from '../../services/motion-csv-export.service';
|
||||
import { ListViewBaseComponent } from '../../../base/list-view-base';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { MotionRepositoryService } from '../../services/motion-repository.service';
|
||||
import { ViewMotion } from '../../models/view-motion';
|
||||
import { WorkflowState } from '../../../../shared/models/motions/workflow-state';
|
||||
import { MotionMultiselectService } from '../../services/motion-multiselect.service';
|
||||
import { TagRepositoryService } from 'app/site/tags/services/tag-repository.service';
|
||||
import { MotionBlockRepositoryService } from '../../services/motion-block-repository.service';
|
||||
import { CategoryRepositoryService } from '../../services/category-repository.service';
|
||||
import { MotionBlockRepositoryService } from '../../services/motion-block-repository.service';
|
||||
import { MotionFilterListService } from '../../services/motion-filter-list.service';
|
||||
import { MotionSortListService } from '../../services/motion-sort-list.service';
|
||||
import { ViewTag } from 'app/site/tags/models/view-tag';
|
||||
import { ViewWorkflow } from '../../models/view-workflow';
|
||||
import { ViewCategory } from '../../models/view-category';
|
||||
import { ViewMotionBlock } from '../../models/view-motion-block';
|
||||
import { WorkflowRepositoryService } from '../../services/workflow-repository.service';
|
||||
|
||||
|
||||
/**
|
||||
* Component that displays all the motions in a Table using DataSource.
|
||||
*/
|
||||
@ -29,6 +31,7 @@ import { WorkflowRepositoryService } from '../../services/workflow-repository.se
|
||||
styleUrls: ['./motion-list.component.scss']
|
||||
})
|
||||
export class MotionListComponent extends ListViewBaseComponent<ViewMotion> implements OnInit {
|
||||
|
||||
/**
|
||||
* Use for minimal width. Please note the 'selector' row for multiSelect mode,
|
||||
* to be able to display an indicator for the state of selection
|
||||
@ -68,8 +71,13 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
|
||||
* @param tagRepo Tag Repository
|
||||
* @param motionBlockRepo
|
||||
* @param categoryRepo
|
||||
* @param categoryRepo: Repo for categories. Used to define filters
|
||||
* @param workflowRepo: Repo for Workflows. Used to define filters
|
||||
* @param motionCsvExport
|
||||
* @param multiselectService Service for the multiSelect actions
|
||||
* @param userRepo
|
||||
* @param sortService
|
||||
* @param filterService
|
||||
*/
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
@ -78,13 +86,14 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private configService: ConfigService,
|
||||
private repo: MotionRepositoryService,
|
||||
private tagRepo: TagRepositoryService,
|
||||
private motionBlockRepo: MotionBlockRepositoryService,
|
||||
private categoryRepo: CategoryRepositoryService,
|
||||
private workflowRepo: WorkflowRepositoryService,
|
||||
private motionCsvExport: MotionCsvExportService,
|
||||
public multiselectService: MotionMultiselectService
|
||||
public multiselectService: MotionMultiselectService,
|
||||
public sortService: MotionSortListService,
|
||||
public filterService: MotionFilterListService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar);
|
||||
|
||||
@ -95,28 +104,23 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
|
||||
/**
|
||||
* Init function.
|
||||
*
|
||||
* Sets the title, inits the table and calls the repository
|
||||
* Sets the title, inits the table, defines the filter/sorting options and
|
||||
* subscribes to filter and sorting services
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
super.setTitle('Motions');
|
||||
this.initTable();
|
||||
this.repo.getViewModelListObservable().subscribe(newMotions => {
|
||||
// TODO: This is for testing purposes. Can be removed with #3963
|
||||
this.dataSource.data = newMotions.sort((a, b) => {
|
||||
if (a.callListWeight !== b.callListWeight) {
|
||||
return a.callListWeight - b.callListWeight;
|
||||
} else {
|
||||
return a.id - b.id;
|
||||
}
|
||||
});
|
||||
this.checkSelection();
|
||||
});
|
||||
this.configService.get('motions_statutes_enabled').subscribe(enabled => (this.statutesEnabled = enabled));
|
||||
this.configService.get('motions_recommendations_by').subscribe(id => (this.recomendationEnabled = !!id));
|
||||
this.motionBlockRepo.getViewModelListObservable().subscribe(mBs => this.motionBlocks = mBs);
|
||||
this.categoryRepo.getViewModelListObservable().subscribe(cats => this.categories = cats);
|
||||
this.tagRepo.getViewModelListObservable().subscribe(tags => this.tags = tags);
|
||||
this.workflowRepo.getViewModelListObservable().subscribe(wfs => this.workflows = wfs);
|
||||
this.filterService.filter().subscribe(filteredData => this.sortService.data = filteredData);
|
||||
this.sortService.sort().subscribe(sortedData => {
|
||||
this.dataSource.data = sortedData;
|
||||
this.checkSelection();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -207,4 +211,6 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
|
||||
this.raiseError(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -50,6 +50,10 @@ export class ViewCategory extends BaseViewModel {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public get prefixedName(): string {
|
||||
return this.category.getTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the local objects if required
|
||||
* @param update
|
||||
|
@ -10,6 +10,7 @@ import { BaseViewModel } from '../../base/base-view-model';
|
||||
* @ignore
|
||||
*/
|
||||
export class ViewWorkflow extends BaseViewModel {
|
||||
|
||||
private _workflow: Workflow;
|
||||
|
||||
public constructor(workflow?: Workflow) {
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { MotionBlock } from 'app/shared/models/motions/motion-block';
|
||||
import { ViewMotionBlock } from '../models/view-motion-block';
|
||||
@ -9,9 +11,7 @@ import { DataSendService } from 'app/core/services/data-send.service';
|
||||
import { Identifiable } from 'app/shared/models/base/identifiable';
|
||||
import { Motion } from 'app/shared/models/motions/motion';
|
||||
import { ViewMotion } from '../models/view-motion';
|
||||
import { Observable } from 'rxjs';
|
||||
import { MotionRepositoryService } from './motion-repository.service';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
/**
|
||||
* Repository service for motion blocks
|
||||
|
@ -0,0 +1,149 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { FilterListService, OsFilter } from "../../../core/services/filter-list.service";
|
||||
import { Motion } from "../../../shared/models/motions/motion";
|
||||
import { ViewMotion } from "../models/view-motion";
|
||||
import { CategoryRepositoryService } from "./category-repository.service";
|
||||
import { WorkflowRepositoryService } from "./workflow-repository.service";
|
||||
import { StorageService } from "../../../core/services/storage.service";
|
||||
import { MotionRepositoryService } from "./motion-repository.service";
|
||||
import { MotionBlockRepositoryService } from "./motion-block-repository.service";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MotionFilterListService extends FilterListService<Motion, ViewMotion> {
|
||||
|
||||
protected name = 'Motion';
|
||||
/**
|
||||
* getter for the filterOptions. Note that in this case, the options are
|
||||
* generated dynamically, as the options change with the datastore
|
||||
*/
|
||||
public get filterOptions(): OsFilter[] {
|
||||
return [
|
||||
this.flowFilterOptions,
|
||||
this.categoryFilterOptions,
|
||||
this.motionBlockFilterOptions
|
||||
].concat(
|
||||
this.staticFilterOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter definitions for the workflow filter. Options will be generated by
|
||||
* getFilterOptions (as the workflows available may change)
|
||||
*/
|
||||
public flowFilterOptions = {
|
||||
property: 'state',
|
||||
label: 'State',
|
||||
isActive: false,
|
||||
options: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter definitions for the category filter. Options will be generated by
|
||||
* getFilterOptions (as the categories available may change)
|
||||
*/
|
||||
public categoryFilterOptions = {
|
||||
property: 'category',
|
||||
isActive: false,
|
||||
options: []
|
||||
};
|
||||
|
||||
public motionBlockFilterOptions = {
|
||||
property: 'motion_block_id',
|
||||
label: 'Motion block',
|
||||
isActive: false,
|
||||
options: []
|
||||
}
|
||||
public commentFilterOptions = {
|
||||
property: 'comment',
|
||||
isActive: false,
|
||||
options: []
|
||||
}
|
||||
|
||||
|
||||
|
||||
public staticFilterOptions = [
|
||||
// TODO favorite (attached to user:whoamI!)
|
||||
// TODO personalNote (attached to user:whoamI!)
|
||||
];
|
||||
|
||||
public constructor(store: StorageService,
|
||||
private workflowRepo: WorkflowRepositoryService,
|
||||
private categoryRepo: CategoryRepositoryService,
|
||||
private motionBlockRepo: MotionBlockRepositoryService,
|
||||
// private commentRepo: MotionCommentRepositoryService
|
||||
motionRepo: MotionRepositoryService,
|
||||
|
||||
){
|
||||
super(store, motionRepo);
|
||||
this.subscribeWorkflows();
|
||||
this.subscribeCategories();
|
||||
this.subscribeMotionBlocks();
|
||||
this.subscribeComments();
|
||||
}
|
||||
|
||||
private subscribeMotionBlocks(): void {
|
||||
this.motionBlockRepo.getViewModelListObservable().subscribe(motionBlocks => {
|
||||
const motionBlockOptions = [];
|
||||
motionBlocks.forEach(mb => {
|
||||
motionBlockOptions.push({
|
||||
condition: mb.id,
|
||||
label: mb.title,
|
||||
isActive: false
|
||||
});
|
||||
});
|
||||
motionBlockOptions.push('-');
|
||||
motionBlockOptions.push({
|
||||
condition: null,
|
||||
label: 'No motion block set',
|
||||
isActive: false
|
||||
});
|
||||
this.motionBlockFilterOptions.options = motionBlockOptions;
|
||||
this.updateFilterDefinitions(this.filterOptions);
|
||||
});
|
||||
}
|
||||
|
||||
private subscribeCategories(): void {
|
||||
this.categoryRepo.getViewModelListObservable().subscribe(categories => {
|
||||
const categoryOptions = [];
|
||||
categories.forEach(cat => {
|
||||
categoryOptions.push({
|
||||
condition: cat.id,
|
||||
label: cat.prefixedName,
|
||||
isActive: false
|
||||
});
|
||||
});
|
||||
this.categoryFilterOptions.options = categoryOptions;
|
||||
this.updateFilterDefinitions(this.filterOptions);
|
||||
});
|
||||
}
|
||||
|
||||
private subscribeWorkflows(): void {
|
||||
this.workflowRepo.getViewModelListObservable().subscribe(workflows => {
|
||||
const workflowOptions = [];
|
||||
workflows.forEach(workflow => {
|
||||
workflowOptions.push(workflow.name);
|
||||
workflow.states.forEach(state => {
|
||||
workflowOptions.push({
|
||||
condition: state.name,
|
||||
label: state.name,
|
||||
isActive: false
|
||||
});
|
||||
});
|
||||
});
|
||||
workflowOptions.push('-');
|
||||
workflowOptions.push({
|
||||
condition: null,
|
||||
label: 'no workflow set',
|
||||
isActive: false
|
||||
});
|
||||
this.flowFilterOptions.options = workflowOptions;
|
||||
this.updateFilterDefinitions(this.filterOptions);
|
||||
});
|
||||
}
|
||||
|
||||
private subscribeComments(): void {
|
||||
// TODO
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SortListService, OsSortingDefinition } from '../../../core/services/sort-list.service';
|
||||
import { ViewMotion } from '../models/view-motion';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MotionSortListService extends SortListService<ViewMotion> {
|
||||
public sortOptions: OsSortingDefinition<ViewMotion> = {
|
||||
sortProperty: 'callListWeight',
|
||||
sortAscending: true,
|
||||
options: [
|
||||
{ property: 'callListWeight', label: 'Call List' },
|
||||
{ property: 'supporters' },
|
||||
{ property: 'identifier' },
|
||||
{ property: 'title' },
|
||||
{ property: 'submitters' },
|
||||
{ property: 'category' },
|
||||
{ property: 'motion_block_id', label: 'Motion block' },
|
||||
{ property: 'state' }
|
||||
// choices from 2.3:
|
||||
// TODO creation date
|
||||
// TODO last modified
|
||||
]
|
||||
};
|
||||
protected name = 'Motion';
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Workflow } from '../../../shared/models/motions/workflow';
|
||||
import { ViewWorkflow } from '../models/view-workflow';
|
||||
import { DataSendService } from '../../../core/services/data-send.service';
|
||||
@ -30,7 +31,7 @@ export class WorkflowRepositoryService extends BaseRepository<ViewWorkflow, Work
|
||||
* @param dataSend
|
||||
*/
|
||||
public constructor(
|
||||
DS: DataStoreService,
|
||||
protected DS: DataStoreService,
|
||||
mapperService: CollectionStringModelMapperService,
|
||||
private dataSend: DataSendService
|
||||
) {
|
||||
@ -84,5 +85,6 @@ export class WorkflowRepositoryService extends BaseRepository<ViewWorkflow, Work
|
||||
states = states.concat(workflow.states);
|
||||
});
|
||||
return states;
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -14,9 +14,10 @@
|
||||
</div>
|
||||
</os-head-bar>
|
||||
|
||||
<div class="custom-table-header on-transition-fade">
|
||||
<button mat-button><span translate>SORT</span></button> <button mat-button><span translate>FILTER</span></button>
|
||||
</div>
|
||||
<mat-drawer-container class="on-transition-fade">
|
||||
<os-sort-filter-bar [sortService]="sortService" [filterService]="filterService"
|
||||
(searchFieldChange)="searchFilter($event)">
|
||||
</os-sort-filter-bar>
|
||||
|
||||
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
|
||||
<!-- Selector column -->
|
||||
@ -36,9 +37,9 @@
|
||||
<!-- prefix column -->
|
||||
<ng-container matColumnDef="group">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Group</mat-header-cell>
|
||||
<mat-cell *matCellDef="let user" (click)="selectItem(user, $event)">
|
||||
<div class="groupsCell">
|
||||
<span *ngIf="user.groups.length > 0">
|
||||
<mat-cell *matCellDef="let user">
|
||||
<div class='groupsCell'>
|
||||
<span *ngIf="user.groups && user.groups.length">
|
||||
<mat-icon>people</mat-icon>
|
||||
{{ user.groups }}
|
||||
</span>
|
||||
@ -143,3 +144,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</mat-menu>
|
||||
</mat-drawer-container>
|
||||
|
@ -5,12 +5,14 @@ import { Title } from '@angular/platform-browser';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { CsvExportService } from '../../../../core/services/csv-export.service';
|
||||
import { ChoiceService } from '../../../../core/services/choice.service';
|
||||
import { ListViewBaseComponent } from '../../../base/list-view-base';
|
||||
import { GroupRepositoryService } from '../../services/group-repository.service';
|
||||
import { PromptService } from '../../../../core/services/prompt.service';
|
||||
import { UserRepositoryService } from '../../services/user-repository.service';
|
||||
import { ViewUser } from '../../models/view-user';
|
||||
import { ChoiceService } from '../../../../core/services/choice.service';
|
||||
import { UserFilterListService } from '../../services/user-filter-list.service';
|
||||
import { UserSortListService } from '../../services/user-sort-list.service';
|
||||
|
||||
/**
|
||||
* Component for the user list view.
|
||||
@ -22,9 +24,10 @@ import { ChoiceService } from '../../../../core/services/choice.service';
|
||||
styleUrls: ['./user-list.component.scss']
|
||||
})
|
||||
export class UserListComponent extends ListViewBaseComponent<ViewUser> implements OnInit {
|
||||
|
||||
/**
|
||||
* /**
|
||||
* The usual constructor for components
|
||||
*
|
||||
* @param titleService Serivce for setting the title
|
||||
* @param translate Service for translation handling
|
||||
* @param matSnackBar Helper to diplay errors
|
||||
@ -34,6 +37,9 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
|
||||
* @param route the local route
|
||||
* @param csvExport CSV export Service,
|
||||
* @param promptService
|
||||
* @param groupRepo
|
||||
* @param filterService
|
||||
* @param sortService
|
||||
*/
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
@ -45,7 +51,9 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
protected csvExport: CsvExportService,
|
||||
private promptService: PromptService
|
||||
private promptService: PromptService,
|
||||
public filterService: UserFilterListService,
|
||||
public sortService: UserSortListService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar);
|
||||
|
||||
@ -56,13 +64,19 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
|
||||
/**
|
||||
* Init function
|
||||
*
|
||||
* sets the title, inits the table and calls the repo
|
||||
* sets the title, inits the table, sets sorting and filter options, subscribes
|
||||
* to filter/sort services
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
super.setTitle('Users');
|
||||
this.initTable();
|
||||
this.repo.getViewModelListObservable().subscribe(newUsers => {
|
||||
this.dataSource.data = newUsers;
|
||||
|
||||
|
||||
this.filterService.filter().subscribe(filteredData => {
|
||||
this.sortService.data = filteredData;
|
||||
});
|
||||
this.sortService.sort().subscribe(sortedData => {
|
||||
this.dataSource.data = sortedData;
|
||||
this.checkSelection();
|
||||
});
|
||||
}
|
||||
@ -240,5 +254,7 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
|
||||
public async setPresent(viewUser: ViewUser): Promise<void> {
|
||||
viewUser.user.is_present = !viewUser.user.is_present;
|
||||
await this.repo.update(viewUser.user, viewUser);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -92,12 +92,20 @@ export class ViewUser extends BaseViewModel {
|
||||
return this.user ? this.user.about_me : null;
|
||||
}
|
||||
|
||||
public get is_last_email_send(): boolean {
|
||||
if (this.user && this.user.last_email_send){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public constructor(user?: User, groups?: Group[]) {
|
||||
super();
|
||||
this._user = user;
|
||||
this._groups = groups;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* required by BaseViewModel. Don't confuse with the users title.
|
||||
*/
|
||||
|
@ -0,0 +1,84 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { FilterListService, OsFilter } from "../../../core/services/filter-list.service";
|
||||
import { StorageService } from "../../../core/services/storage.service";
|
||||
import { User } from "../../../shared/models/users/user";
|
||||
import { ViewUser } from "../models/view-user";
|
||||
import { GroupRepositoryService } from "./group-repository.service";
|
||||
import { UserRepositoryService } from "./user-repository.service";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class UserFilterListService extends FilterListService<User, ViewUser> {
|
||||
|
||||
protected name = 'User';
|
||||
|
||||
private userGroupFilterOptions = {
|
||||
isActive: false,
|
||||
property: 'group',
|
||||
label: 'User Group',
|
||||
options: []
|
||||
};
|
||||
|
||||
public staticFilterOptions = [
|
||||
{
|
||||
property: 'is_present',
|
||||
label: 'Presence',
|
||||
isActive: false,
|
||||
options: [
|
||||
{ condition: true, label: 'Is present'},
|
||||
{ condition: false, label: 'Is not present'}]
|
||||
}, {
|
||||
property: 'is_active',
|
||||
label: 'Active',
|
||||
isActive: false,
|
||||
options: [
|
||||
{ condition: true, label: 'Is active' },
|
||||
{ condition: false, label: 'Is not active' }]
|
||||
}, {
|
||||
property: 'is_committee',
|
||||
label: 'Committee',
|
||||
isActive: false,
|
||||
options: [
|
||||
{ condition: true, label: 'Is a committee' },
|
||||
{ condition: false, label: 'Is not a committee'}]
|
||||
}, {
|
||||
property: 'is_last_email_send',
|
||||
label: 'Last email send',
|
||||
isActive: false,
|
||||
options: [
|
||||
{ condition: true, label: 'Got an email' },
|
||||
{ condition: false, label: 'Didn\'t get an email' }]
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* getter for the filterOptions. Note that in this case, the options are
|
||||
* generated dynamically, as the options change with the datastore
|
||||
*/
|
||||
public get filterOptions(): OsFilter[] {
|
||||
return [this.userGroupFilterOptions].concat(this.staticFilterOptions);
|
||||
}
|
||||
|
||||
public constructor(store: StorageService, private groupRepo: GroupRepositoryService,
|
||||
repo: UserRepositoryService){
|
||||
super(store, repo);
|
||||
this.subscribeGroups();
|
||||
}
|
||||
|
||||
public subscribeGroups(): void {
|
||||
this.groupRepo.getViewModelListObservable().subscribe(groups => {
|
||||
const groupOptions = [];
|
||||
groupOptions.forEach(group => {
|
||||
groupOptions.push({
|
||||
condition: group.name,
|
||||
label: group.name,
|
||||
isActive: false
|
||||
});
|
||||
});
|
||||
this.userGroupFilterOptions.options = groupOptions;
|
||||
this.updateFilterDefinitions(this.filterOptions);
|
||||
})
|
||||
}
|
||||
}
|
26
client/src/app/site/users/services/user-sort-list.service.ts
Normal file
26
client/src/app/site/users/services/user-sort-list.service.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SortListService, OsSortingDefinition } from '../../../core/services/sort-list.service';
|
||||
import { ViewUser } from '../models/view-user';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class UserSortListService extends SortListService<ViewUser> {
|
||||
|
||||
public sortOptions: OsSortingDefinition<ViewUser> = {
|
||||
sortProperty: 'first_name',
|
||||
sortAscending: true,
|
||||
options: [
|
||||
{ property: 'first_name', label: 'Given name' },
|
||||
{ property: 'last_name', label: 'Surname' },
|
||||
{ property: 'is_present', label: 'Presence' },
|
||||
{ property: 'is_active', label: 'Is active' },
|
||||
{ property: 'is_committee', label: 'Is Committee' },
|
||||
{ property: 'participant_number', label: 'Number' },
|
||||
{ property: 'structure_level', label: 'Structure level' },
|
||||
{ property: 'comment' }
|
||||
// TODO email send?
|
||||
]
|
||||
};
|
||||
protected name = 'User';
|
||||
}
|
@ -110,6 +110,11 @@ body {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {width:0%; margin-left:0;}
|
||||
100% {width:100%;margin-left:-100%;}
|
||||
}
|
||||
|
||||
//custom table header for search button, filtering and more. Used in ListViews
|
||||
.custom-table-header {
|
||||
width: 100%;
|
||||
@ -118,6 +123,32 @@ body {
|
||||
text-align: right;
|
||||
background: white;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
button {
|
||||
border-radius: 0%;
|
||||
}
|
||||
|
||||
input {
|
||||
position: relative;
|
||||
max-width: 400px;
|
||||
z-index: 2;
|
||||
background-color: #EEE;
|
||||
padding-right: 5px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
input.vp {
|
||||
margin-left: -100%;
|
||||
max-width: 100%;
|
||||
animation-name: fadeIn;
|
||||
animation-duration: 0.3s;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
}
|
||||
|
||||
.os-listview-table {
|
||||
@ -272,3 +303,7 @@ button.mat-menu-item.selected {
|
||||
background-color: #e0e0e0 !important;
|
||||
color: rgba(0, 0, 0, 0.87) !important;
|
||||
}
|
||||
|
||||
.os-listview-table {
|
||||
min-height: 800px;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user