Rework sort and filter

More observable based, more scaleable filtering
based on an old branch of @FinnStutzenstein.

cleans some functions and provides some differend features.
This commit is contained in:
Sean Engelhardt 2019-05-07 16:25:39 +02:00
parent 65dbf37106
commit 850fcbe243
46 changed files with 1167 additions and 1049 deletions

View File

@ -136,7 +136,7 @@ export class DataStoreUpdateManagerService {
if (this.currentUpdateSlot) {
const request = new Deferred();
this.updateSlotRequests.push(request);
await request.promise;
await request;
}
this.currentUpdateSlot = new UpdateSlot(DS);
return this.currentUpdateSlot;

View File

@ -120,7 +120,7 @@ export class OperatorService implements OnAfterAppsLoaded {
private readonly _loaded: Deferred<void> = new Deferred();
public get loaded(): Promise<void> {
return this._loaded.promise;
return this._loaded;
}
/**

View File

@ -51,7 +51,7 @@ export class PingService {
isStable.resolve();
});
await Promise.all([gotConstants.promise, isStable.promise]);
await Promise.all([gotConstants, isStable]);
// Connects the ping-pong mechanism to the opening and closing of the connection.
this.websocketService.closeEvent.subscribe(() => this.stopPing());

View File

@ -13,7 +13,7 @@
* //
* ```
*/
export class Deferred<T = void> {
export class Deferred<T = void> extends Promise<T> {
/**
* The promise to wait for
*/
@ -28,9 +28,11 @@ export class Deferred<T = void> {
* Creates the promise and overloads the resolve function
*/
public constructor() {
this.promise = new Promise<T>(resolve => {
this.resolve = resolve;
let preResolve: (val?: T) => void;
super(resolve => {
preResolve = resolve;
});
this._resolve = preResolve;
}
/**

View File

@ -1,19 +1,16 @@
import { auditTime } from 'rxjs/operators';
import { BehaviorSubject, Observable } from 'rxjs';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { BaseRepository } from 'app/core/repositories/base-repository';
import { BaseModel } from '../../shared/models/base/base-model';
import { BaseModel } from 'app/shared/models/base/base-model';
import { BaseRepository } from '../repositories/base-repository';
import { BaseViewModel } from '../../site/base/base-view-model';
import { StorageService } from '../core-services/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
* @param count
*/
export interface OsFilter {
property: string;
@ -37,248 +34,296 @@ export type OsFilterOptions = (OsFilterOption | string)[];
*/
export interface OsFilterOption {
label: string;
condition: string | boolean | number | number[];
condition: OsFilterOptionCondition;
isActive?: boolean;
}
/**
* Define the type of a filter condition
*/
type OsFilterOptionCondition = string | boolean | number | number[];
/**
* 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 BaseFilterListService<V extends BaseViewModel> {
/**
* stores the currently used raw data to be used for the filter
*/
protected currentRawData: V[];
private inputData: V[];
/**
* Subscription for the inputData list.
* Acts as an semaphore for new filtered data
*/
protected inputDataSubscription: Subscription;
/**
* 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;
/**
* @returns the total count of items before the filter
*/
public get totalCount(): number {
return this.currentRawData ? this.currentRawData.length : 0;
public get unfilteredCount(): number {
return this.inputData ? this.inputData.length : 0;
}
/**
* The observable output for the filtered data
*/
private readonly outputSubject = new BehaviorSubject<V[]>([]);
/**
* @return Observable data for the filtered output subject
*/
public get outputObservable(): Observable<V[]> {
return this.outputSubject.asObservable();
}
/**
* @returns the amount of items that pass the filter service's filters
*/
public get filteredCount(): number {
return this.filteredData ? this.filteredData.length : 0;
return this.outputSubject.getValue().length;
}
/**
* Get the amount of filters currently in use by this filter Service
*
* @returns a number of filters
* @returns the amount of currently active filters
*/
public get activeFilterCount(): 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;
return this.filterDefinitions ? this.filterDefinitions.filter(filter => filter.count).length : 0;
}
/**
* Boolean indicationg if there are any filters described in this service
* Boolean indicating if there are any filters described in this service
*
* @returns true if there are defined filters (regardless of current state)
*/
public get hasFilterOptions(): boolean {
return this.filterDefinitions && this.filterDefinitions.length ? true : false;
return !!this.filterDefinitions && this.filterDefinitions.length > 0;
}
/**
* Constructor.
*
* @param name the name of the filter service
* @param store storage service, to read saved filter variables
*/
public constructor(protected store: StorageService, protected repo: BaseRepository<V, BaseModel>) {}
public constructor(protected name: string, private store: StorageService) {}
/**
* Initializes the filterService. Returns the filtered data as Observable
* Initializes the filterService.
*
* @param inputData Observable array with ViewModels
*/
public filter(): Observable<V[]> {
this.repo
.getViewModelListObservable()
.pipe(auditTime(10))
.subscribe(data => {
this.currentRawData = data;
this.filteredData = this.filterData(data);
this.filterDataOutput.next(this.filteredData);
public async initFilters(inputData: Observable<V[]>): Promise<void> {
const storedFilter = await this.store.get<OsFilter[]>('filter_' + this.name);
if (storedFilter) {
this.filterDefinitions = storedFilter;
} else {
this.filterDefinitions = this.getFilterDefinitions();
this.storeActiveFilters();
}
if (this.inputDataSubscription) {
this.inputDataSubscription.unsubscribe();
this.inputDataSubscription = null;
}
this.inputDataSubscription = inputData.subscribe(data => {
this.inputData = data;
this.updateFilteredData();
});
}
/**
* Enforce children implement a method that returns actual filter definitions
*/
protected abstract getFilterDefinitions(): OsFilter[];
/**
* Takes the filter definition from children and using {@link getFilterDefinitions}
* and sets/updates {@link filterDefinitions}
*/
public setFilterDefinitions(): void {
if (this.filterDefinitions) {
const newDefinitions = this.getFilterDefinitions();
this.store.get('filter_' + this.name).then((storedDefinition: OsFilter[]) => {
for (const newDef of newDefinitions) {
let count = 0;
const matchingExistingFilter = storedDefinition.find(oldDef => oldDef.property === newDef.property);
for (const option of newDef.options) {
if (typeof option === 'object') {
if (matchingExistingFilter && matchingExistingFilter.options) {
const existingOption = matchingExistingFilter.options.find(
o =>
typeof o !== 'string' &&
JSON.stringify(o.condition) === JSON.stringify(option.condition)
) as OsFilterOption;
if (existingOption) {
option.isActive = existingOption.isActive;
}
if (option.isActive) {
count++;
}
}
}
}
newDef.count = count;
}
this.filterDefinitions = newDefinitions;
this.storeActiveFilters();
});
this.loadStorageDefinition(this.filterDefinitions);
return this.filterDataOutput;
}
}
/**
* Helper function to get the `viewModelListObservable` of a given repository object and creates dynamic filters for them
*
* @param repo repository to create dynamic filters from
* @param filter the OSFilter for the filter property
* @param noneOptionLabel The label of the non option, if set
* @param exexcludeIds Set if certain ID's should be excluded from filtering
*/
protected updateFilterForRepo(
repo: BaseRepository<BaseViewModel, BaseModel>,
filter: OsFilter,
noneOptionLabel?: string,
excludeIds?: number[]
): void {
repo.getViewModelListObservable().subscribe(viewModel => {
if (viewModel && viewModel.length) {
let filterProperties: (OsFilterOption | string)[];
filterProperties = viewModel
.filter(model => (excludeIds && excludeIds.length ? !excludeIds.includes(model.id) : true))
.map(model => {
return {
condition: model.id,
label: model.getTitle()
};
});
filterProperties.push('-');
filterProperties.push({
condition: null,
label: noneOptionLabel
});
filter.options = filterProperties;
this.setFilterDefinitions();
}
});
}
/**
* Update the filtered data and store the current filter options
*/
public storeActiveFilters(): void {
this.updateFilteredData();
this.store.set('filter_' + this.name, this.filterDefinitions);
}
/**
* Applies current filters in {@link filterDefinitions} to the {@link inputData} list
* and publishes the filtered data to the observable {@link outputSubject}
*/
private updateFilteredData(): void {
let filteredData: V[];
if (!this.inputData) {
filteredData = [];
} else {
const preFilteredList = this.preFilter(this.inputData);
if (preFilteredList) {
this.inputData = preFilteredList;
}
if (!this.filterDefinitions || !this.filterDefinitions.length) {
filteredData = this.inputData;
} else {
filteredData = this.inputData.filter(item =>
this.filterDefinitions.every(filter => !filter.count || this.checkIncluded(item, filter))
);
}
}
this.outputSubject.next(filteredData);
}
/**
* Had to be overwritten by children if required
* Adds the possibility to filter the inputData before the user applied filter
*
* @param rawInputData will be set to {@link this.inputData}
* @returns should be a filtered version of `rawInputData`. Returns void if unused
*/
protected preFilter(rawInputData: V[]): V[] | void {}
/**
* Toggles a filter option, to be called after a checkbox state has changed.
*
* @param filterName a filter name as string
* @param option filter option
*/
public toggleFilterOption(filterName: string, option: OsFilterOption): void {
option.isActive ? this.removeFilterOption(filterName, option) : this.addFilterOption(filterName, option);
this.storeActiveFilters();
}
/**
* Apply a newly created filter
* @param filter
*
* @param filterProperty new filter as string
* @param option filter option
*/
public addFilterOption(filterName: string, option: OsFilterOption): void {
const filter = this.filterDefinitions.find(f => f.property === filterName);
protected addFilterOption(filterProperty: string, option: OsFilterOption): void {
const filter = this.filterDefinitions.find(f => f.property === filterProperty);
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) {
filter.count = 1;
} else {
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();
}
}
/**
* Remove a filter option.
*
* @param filterName: The property name of this filter
* @param option: The option to disable
* @param filterName The property name of this filter
* @param option The option to disable
*/
public removeFilterOption(filterName: string, option: OsFilterOption): void {
const filter = this.filterDefinitions.find(f => f.property === filterName);
protected removeFilterOption(filterProperty: string, option: OsFilterOption): void {
const filter = this.filterDefinitions.find(f => f.property === filterProperty);
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
*/
protected 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 ||
(Array.isArray(o.condition) &&
Array.isArray(option.condition) &&
o.label === option.label))
) 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
*/
protected 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 (filter.count) {
filter.count -= 1;
}
}
if (!excluded) {
filteredData.push(newItem);
}
});
return filteredData;
}
}
/**
* Checks if a given ViewBaseModel passes the filter.
*
* @param item
* @param filter
* @returns true if the item is to be dispalyed according to the filter
* @param item Usually a view model
* @param filter The filter to check
* @returns true if the item is to be displayed according to the filter
*/
private checkIncluded(item: V, filter: OsFilter): boolean {
const nullFilter = filter.options.find(
@ -380,22 +425,30 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
/**
* Removes all active options of a given filter, clearing it
*
* @param filter
* @param update
*/
public clearFilter(filter: OsFilter): void {
public clearFilter(filter: OsFilter, update: boolean = true): void {
filter.options.forEach(option => {
if (typeof option === 'object' && option.isActive) {
this.removeFilterOption(filter.property, option);
}
});
if (update) {
this.storeActiveFilters();
}
}
/**
* Removes all filters currently in use from this filterService
*/
public clearAllFilters(): void {
this.filterDefinitions.forEach(filter => {
this.clearFilter(filter);
});
if (this.filterDefinitions && this.filterDefinitions.length) {
this.filterDefinitions.forEach(filter => {
this.clearFilter(filter, false);
});
this.storeActiveFilters();
}
}
}

View File

@ -1,155 +1,191 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { BaseViewModel } from '../../site/base/base-view-model';
import { BehaviorSubject, Subscription, Observable } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { BaseViewModel } from '../../site/base/base-view-model';
import { StorageService } from '../core-services/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>[];
sortAscending: boolean;
}
/**
* 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> {
export interface OsSortingOption<V> {
property: keyof V;
label?: string;
}
@Injectable({
providedIn: 'root'
})
/**
* Base class for generic sorting purposes
*/
export abstract class BaseSortListService<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[];
private inputData: V[];
/**
* Subscription for the inputData list.
* Acts as an semaphore for new filtered data
*/
private inputDataSubscription: Subscription | null;
/**
* Observable output that submits the newly sorted data each time a sorting has been done
*/
private outputSubject = new BehaviorSubject<V[]>([]);
/**
* @returns the sorted output subject as observable
*/
public get outputObservable(): Observable<V[]> {
return this.outputSubject.asObservable();
}
/**
* The current sorting definitions
*/
public sortOptions: OsSortingDefinition<V>;
private sortDefinition: 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}.
* The sorting function according to current settings.
*/
private sortFn: (a: V, b: V) => number;
/**
* default sorting to use if the client was not initialized before
*/
protected defaultSorting: keyof V = 'id';
/**
* Constructor. Does nothing. TranslateService is used for localeCompeare.
*/
public constructor(protected 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
*
* @param ascending ascending sorting if true, descending sorting if false
*/
public set ascending(ascending: boolean) {
this.sortOptions.sortAscending = ascending;
this.updateSortFn();
this.saveStorageDefinition();
this.doAsyncSorting();
this.sortDefinition.sortAscending = ascending;
this.updateSortDefinitions();
}
/**
* get the current sorting order
* @param returns wether current the sorting is ascending or descending
*/
public get ascending(): boolean {
return this.sortOptions.sortAscending;
return this.sortDefinition.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.
*
* @param property a part of a view model
*/
public set sortProperty(property: string) {
if (this.sortOptions.sortProperty === (property as keyof V)) {
public set sortProperty(property: keyof V) {
if (this.sortDefinition.sortProperty === property) {
this.ascending = !this.ascending;
this.updateSortFn();
} else {
this.sortOptions.sortProperty = property as keyof V;
this.sortOptions.sortAscending = true;
this.updateSortFn();
this.doAsyncSorting();
this.sortDefinition.sortProperty = property;
this.sortDefinition.sortAscending = true;
}
this.saveStorageDefinition();
this.updateSortDefinitions();
}
/**
* get the property of the viewModel the sorting is based on.
* @returns the current sorting property
*/
public get sortProperty(): string {
return this.sortOptions ? (this.sortOptions.sortProperty as string) : '';
public get sortProperty(): keyof V {
return this.sortDefinition.sortProperty;
}
/**
* @returns wether sorting is active or not
*/
public get isActive(): boolean {
return this.sortOptions && this.sortOptions.options.length > 0;
return this.sortDefinition && this.sortOptions.length > 0;
}
/**
* Enforce children to implement sortOptions
*/
public abstract sortOptions: OsSortingOption<V>[];
/**
* Constructor.
*
* @param name the name of the sort view, required for store access
* @param translate required for language sensitive comparing
* @param store to save and load sorting preferences
*/
public constructor(protected name: string, protected translate: TranslateService, private store: StorageService) {}
/**
* Enforce children to implement a method that returns the fault sorting
*/
protected abstract async getDefaultDefinition(): Promise<OsSortingDefinition<V>>;
/**
* 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 async initSorting(inputObservable: Observable<V[]>): Promise<void> {
if (this.inputDataSubscription) {
this.inputDataSubscription.unsubscribe();
this.inputDataSubscription = null;
}
if (!this.sortDefinition) {
this.sortDefinition = await this.store.get<OsSortingDefinition<V> | null>('sorting_' + this.name);
if (this.sortDefinition && this.sortDefinition.sortProperty) {
this.updateSortedData();
} else {
this.sortDefinition = await this.getDefaultDefinition();
this.updateSortDefinitions();
}
}
this.inputDataSubscription = inputObservable.subscribe(data => {
this.inputData = data;
this.updateSortedData();
});
}
/**
* Change the property and the sorting direction at the same time
* @param property
* @param ascending
*
* @param property a sorting property of a view model
* @param ascending ascending or descending
*/
public setSorting(property: string, ascending: boolean): void {
this.sortOptions.sortProperty = property as keyof V;
this.sortOptions.sortAscending = ascending;
this.saveStorageDefinition();
this.updateSortFn();
this.doAsyncSorting();
public setSorting(property: keyof V, ascending: boolean): void {
this.sortDefinition.sortProperty = property;
this.sortDefinition.sortAscending = ascending;
this.updateSortDefinitions();
}
/**
* Retrieves the currently active icon for an option.
*
* @param option
* @returns the name of the sorting icon, fit to material icon ligatures
*/
public getSortIcon(option: OsSortingItem<V>): string {
if (!this.sortProperty || this.sortProperty !== (option.property as string)) {
public getSortIcon(option: OsSortingOption<V>): string {
if (this.sortProperty !== option.property) {
return '';
}
return this.ascending ? 'arrow_downward' : 'arrow_upward';
}
public getSortLabel(option: OsSortingItem<V>): string {
/**
* Determines and returns an untranslated sorting label as string
*
* @param option The sorting option to a ViewModel
* @returns a sorting label as string
*/
public getSortLabel(option: OsSortingOption<V>): string {
if (option.label) {
return option.label;
}
@ -158,51 +194,17 @@ export abstract class BaseSortListService<V extends BaseViewModel> {
}
/**
* Retrieve the currently saved sorting definition from the borwser's
* store
* Saves the current sorting definitions to the local store
*/
private async loadStorageDefinition(): Promise<void> {
const sorting: OsSortingDefinition<V> | null = await this.store.get('sorting_' + this.name);
if (sorting) {
if (sorting.sortProperty) {
this.sortOptions.sortProperty = sorting.sortProperty;
if (sorting.sortAscending !== undefined) {
this.sortOptions.sortAscending = sorting.sortAscending;
}
}
} else {
this.sortOptions.sortProperty = this.defaultSorting;
this.sortOptions.sortAscending = true;
}
this.updateSortFn();
this.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);
});
private updateSortDefinitions(): void {
this.updateSortedData();
this.store.set('sorting_' + this.name, this.sortDefinition);
}
/**
* Sorts an array of data synchronously, using the currently configured sorting
*
* @param data
* @param data Array of ViewModels
* @returns the data, sorted with the definitions of this service
*/
public sortSync(data: V[]): V[] {
@ -213,60 +215,62 @@ export abstract class BaseSortListService<V extends BaseViewModel> {
* Recreates the sorting function. Is supposed to be called on init and
* every time the sorting (property, ascending/descending) or the language changes
*/
protected updateSortFn(): void {
const property = this.sortProperty as string;
const ascending = this.ascending;
const intl = new Intl.Collator(this.translate.currentLang); // TODO: observe and update sorting on language 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 1;
} else if (!secondProperty) {
return -1;
} else {
throw new TypeError('sorting of items failed because of mismatched types');
}
} else {
if (
(firstProperty === null || firstProperty === undefined) &&
(secondProperty === null || secondProperty === undefined)
) {
return 1;
}
switch (typeof firstProperty) {
case 'boolean':
if (firstProperty === false && secondProperty === true) {
return -1;
} else {
return 1;
}
case 'number':
return firstProperty > secondProperty ? 1 : -1;
case 'string':
protected updateSortedData(): void {
if (this.inputData) {
const property = this.sortProperty as string;
const intl = new Intl.Collator(this.translate.currentLang);
this.outputSubject.next(
this.inputData.sort((itemA, itemB) => {
const firstProperty = this.ascending ? itemA[property] : itemB[property];
const secondProperty = this.ascending ? itemB[property] : itemA[property];
if (typeof firstProperty !== typeof secondProperty) {
// undefined/null items should always land at the end
if (!firstProperty) {
return 1;
}
return intl.compare(firstProperty, secondProperty);
case 'function':
const a = firstProperty();
const b = secondProperty();
return intl.compare(a, b);
case 'object':
if (firstProperty instanceof Date) {
return firstProperty > secondProperty ? 1 : -1;
} else if (!secondProperty) {
return -1;
} else {
return intl.compare(firstProperty.toString(), secondProperty.toString());
throw new TypeError('sorting of items failed because of mismatched types');
}
case 'undefined':
return 1;
default:
return -1;
}
}
};
} else {
if (
(firstProperty === null || firstProperty === undefined) &&
(secondProperty === null || secondProperty === undefined)
) {
return 1;
}
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 intl.compare(firstProperty, secondProperty);
case 'function':
const a = firstProperty();
const b = secondProperty();
return intl.compare(a, b);
case 'object':
if (firstProperty instanceof Date) {
return firstProperty > secondProperty ? 1 : -1;
} else {
return intl.compare(firstProperty.toString(), secondProperty.toString());
}
case 'undefined':
return 1;
default:
return -1;
}
}
})
);
}
}
}

View File

@ -1,13 +0,0 @@
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;
}

View File

@ -1,8 +0,0 @@
<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>{{ data.getSortLabel(option) | translate}}</span>
</button>
</mat-list-item>
</mat-nav-list>

View File

@ -1,9 +1,9 @@
<mat-accordion (keyup)=checkKeyEvent($event)>
<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' : ''}}
{{ filter.count ? 'checked' : '' }}
</mat-icon>
<span>{{ service.getFilterName(filter) | translate }}</span>
</mat-panel-title>
@ -12,7 +12,11 @@
<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)">
<mat-checkbox
class="filter-title"
[checked]="option.isActive"
(change)="service.toggleFilterOption(filter.property, option)"
>
{{ option.label | translate }}
</mat-checkbox>
</div>

View File

@ -0,0 +1,30 @@
div.indent {
margin-left: 24px;
}
mat-divider {
margin-top: 5px;
margin-bottom: 5px;
}
.mat-expansion-panel {
width: 400px;
max-width: 95vw;
}
.filter-subtitle {
margin-top: 5px;
margin-bottom: 5px;
opacity: 0.9;
font-style: italic;
}
// adds breaks to mat-checkboxes with long labels
::ng-deep .mat-checkbox-layout {
white-space: normal !important;
}
// rather than center the checkbox, put the checkbox in the first line
::ng-deep .mat-checkbox-inner-container {
margin-top: 3px !important;
}

View File

@ -58,6 +58,7 @@ export class FilterMenuComponent implements OnInit {
this.dismissed.next(true);
}
}
public isFilter(option: OsFilterOption): boolean {
return typeof option === 'string' ? false : true;
}

View File

@ -0,0 +1,8 @@
<mat-nav-list class="main-nav">
<mat-list-item *ngFor="let option of this.data.sortOptions" (click)="clickedOption(option.property)">
<button mat-menu-item>
<mat-icon>{{ data.getSortIcon(option) }}</mat-icon>
<span>{{ data.getSortLabel(option) | translate }}</span>
</button>
</mat-list-item>
</mat-nav-list>

View File

@ -1,11 +1,10 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module';
import { OsSortBottomSheetComponent } from './os-sort-bottom-sheet.component';
import { SortBottomSheetComponent } from './sort-bottom-sheet.component';
describe('OsSortBottomSheetComponent', () => {
// let component: OsSortBottomSheetComponent<any>;
let fixture: ComponentFixture<OsSortBottomSheetComponent<any>>;
describe('SortBottomSheetComponent', () => {
let fixture: ComponentFixture<SortBottomSheetComponent<any>>;
beforeEach(async(() => {
TestBed.configureTestingModule({
@ -14,7 +13,7 @@ describe('OsSortBottomSheetComponent', () => {
}));
beforeEach(() => {
fixture = TestBed.createComponent(OsSortBottomSheetComponent);
fixture = TestBed.createComponent(SortBottomSheetComponent);
// component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -9,17 +9,17 @@ import { BaseViewModel } from 'app/site/base/base-view-model';
* usage:
* ```
* @ViewChild('sortBottomSheet')
* public sortBottomSheet: OsSortBottomSheetComponent<V>;
* public sortBottomSheet: SortBottomSheetComponent<V>;
* ...
* this.bottomSheet.open(OsSortBottomSheetComponent, { data: SortService });
* this.bottomSheet.open(SortBottomSheetComponent, { data: SortService });
* ```
*/
@Component({
selector: 'os-sort-bottom-sheet',
templateUrl: './os-sort-bottom-sheet.component.html',
styleUrls: ['./os-sort-bottom-sheet.component.scss']
templateUrl: './sort-bottom-sheet.component.html',
styleUrls: ['./sort-bottom-sheet.component.scss']
})
export class OsSortBottomSheetComponent<V extends BaseViewModel> implements OnInit {
export class SortBottomSheetComponent<V extends BaseViewModel> implements OnInit {
/**
* Constructor. Gets a reference to itself (for closing after interaction)
* @param data
@ -31,10 +31,10 @@ export class OsSortBottomSheetComponent<V extends BaseViewModel> implements OnIn
) {}
/**
* init fucntion. Closes inmediately if no sorting is available.
* init function. Closes immediately if no sorting is available.
*/
public ngOnInit(): void {
if (!this.data || !this.data.sortOptions || !this.data.sortOptions.options.length) {
if (!this.data || !this.data.sortOptions || !this.data.sortOptions.length) {
throw new Error('No sorting available for a sorting list');
}
}

View File

@ -1,7 +1,7 @@
<div class="custom-table-header flex-spaced on-transition-fade">
<div class="filter-count" *ngIf="filterService">
<span>{{ displayedCount }}&nbsp;</span><span translate>of</span>
<span>&nbsp;{{ filterService.totalCount }}</span>
<span>&nbsp;{{ totalCount }}</span>
<span *ngIf="extraItemInfo">&nbsp;·&nbsp;{{ extraItemInfo }}</span>
</div>
<div class="current-filters" *ngIf="filterService && filterService.activeFilterCount">
@ -61,12 +61,9 @@
<!-- 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"
>
<mat-list-item *ngFor="let option of sortOptions" (click)="sortOption = option">
<button mat-menu-item>
<mat-icon>{{ sortService.getSortIcon(option) }}</mat-icon>
<mat-icon>{{ getSortIcon(option) }}</mat-icon>
<span>{{ sortService.getSortLabel(option) | translate }}</span>
</button>
</mat-list-item>

View File

@ -1,10 +1,10 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module';
import { OsSortFilterBarComponent } from './os-sort-filter-bar.component';
import { SortFilterBarComponent } from './sort-filter-bar.component';
describe('OsSortFilterBarComponent', () => {
let component: OsSortFilterBarComponent<any>;
let component: SortFilterBarComponent<any>;
let fixture: ComponentFixture<any>;
beforeEach(async(() => {
@ -14,7 +14,7 @@ describe('OsSortFilterBarComponent', () => {
}));
beforeEach(() => {
fixture = TestBed.createComponent(OsSortFilterBarComponent);
fixture = TestBed.createComponent(SortFilterBarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -4,9 +4,9 @@ import { MatBottomSheet } from '@angular/material';
import { TranslateService } from '@ngx-translate/core';
import { BaseViewModel } from 'app/site/base/base-view-model';
import { OsSortBottomSheetComponent } from './os-sort-bottom-sheet/os-sort-bottom-sheet.component';
import { SortBottomSheetComponent } from './sort-bottom-sheet/sort-bottom-sheet.component';
import { FilterMenuComponent } from './filter-menu/filter-menu.component';
import { OsSortingItem } from 'app/core/ui-services/base-sort-list.service';
import { OsSortingOption } from 'app/core/ui-services/base-sort-list.service';
import { BaseSortListService } from 'app/core/ui-services/base-sort-list.service';
import { ViewportService } from 'app/core/ui-services/viewport.service';
import { BaseFilterListService } from 'app/core/ui-services/base-filter-list.service';
@ -27,10 +27,10 @@ import { BaseFilterListService } from 'app/core/ui-services/base-filter-list.ser
*/
@Component({
selector: 'os-sort-filter-bar',
templateUrl: './os-sort-filter-bar.component.html',
styleUrls: ['./os-sort-filter-bar.component.scss']
templateUrl: './sort-filter-bar.component.html',
styleUrls: ['./sort-filter-bar.component.scss']
})
export class OsSortFilterBarComponent<V extends BaseViewModel> {
export class SortFilterBarComponent<V extends BaseViewModel> {
/**
* The currently active sorting service for the list view
*/
@ -58,6 +58,7 @@ export class OsSortFilterBarComponent<V extends BaseViewModel> {
@Output()
public searchFieldChange = new EventEmitter<string>();
/**
* The filter side drawer
*/
@ -68,7 +69,7 @@ export class OsSortFilterBarComponent<V extends BaseViewModel> {
* The bottom sheet used to alter sorting in mobile view
*/
@ViewChild('sortBottomSheet')
public sortBottomSheet: OsSortBottomSheetComponent<V>;
public sortBottomSheet: SortBottomSheetComponent<V>;
/**
* The 'opened/active' state of the fulltext filter input field
@ -87,6 +88,21 @@ export class OsSortFilterBarComponent<V extends BaseViewModel> {
}
}
/**
* Return the total count of potential filters
*/
public get totalCount(): number {
return this.filterService.unfilteredCount;
}
public get sortOptions(): any {
return this.sortService.sortOptions;
}
public set sortOption(option: OsSortingOption<V>) {
this.sortService.sortProperty = option.property;
}
/**
* Constructor. Also creates a filtermenu component and a bottomSheet
* @param translate
@ -106,7 +122,7 @@ export class OsSortFilterBarComponent<V extends BaseViewModel> {
*/
public openSortDropDown(): void {
if (this.vp.isMobile) {
const bottomSheetRef = this.bottomSheet.open(OsSortBottomSheetComponent, { data: this.sortService });
const bottomSheetRef = this.bottomSheet.open(SortBottomSheetComponent, { data: this.sortService });
bottomSheetRef.afterDismissed().subscribe(result => {
if (result) {
this.sortService.sortProperty = result;
@ -136,23 +152,18 @@ export class OsSortFilterBarComponent<V extends BaseViewModel> {
/**
* Checks if there is an active FilterService present
* @returns wether the filters are present or not
*/
public get hasFilters(): boolean {
if (this.filterService && this.filterService.hasFilterOptions) {
return true;
}
return false;
return this.filterService && this.filterService.hasFilterOptions;
}
/**
* 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';
public getSortIcon(option: OsSortingOption<V>): string {
return this.sortService.getSortIcon(option);
}
/**
@ -160,7 +171,7 @@ export class OsSortFilterBarComponent<V extends BaseViewModel> {
* the property is used.
* @param option
*/
public getSortLabel(option: OsSortingItem<V>): string {
public getSortLabel(option: OsSortingOption<V>): string {
if (option.label) {
return option.label;
}

View File

@ -68,9 +68,9 @@ import { PromptDialogComponent } from './components/prompt-dialog/prompt-dialog.
import { SortingListComponent } from './components/sorting-list/sorting-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';
import { SortFilterBarComponent } from './components/sort-filter-bar/sort-filter-bar.component';
import { SortBottomSheetComponent } from './components/sort-filter-bar/sort-bottom-sheet/sort-bottom-sheet.component';
import { FilterMenuComponent } from './components/sort-filter-bar/filter-menu/filter-menu.component';
import { LogoComponent } from './components/logo/logo.component';
import { C4DialogComponent, CopyrightSignComponent } from './components/copyright-sign/copyright-sign.component';
import { ProjectorButtonComponent } from './components/projector-button/projector-button.component';
@ -191,7 +191,7 @@ import { PrecisionPipe } from './pipes/precision.pipe';
SortingListComponent,
EditorModule,
SortingTreeComponent,
OsSortFilterBarComponent,
SortFilterBarComponent,
LogoComponent,
CopyrightSignComponent,
C4DialogComponent,
@ -220,8 +220,8 @@ import { PrecisionPipe } from './pipes/precision.pipe';
SortingListComponent,
SortingTreeComponent,
ChoiceDialogComponent,
OsSortFilterBarComponent,
OsSortBottomSheetComponent,
SortFilterBarComponent,
SortBottomSheetComponent,
FilterMenuComponent,
LogoComponent,
CopyrightSignComponent,
@ -241,10 +241,10 @@ import { PrecisionPipe } from './pipes/precision.pipe';
SearchValueSelectorComponent,
SortingListComponent,
SortingTreeComponent,
OsSortFilterBarComponent,
OsSortBottomSheetComponent,
SortFilterBarComponent,
SortBottomSheetComponent,
DecimalPipe
],
entryComponents: [OsSortBottomSheetComponent, C4DialogComponent]
entryComponents: [SortBottomSheetComponent, C4DialogComponent]
})
export class SharedModule {}

View File

@ -30,7 +30,8 @@ import { StorageService } from 'app/core/core-services/storage.service';
templateUrl: './agenda-list.component.html',
styleUrls: ['./agenda-list.component.scss']
})
export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item> implements OnInit {
export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item, ItemRepositoryService>
implements OnInit {
/**
* Determine the display columns in desktop view
*/
@ -106,7 +107,7 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item> i
private agendaPdfService: AgendaPdfService,
private pdfService: PdfDocumentService
) {
super(titleService, translate, matSnackBar, route, storage, filterService);
super(titleService, translate, matSnackBar, repo, route, storage, filterService);
// activate multiSelect mode for this listview
this.canMultiSelect = true;
@ -125,14 +126,6 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item> i
this.setFulltextFilter();
}
protected onFilter(): void {
this.filterService.filter().subscribe(newAgendaItems => {
newAgendaItems.sort((a, b) => a.weight - b.weight);
this.dataSource.data = newAgendaItems;
this.checkSelection();
});
}
/**
* Links to the content object.
*

View File

@ -1,32 +1,34 @@
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { auditTime, map } from 'rxjs/operators';
import { BaseFilterListService, OsFilter, OsFilterOption } from 'app/core/ui-services/base-filter-list.service';
import { itemVisibilityChoices } from 'app/shared/models/agenda/item';
import { ViewItem } from '../models/view-item';
import { StorageService } from 'app/core/core-services/storage.service';
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
/**
* Filter the agenda list
*/
@Injectable({
providedIn: 'root'
})
export class AgendaFilterListService extends BaseFilterListService<ViewItem> {
protected name = 'Agenda';
public filterOptions: OsFilter[] = [];
/**
* Constructor. Also creates the dynamic filter options
*
* @param store
* @param repo
* @param translate Translation service
*/
public constructor(store: StorageService, repo: ItemRepositoryService, private translate: TranslateService) {
super(store, repo);
this.filterOptions = [
public constructor(store: StorageService, private translate: TranslateService) {
super('Agenda', store);
}
/**
* @returns the filter definition
*/
protected getFilterDefinitions(): OsFilter[] {
return [
{
label: 'Visibility',
property: 'type',
@ -41,37 +43,26 @@ export class AgendaFilterListService extends BaseFilterListService<ViewItem> {
]
}
];
this.updateFilterDefinitions(this.filterOptions);
}
/**
* @override from base filter list service: Added custom filtering of items
* Initializes the filterService. Returns the filtered data as Observable
* @override from base filter list service
*
* @returns the list of ViewItems without the types
*/
public filter(): Observable<ViewItem[]> {
this.repo
.getViewModelListObservable()
.pipe(auditTime(10))
// Exclude items that are just there to provide a list of speakers. They have many
// restricted fields and must not be shown in the agenda!
.pipe(map(itemList => itemList.filter(item => item.type !== undefined)))
.subscribe(data => {
this.currentRawData = data;
this.filteredData = this.filterData(data);
this.filterDataOutput.next(this.filteredData);
});
this.loadStorageDefinition(this.filterDefinitions);
return this.filterDataOutput;
protected preFilter(viewItems: ViewItem[]): ViewItem[] {
return viewItems.filter(item => item.type !== undefined);
}
/**
* helper function to create options for visibility filters
*
* @returns a list of choices to filter from
*/
private createVisibilityFilterOptions(): OsFilterOption[] {
const options = [];
itemVisibilityChoices.forEach(choice => {
options.push({
condition: choice.key as number,
label: choice.name
});
});
return options;
return itemVisibilityChoices.map(choice => ({
condition: choice.key as number,
label: choice.name
}));
}
}

View File

@ -23,7 +23,9 @@ import { ViewAssignment, AssignmentPhases } from '../../models/view-assignment';
templateUrl: './assignment-list.component.html',
styleUrls: ['./assignment-list.component.scss']
})
export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignment, Assignment> implements OnInit {
export class AssignmentListComponent
extends ListViewBaseComponent<ViewAssignment, Assignment, AssignmentRepositoryService>
implements OnInit {
/**
* The different phases of an assignment. Info is fetched from server
*/
@ -57,7 +59,7 @@ export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignmen
private router: Router,
public operator: OperatorService
) {
super(titleService, translate, matSnackBar, route, storage, filterService, sortService);
super(titleService, translate, matSnackBar, repo, route, storage, filterService, sortService);
// activate multiSelect mode for this list view
this.canMultiSelect = true;
}

View File

@ -1,53 +1,45 @@
import { Injectable } from '@angular/core';
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
import { BaseFilterListService, OsFilter } from 'app/core/ui-services/base-filter-list.service';
import { BaseFilterListService, OsFilter, OsFilterOption } from 'app/core/ui-services/base-filter-list.service';
import { StorageService } from 'app/core/core-services/storage.service';
import { ViewAssignment, AssignmentPhases } from '../models/view-assignment';
/**
* Filter service for the assignment list
*/
@Injectable({
providedIn: 'root'
})
export class AssignmentFilterListService extends BaseFilterListService<ViewAssignment> {
protected name = 'Assignment';
/**
* Getter for the current filter options
*
* @returns filter definitions to use
*/
public get filterOptions(): OsFilter[] {
return [this.phaseFilter];
}
/**
* Filter for assignment phases. Defined in the servers' constants
*/
public phaseFilter: OsFilter = {
property: 'phase',
options: []
};
/**
* Constructor. Activates the phase options subscription
*
* @param store StorageService
* @param assignmentRepo Repository
* @param constants the openslides constant service to get the assignment options
* @param translate translate service
*/
public constructor(store: StorageService, assignmentRepo: AssignmentRepositoryService) {
super(store, assignmentRepo);
this.createPhaseOptions();
public constructor(store: StorageService) {
super('Assignments', store);
}
/**
* Subscribes to the phases of an assignment that are defined in the server's
* constants
* @returns the filter definition
*/
private createPhaseOptions(): void {
this.phaseFilter.options = AssignmentPhases.map(ap => {
protected getFilterDefinitions(): OsFilter[] {
return [
{
label: 'Phase',
property: 'phase',
options: this.createPhaseOptions()
}
];
}
/**
* Creates options for assignment phases
*/
private createPhaseOptions(): OsFilterOption[] {
return AssignmentPhases.map(ap => {
return { label: ap.display_name, condition: ap.value, isActive: false };
});
this.updateFilterDefinitions(this.filterOptions);
}
}

View File

@ -1,20 +1,46 @@
import { Injectable } from '@angular/core';
import { BaseSortListService, OsSortingDefinition } from 'app/core/ui-services/base-sort-list.service';
import { TranslateService } from '@ngx-translate/core';
import { BaseSortListService, OsSortingDefinition, OsSortingOption } from 'app/core/ui-services/base-sort-list.service';
import { StorageService } from 'app/core/core-services/storage.service';
import { ViewAssignment } from '../models/view-assignment';
/**
* Sorting service for the assignment list
*/
@Injectable({
providedIn: 'root'
})
export class AssignmentSortListService extends BaseSortListService<ViewAssignment> {
public sortOptions: OsSortingDefinition<ViewAssignment> = {
sortProperty: 'assignment',
sortAscending: true,
options: [
{ property: 'assignment', label: 'Name' },
{ property: 'phase', label: 'Phase' },
{ property: 'candidateAmount', label: 'Number of candidates' }
]
};
protected name = 'Assignment';
/**
* Define the sort options
*/
public sortOptions: OsSortingOption<ViewAssignment>[] = [
{ property: 'assignment', label: 'Name' },
{ property: 'phase', label: 'Phase' },
{ property: 'candidateAmount', label: 'Number of candidates' }
];
/**
* Constructor.
*
* @param translate required by parent
* @param storage required by parent
*/
public constructor(translate: TranslateService, storage: StorageService) {
super('Assignment', translate, storage);
}
/**
* Required by parent
*
* @returns the default sorting strategy
*/
public async getDefaultDefinition(): Promise<OsSortingDefinition<ViewAssignment>> {
return {
sortProperty: 'assignment',
sortAscending: true
};
}
}

View File

@ -10,9 +10,14 @@ import { BaseSortListService } from 'app/core/ui-services/base-sort-list.service
import { BaseFilterListService } from 'app/core/ui-services/base-filter-list.service';
import { BaseModel } from 'app/shared/models/base/base-model';
import { StorageService } from 'app/core/core-services/storage.service';
import { BaseRepository } from 'app/core/repositories/base-repository';
import { Observable } from 'rxjs';
export abstract class ListViewBaseComponent<V extends BaseViewModel, M extends BaseModel> extends BaseViewComponent
implements OnDestroy {
export abstract class ListViewBaseComponent<
V extends BaseViewModel,
M extends BaseModel,
R extends BaseRepository<V, M>
> extends BaseViewComponent implements OnDestroy {
/**
* The data source for a table. Requires to be initialized with a BaseViewModel
*/
@ -76,21 +81,24 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel, M extends B
}
/**
* Constructor for list view bases
* @param titleService the title serivce
* @param translate the translate service
* @param matSnackBar showing errors
* @param filterService filter
* @param sortService sorting
* @param viewModelRepo Repository for the view Model. Do NOT rename to "repo"
* @param route Access the current route
* @param storage Access the store
* @param modelFilterListService filter do NOT rename to "filterListService"
* @param modelSortService sorting do NOT rename to "sortService"
*/
public constructor(
titleService: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
protected viewModelRepo: R,
protected route?: ActivatedRoute,
protected storage?: StorageService,
public filterService?: BaseFilterListService<V>,
public sortService?: BaseSortListService<V>
protected modelFilterListService?: BaseFilterListService<V>,
protected modelSortService?: BaseSortListService<V>
) {
super(titleService, translate, matSnackBar);
this.selectedRows = [];
@ -114,40 +122,41 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel, M extends B
this.initializePagination();
this.dataSource.paginator._intl.itemsPerPageLabel = this.translate.instant('items per page');
}
if (this.filterService) {
this.onFilter();
}
if (this.sortService) {
this.onSort();
// TODO: Add subscription to this.subscriptions
if (this.modelFilterListService && this.modelSortService) {
// filtering and sorting
this.modelFilterListService.initFilters(this.getModelListObservable());
this.modelSortService.initSorting(this.modelFilterListService.outputObservable);
this.modelSortService.outputObservable.subscribe(data => this.setDataSource(data));
} else if (this.modelFilterListService) {
// only filter service
this.modelFilterListService.initFilters(this.getModelListObservable());
this.modelFilterListService.outputObservable.subscribe(data => this.setDataSource(data));
} else if (this.modelSortService) {
// only sorting
this.modelSortService.initSorting(this.getModelListObservable());
this.modelSortService.outputObservable.subscribe(data => this.setDataSource(data));
} else {
// none of both
this.getModelListObservable().subscribe(data => this.setDataSource(data));
}
}
/**
* Standard filtering function. Sufficient for most list views but can be overwritten
*/
protected onFilter(): void {
if (this.sortService) {
this.subscriptions.push(
this.filterService.filter().subscribe(filteredData => (this.sortService.data = filteredData))
);
} else {
this.filterService.filter().subscribe(filteredData => (this.dataSource.data = filteredData));
}
protected getModelListObservable(): Observable<V[]> {
return this.viewModelRepo.getViewModelListObservable();
}
/**
* Standard sorting function. Sufficient for most list views but can be overwritten
*/
protected onSort(): void {
this.subscriptions.push(
this.sortService.sort().subscribe(sortedData => {
// the dataArray needs to be cleared (since angular 7)
// changes are not detected properly anymore
this.dataSource.data = [];
this.dataSource.data = sortedData;
this.checkSelection();
})
);
private setDataSource(data: V[]): void {
// the dataArray needs to be cleared (since angular 7)
// changes are not detected properly anymore
this.dataSource.data = [];
this.dataSource.data = data;
this.checkSelection();
}
public onSortButton(itemProperty: string): void {

View File

@ -25,7 +25,8 @@ import { langToLocale } from 'app/shared/utils/lang-to-locale';
templateUrl: './history-list.component.html',
styleUrls: ['./history-list.component.scss']
})
export class HistoryListComponent extends ListViewBaseComponent<ViewHistory, History> implements OnInit {
export class HistoryListComponent extends ListViewBaseComponent<ViewHistory, History, HistoryRepositoryService>
implements OnInit {
/**
* Subject determine when the custom timestamp subject changes
*/
@ -51,7 +52,7 @@ export class HistoryListComponent extends ListViewBaseComponent<ViewHistory, His
private router: Router,
private operator: OperatorService
) {
super(titleService, translate, matSnackBar);
super(titleService, translate, matSnackBar, repo);
}
/**

View File

@ -26,7 +26,8 @@ import { StorageService } from 'app/core/core-services/storage.service';
templateUrl: './mediafile-list.component.html',
styleUrls: ['./mediafile-list.component.scss']
})
export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile, Mediafile> implements OnInit {
export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile, Mediafile, MediafileRepositoryService>
implements OnInit {
/**
* Holds the actions for logos. Updated via an observable
*/
@ -108,7 +109,7 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile,
public sortService: MediafilesSortListService,
private operator: OperatorService
) {
super(titleService, translate, matSnackBar, route, storage, filterService, sortService);
super(titleService, translate, matSnackBar, repo, route, storage, filterService, sortService);
// enables multiSelection for this listView
this.canMultiSelect = true;

View File

@ -1,65 +1,62 @@
import { Injectable } from '@angular/core';
import { BaseFilterListService, OsFilter } from 'app/core/ui-services/base-filter-list.service';
import { ViewMediafile } from '../models/view-mediafile';
import { StorageService } from 'app/core/core-services/storage.service';
import { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service';
import { OperatorService } from 'app/core/core-services/operator.service';
import { StorageService } from 'app/core/core-services/storage.service';
import { TranslateService } from '@ngx-translate/core';
import { ViewMediafile } from '../models/view-mediafile';
/**
* Filter service for media files
*/
@Injectable({
providedIn: 'root'
})
export class MediafileFilterListService extends BaseFilterListService<ViewMediafile> {
protected name = 'Mediafile';
/**
* A filter checking if a file is a pdf or not
*/
public pdfOption: OsFilter = {
property: 'type',
label: 'PDF',
options: [
{
condition: 'application/pdf',
label: this.translate.instant('Is PDF file')
},
{
condition: null,
label: this.translate.instant('Is no PDF file')
}
]
};
/**
* A filter checking if a file is hidden. Only included if the operator has permission to see hidden files
*/
public hiddenOptions: OsFilter = {
property: 'is_hidden',
label: this.translate.instant('Visibility'),
options: [
{ condition: true, label: this.translate.instant('is hidden') },
{ condition: false, label: this.translate.instant('is not hidden') }
]
};
/**
* Constructor. Sets the filter options according to permissions
* Constructor.
* Sets the filter options according to permissions
*
* @param store
* @param repo
* @param operator
* @param translate
*/
public constructor(
store: StorageService,
repo: MediafileRepositoryService,
operator: OperatorService,
private translate: TranslateService
) {
super(store, repo);
const filterOptions = operator.hasPerms('mediafiles.can_see_hidden')
? [this.hiddenOptions, this.pdfOption]
: [this.pdfOption];
this.updateFilterDefinitions(filterOptions);
public constructor(store: StorageService, private operator: OperatorService, private translate: TranslateService) {
super('Mediafiles', store);
this.operator.getUserObservable().subscribe(() => {
this.setFilterDefinitions();
});
}
/**
* @returns the filter definition
*/
protected getFilterDefinitions(): OsFilter[] {
const pdfOption: OsFilter = {
property: 'type',
label: 'PDF',
options: [
{
condition: 'application/pdf',
label: this.translate.instant('Is PDF file')
},
{
condition: null,
label: this.translate.instant('Is no PDF file')
}
]
};
const hiddenOptions: OsFilter = {
property: 'is_hidden',
label: this.translate.instant('Visibility'),
options: [
{ condition: true, label: this.translate.instant('is hidden') },
{ condition: false, label: this.translate.instant('is not hidden') }
]
};
return this.operator.hasPerms('mediafiles.can_see_hidden') ? [hiddenOptions, pdfOption] : [pdfOption];
}
}

View File

@ -1,26 +1,48 @@
import { Injectable } from '@angular/core';
import { BaseSortListService, OsSortingDefinition } from 'app/core/ui-services/base-sort-list.service';
import { BaseSortListService, OsSortingDefinition, OsSortingOption } from 'app/core/ui-services/base-sort-list.service';
import { StorageService } from 'app/core/core-services/storage.service';
import { TranslateService } from '@ngx-translate/core';
import { ViewMediafile } from '../models/view-mediafile';
/**
* Sorting service for the mediafile list
*/
@Injectable({
providedIn: 'root'
})
export class MediafilesSortListService extends BaseSortListService<ViewMediafile> {
public sortOptions: OsSortingDefinition<ViewMediafile> = {
sortProperty: 'title',
sortAscending: true,
options: [
{ property: 'title' },
{
property: 'type',
label: this.translate.instant('Type')
},
{
property: 'size',
label: this.translate.instant('Size')
}
]
};
protected name = 'Mediafile';
public sortOptions: OsSortingOption<ViewMediafile>[] = [
{ property: 'title' },
{
property: 'type',
label: this.translate.instant('Type')
},
{
property: 'size',
label: this.translate.instant('Size')
}
];
/**
* Constructor.
*
* @param translate required by parent
* @param store required by parent
*/
public constructor(translate: TranslateService, store: StorageService) {
super('Mediafiles', translate, store);
}
/**
* Required by parent
*
* @returns the default sorting strategy
*/
public async getDefaultDefinition(): Promise<OsSortingDefinition<ViewMediafile>> {
return {
sortProperty: 'title',
sortAscending: true
};
}
}

View File

@ -41,10 +41,10 @@
<span translate>Follow recommendations for all motions</span>
</button>
<table class="os-headed-listview-table on-transition-fade" mat-table [dataSource]="dataSource" matSort>
<table class="os-headed-listview-table on-transition-fade" mat-table [dataSource]="dataSource">
<!-- title column -->
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header> <span translate>Motion</span> </mat-header-cell>
<mat-header-cell *matHeaderCellDef> <span translate>Motion</span> </mat-header-cell>
<mat-cell *matCellDef="let motion">
{{ motion.getTitle() }}
</mat-cell>

View File

@ -15,6 +15,7 @@ import { PromptService } from 'app/core/ui-services/prompt.service';
import { ViewMotion } from 'app/site/motions/models/view-motion';
import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
import { StorageService } from 'app/core/core-services/storage.service';
import { Motion } from 'app/shared/models/motions/motion';
/**
* Detail component to display one motion block
@ -24,17 +25,13 @@ import { StorageService } from 'app/core/core-services/storage.service';
templateUrl: './motion-block-detail.component.html',
styleUrls: ['./motion-block-detail.component.scss']
})
export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion, MotionBlock> implements OnInit {
export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion, Motion, MotionRepositoryService>
implements OnInit {
/**
* Determines the block id from the given URL
*/
public block: ViewMotionBlock;
/**
* All motions in this block
*/
public motions: ViewMotion[];
/**
* Determine the edit mode
*/
@ -67,11 +64,11 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion
storage: StorageService,
private operator: OperatorService,
private router: Router,
private repo: MotionBlockRepositoryService,
private motionRepo: MotionRepositoryService,
protected repo: MotionBlockRepositoryService,
protected motionRepo: MotionRepositoryService,
private promptService: PromptService
) {
super(titleService, translate, matSnackBar, route, storage);
super(titleService, translate, matSnackBar, motionRepo, route, storage);
}
/**
@ -81,28 +78,29 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion
public ngOnInit(): void {
super.setTitle('Motion block');
this.initTable();
const blockId = parseInt(this.route.snapshot.params.id, 10);
this.blockEditForm = new FormGroup({
title: new FormControl('', Validators.required)
});
const blockId = +this.route.snapshot.params.id;
this.block = this.repo.getViewModel(blockId);
this.repo.getViewModelObservable(blockId).subscribe(newBlock => {
// necessary since the subscription can return undefined
if (newBlock) {
this.block = newBlock;
// set the blocks title in the form
this.blockEditForm.get('title').setValue(this.block.title);
this.repo.getViewMotionsByBlock(this.block.motionBlock).subscribe(newMotions => {
this.motions = newMotions;
this.dataSource.data = this.motions;
});
}
});
// pseudo filter
this.subscriptions.push(
this.repo.getViewModelObservable(blockId).subscribe(newBlock => {
if (newBlock) {
this.block = newBlock;
this.subscriptions.push(
this.repo.getViewMotionsByBlock(this.block.motionBlock).subscribe(viewMotions => {
if (viewMotions && viewMotions.length) {
this.dataSource.data = viewMotions;
} else {
this.dataSource.data = [];
}
})
);
}
})
);
}
/**
@ -193,8 +191,8 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion
* Following a recommendation implies, that a valid recommendation exists.
*/
public isFollowingProhibited(): boolean {
if (this.motions) {
return this.motions.every(motion => motion.isInFinalState() || !motion.recommendation_id);
if (this.dataSource.data) {
return this.dataSource.data.every(motion => motion.isInFinalState() || !motion.recommendation_id);
} else {
return false;
}

View File

@ -51,10 +51,10 @@
<!-- Table -->
<mat-card class="os-card">
<table class="os-headed-listview-table on-transition-fade" mat-table [dataSource]="dataSource" matSort>
<table class="os-headed-listview-table on-transition-fade" mat-table [dataSource]="dataSource">
<!-- Projector column -->
<ng-container matColumnDef="projector">
<mat-header-cell *matHeaderCellDef mat-sort-header>Projector</mat-header-cell>
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let block">
<os-projector-button [object]="block"></os-projector-button>
</mat-cell>
@ -62,13 +62,17 @@
<!-- title column -->
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header> <span translate>Title</span> </mat-header-cell>
<mat-header-cell *matHeaderCellDef>
<span translate>Title</span>
</mat-header-cell>
<mat-cell *matCellDef="let block"> {{ block.title }} </mat-cell>
</ng-container>
<!-- amount column -->
<ng-container matColumnDef="amount">
<mat-header-cell *matHeaderCellDef> <span translate>Motions</span> </mat-header-cell>
<mat-header-cell *matHeaderCellDef>
<span translate>Motions</span>
</mat-header-cell>
<mat-cell *matCellDef="let block">
<span class="os-amount-chip">{{ getMotionAmount(block.motionBlock) }}</span>
</mat-cell>
@ -76,7 +80,9 @@
<!-- menu -->
<ng-container matColumnDef="menu">
<mat-header-cell *matHeaderCellDef>Menu</mat-header-cell>
<mat-header-cell *matHeaderCellDef>
<span translate>Menu</span>
</mat-header-cell>
<mat-cell *matCellDef="let block">
<button
*ngIf="canEdit"

View File

@ -12,6 +12,7 @@ import { itemVisibilityChoices } from 'app/shared/models/agenda/item';
import { ListViewBaseComponent } from 'app/site/base/list-view-base';
import { MotionBlock } from 'app/shared/models/motions/motion-block';
import { MotionBlockRepositoryService } from 'app/core/repositories/motions/motion-block-repository.service';
import { MotionBlockSortService } from 'app/site/motions/services/motion-block-sort.service';
import { OperatorService } from 'app/core/core-services/operator.service';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { StorageService } from 'app/core/core-services/storage.service';
@ -26,7 +27,9 @@ import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
templateUrl: './motion-block-list.component.html',
styleUrls: ['./motion-block-list.component.scss']
})
export class MotionBlockListComponent extends ListViewBaseComponent<ViewMotionBlock, MotionBlock> implements OnInit {
export class MotionBlockListComponent
extends ListViewBaseComponent<ViewMotionBlock, MotionBlock, MotionBlockRepositoryService>
implements OnInit {
/**
* Holds the create form
*/
@ -88,9 +91,10 @@ export class MotionBlockListComponent extends ListViewBaseComponent<ViewMotionBl
private formBuilder: FormBuilder,
private promptService: PromptService,
private itemRepo: ItemRepositoryService,
private operator: OperatorService
private operator: OperatorService,
sortService: MotionBlockSortService
) {
super(titleService, translate, matSnackBar, route, storage);
super(titleService, translate, matSnackBar, repo, route, storage, null, sortService);
this.createBlockForm = this.formBuilder.group({
title: ['', Validators.required],
@ -105,14 +109,7 @@ export class MotionBlockListComponent extends ListViewBaseComponent<ViewMotionBl
public ngOnInit(): void {
super.setTitle('Motion blocks');
this.initTable();
this.items = this.itemRepo.getViewModelListBehaviorSubject();
this.repo.getViewModelListObservable().subscribe(newMotionblocks => {
newMotionblocks.sort((a, b) => (a > b ? 1 : -1));
this.dataSource.data = newMotionblocks;
});
this.agendaRepo.getDefaultAgendaVisibility().subscribe(visibility => (this.defaultVisibility = visibility));
}

View File

@ -66,7 +66,8 @@ interface InfoDialog {
templateUrl: './motion-list.component.html',
styleUrls: ['./motion-list.component.scss']
})
export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motion> implements OnInit {
export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motion, MotionRepositoryService>
implements OnInit {
/**
* Reference to the dialog for quick editing meta information.
*/
@ -130,15 +131,15 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
matSnackBar: MatSnackBar,
route: ActivatedRoute,
storage: StorageService,
filterService: MotionFilterListService,
sortService: MotionSortListService,
public filterService: MotionFilterListService,
public sortService: MotionSortListService,
private router: Router,
private configService: ConfigService,
private tagRepo: TagRepositoryService,
private motionBlockRepo: MotionBlockRepositoryService,
private categoryRepo: CategoryRepositoryService,
private workflowRepo: WorkflowRepositoryService,
private motionRepo: MotionRepositoryService,
protected motionRepo: MotionRepositoryService,
private motionCsvExport: MotionCsvExportService,
private operator: OperatorService,
private pdfExport: MotionPdfExportService,
@ -148,7 +149,7 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
public perms: LocalPermissionsService,
private motionXlsxExport: MotionXlsxExportService
) {
super(titleService, translate, matSnackBar, route, storage, filterService, sortService);
super(titleService, translate, matSnackBar, motionRepo, route, storage, filterService, sortService);
// enable multiSelect for this listView
this.canMultiSelect = true;
@ -357,23 +358,6 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
return false;
}
filter = filter ? filter.toLowerCase() : '';
if (
data.recommendation &&
this.translate
.instant(data.recommendation.recommendation_label)
.toLowerCase()
.includes(filter)
) {
return true;
}
if (
this.translate
.instant(data.state.name)
.toLowerCase()
.includes(filter)
) {
return true;
}
if (data.submitters.length && data.submitters.find(user => user.full_name.toLowerCase().includes(filter))) {
return true;
}
@ -387,6 +371,24 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
return true;
}
if (
this.getStateLabel(data) &&
this.getStateLabel(data)
.toLocaleLowerCase()
.includes(filter)
) {
return true;
}
if (
this.getRecommendationLabel(data) &&
this.getRecommendationLabel(data)
.toLocaleLowerCase()
.includes(filter)
) {
return true;
}
const dataid = '' + data.id;
if (dataid.includes(filter)) {
return true;

View File

@ -20,7 +20,8 @@ import { StorageService } from 'app/core/core-services/storage.service';
templateUrl: './workflow-list.component.html',
styleUrls: ['./workflow-list.component.scss']
})
export class WorkflowListComponent extends ListViewBaseComponent<ViewWorkflow, Workflow> implements OnInit {
export class WorkflowListComponent extends ListViewBaseComponent<ViewWorkflow, Workflow, WorkflowRepositoryService>
implements OnInit {
/**
* Holds the new workflow title
*/
@ -51,10 +52,10 @@ export class WorkflowListComponent extends ListViewBaseComponent<ViewWorkflow, W
storage: StorageService,
private dialog: MatDialog,
private router: Router,
private workflowRepo: WorkflowRepositoryService,
protected workflowRepo: WorkflowRepositoryService,
private promptService: PromptService
) {
super(titleService, translate, matSnackBar, route, storage);
super(titleService, translate, matSnackBar, workflowRepo, route, storage);
}
/**

View File

@ -0,0 +1,17 @@
import { TestBed } from '@angular/core/testing';
import { MotionBlockSortService } from './motion-block-sort.service';
import { E2EImportsModule } from 'e2e-imports.module';
describe('MotionBlockSortService', () => {
beforeEach(() =>
TestBed.configureTestingModule({
imports: [E2EImportsModule]
})
);
it('should be created', () => {
const service: MotionBlockSortService = TestBed.get(MotionBlockSortService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { BaseSortListService, OsSortingDefinition, OsSortingOption } from 'app/core/ui-services/base-sort-list.service';
import { StorageService } from 'app/core/core-services/storage.service';
import { TranslateService } from '@ngx-translate/core';
import { ViewMotionBlock } from '../models/view-motion-block';
@Injectable({
providedIn: 'root'
})
export class MotionBlockSortService extends BaseSortListService<ViewMotionBlock> {
public sortOptions: OsSortingOption<ViewMotionBlock>[] = [{ property: 'title' }];
public constructor(translate: TranslateService, store: StorageService) {
super('Motion block', translate, store);
}
protected async getDefaultDefinition(): Promise<OsSortingDefinition<ViewMotionBlock>> {
return {
sortProperty: 'title',
sortAscending: true
};
}
}

View File

@ -2,69 +2,52 @@ import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { BaseFilterListService, OsFilter, OsFilterOptions } from 'app/core/ui-services/base-filter-list.service';
import {
BaseFilterListService,
OsFilter,
OsFilterOptions,
OsFilterOption
} from 'app/core/ui-services/base-filter-list.service';
import { ViewMotion } from '../models/view-motion';
import { CategoryRepositoryService } from 'app/core/repositories/motions/category-repository.service';
import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-repository.service';
import { StorageService } from 'app/core/core-services/storage.service';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
import { MotionBlockRepositoryService } from 'app/core/repositories/motions/motion-block-repository.service';
import { MotionCommentSectionRepositoryService } from 'app/core/repositories/motions/motion-comment-section-repository.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { ViewWorkflow } from '../models/view-workflow';
import { OperatorService } from 'app/core/core-services/operator.service';
import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service';
import { ConfigService } from 'app/core/ui-services/config.service';
/**
* Filter description to easier parse dynamically occurring workflows
*/
interface WorkflowFilterDesc {
name: string;
filter: OsFilterOption[];
}
/**
* Filter the motion list
*/
@Injectable({
providedIn: 'root'
})
export class MotionFilterListService extends BaseFilterListService<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[] {
let filterOptions = [
this.flowFilterOptions,
this.categoryFilterOptions,
this.motionBlockFilterOptions,
this.recommendationFilterOptions,
this.motionCommentFilterOptions,
this.tagFilterOptions
];
if (!this.operator.isAnonymous) {
filterOptions = filterOptions.concat(this.personalNoteFilterOptions);
}
return filterOptions;
}
/**
* Filter definitions for the workflow filter. Options will be generated by
* getFilterOptions (as the workflows available may change)
*/
public flowFilterOptions: OsFilter = {
property: 'state',
label: 'State',
options: []
};
/**
* Listen to the configuration for change in defined/used workflows
*/
private enabledWorkflows = { statuteEnabled: false, statute: null, motion: null };
/**
* storage for currently used workflows
* Filter definitions for the workflow filter. Options will be generated by
* getFilterOptions (as the workflows available may change)
*/
private currentWorkflows: ViewWorkflow[];
public stateFilterOptions: OsFilter = {
property: 'state',
label: 'State',
options: []
};
/**
* Filter definitions for the category filter. Options will be generated by
* getFilterOptions (as the categories available may change)
*/
public categoryFilterOptions: OsFilter = {
property: 'category',
options: []
@ -98,7 +81,6 @@ export class MotionFilterListService extends BaseFilterListService<ViewMotion> {
{
property: 'star',
label: this.translate.instant('Favorites'),
isActive: false,
options: [
{
condition: true,
@ -113,7 +95,6 @@ export class MotionFilterListService extends BaseFilterListService<ViewMotion> {
{
property: 'hasNotes',
label: this.translate.instant('Personal notes'),
isActive: false,
options: [
{
condition: true,
@ -132,216 +113,175 @@ export class MotionFilterListService extends BaseFilterListService<ViewMotion> {
* the available filters
*
* @param store The browser's storage; required for fetching filters from any previous sessions
* @param workflowRepo Subscribing to filters by states/Recommendation
* @param categoryRepo Subscribing to filters by Categories
* @param motionBlockRepo Subscribing to filters by MotionBlock
* @param commentRepo subycribing filter by presense of comment
* @param categoryRepo to filter by Categories
* @param motionBlockRepo to filter by MotionBlock
* @param commentRepo to filter by motion comments
* @param tagRepo to filter by tags
* @param workflowRepo Subscribing to filters by states and recommendation
* @param translate Translation service
* @param config the current configuration (to determine which workflow filters to offer )
* @param motionRepo the motion's own repository, required by the parent
* @param operator
*/
public constructor(
store: StorageService,
categoryRepo: CategoryRepositoryService,
motionBlockRepo: MotionBlockRepositoryService,
commentRepo: MotionCommentSectionRepositoryService,
tagRepo: TagRepositoryService,
private workflowRepo: WorkflowRepositoryService,
private categoryRepo: CategoryRepositoryService,
private motionBlockRepo: MotionBlockRepositoryService,
private commentRepo: MotionCommentSectionRepositoryService,
private translate: TranslateService,
private config: ConfigService,
motionRepo: MotionRepositoryService,
private operator: OperatorService,
private tagRepo: TagRepositoryService
private config: ConfigService
) {
super(store, motionRepo);
super('Motion', store);
this.getWorkflowConfig();
this.updateFilterForRepo(categoryRepo, this.categoryFilterOptions, this.translate.instant('No category set'));
this.updateFilterForRepo(
motionBlockRepo,
this.motionBlockFilterOptions,
this.translate.instant('No motion block set')
);
this.updateFilterForRepo(commentRepo, this.motionCommentFilterOptions, this.translate.instant('No comment'));
this.updateFilterForRepo(tagRepo, this.tagFilterOptions, this.translate.instant('No tags'));
this.subscribeWorkflows();
this.subscribeCategories();
this.subscribeMotionBlocks();
this.subscribeComments();
this.subscribeTags();
this.operator.getUserObservable().subscribe(() => {
this.updateFilterDefinitions(this.filterOptions);
this.setFilterDefinitions();
});
}
private getWorkflowConfig(): void {
this.config.get<string>('motions_statute_amendments_workflow').subscribe(id => {
this.enabledWorkflows.statute = +id;
});
this.config.get<string>('motions_workflow').subscribe(id => {
this.enabledWorkflows.motion = +id;
});
this.config.get<boolean>('motions_statutes_enabled').subscribe(bool => {
this.enabledWorkflows.statuteEnabled = bool;
});
}
/**
* Subscibes to changing MotionBlocks, and updates the filter accordingly
* @returns the filter definition
*/
private subscribeMotionBlocks(): void {
this.motionBlockRepo.getViewModelListObservable().subscribe(motionBlocks => {
const motionBlockOptions: OsFilterOptions = motionBlocks.map(mb => ({
condition: mb.id,
label: mb.title,
isActive: false
}));
if (motionBlocks.length) {
motionBlockOptions.push('-');
motionBlockOptions.push({
condition: null,
label: this.translate.instant('No motion block set'),
isActive: false
});
}
this.motionBlockFilterOptions.options = motionBlockOptions;
this.updateFilterDefinitions(this.filterOptions);
});
protected getFilterDefinitions(): OsFilter[] {
let filterDefinitions = [
this.stateFilterOptions,
this.categoryFilterOptions,
this.motionBlockFilterOptions,
this.recommendationFilterOptions,
this.motionCommentFilterOptions,
this.tagFilterOptions
];
if (!this.operator.isAnonymous) {
filterDefinitions = filterDefinitions.concat(this.personalNoteFilterOptions);
}
return filterDefinitions;
}
/**
* Subscibes to changing Categories, and updates the filter accordingly
*/
private subscribeCategories(): void {
this.categoryRepo.getViewModelListObservable().subscribe(categories => {
const categoryOptions: OsFilterOptions = categories.map(cat => ({
condition: cat.id,
label: cat.prefixedName,
isActive: false
}));
if (categories.length) {
categoryOptions.push('-');
categoryOptions.push({
label: this.translate.instant('No category set'),
condition: null
});
}
this.categoryFilterOptions.options = categoryOptions;
this.updateFilterDefinitions(this.filterOptions);
});
}
/**
* Subscibes to changing Workflows, and updates the state and recommendation filters accordingly
* Only subscribes to workflows that are enabled in the config as motion or statute paragraph workflow
* Subscribes to changing Workflows, and updates the state and recommendation filters accordingly.
*/
private subscribeWorkflows(): void {
this.workflowRepo.getViewModelListObservable().subscribe(workflows => {
this.currentWorkflows = workflows;
this.updateWorkflows();
});
this.config.get<string>('motions_statute_amendments_workflow').subscribe(id => {
this.enabledWorkflows.statute = +id;
this.updateWorkflows();
});
this.config.get<string>('motions_workflow').subscribe(id => {
this.enabledWorkflows.motion = +id;
this.updateWorkflows();
});
this.config.get<boolean>('motions_statutes_enabled').subscribe(bool => {
this.enabledWorkflows.statuteEnabled = bool;
this.updateWorkflows();
});
}
if (workflows && workflows.length) {
const workflowFilters: WorkflowFilterDesc[] = [];
const recoFilters: WorkflowFilterDesc[] = [];
/**
* Helper to show only filter for workflows that are included in to currently
* set config options
*/
private updateWorkflows(): void {
const workflowOptions: OsFilterOptions = [];
const finalStates: number[] = [];
const nonFinalStates: number[] = [];
const recommendationOptions: OsFilterOptions = [];
if (!this.currentWorkflows) {
return;
}
this.currentWorkflows.forEach(workflow => {
if (
workflow.id === this.enabledWorkflows.motion ||
(this.enabledWorkflows.statuteEnabled && workflow.id === this.enabledWorkflows.statute)
) {
workflowOptions.push(workflow.name);
recommendationOptions.push(workflow.name);
workflow.states.forEach(state => {
// filter out restricted states for unpriviledged users
if (
this.operator.hasPerms('motions.can_manage', 'motions.can_manage_metadata') ||
state.restriction.length === 0
) {
if (state.isFinalState) {
finalStates.push(state.id);
} else {
nonFinalStates.push(state.id);
const finalStates: number[] = [];
const nonFinalStates: number[] = [];
// get all relevant information
for (const workflow of workflows) {
if (this.isWorkflowEnabled(workflow.id)) {
workflowFilters.push({
name: workflow.name,
filter: []
});
recoFilters.push({
name: workflow.name,
filter: []
});
for (const state of workflow.states) {
if (
this.operator.hasPerms('motions.can_manage', 'motions.can_manage_metadata') &&
state.restriction
) {
// sort final and non final states
state.isFinalState ? finalStates.push(state.id) : nonFinalStates.push(state.id);
workflowFilters[workflowFilters.length - 1].filter.push({
condition: state.id,
label: state.name
});
if (state.recommendation_label) {
recoFilters[workflowFilters.length - 1].filter.push({
condition: state.id,
label: state.recommendation_label
});
}
}
}
workflowOptions.push({
condition: state.id,
label: state.name,
isActive: false
});
}
if (state.recommendation_label) {
recommendationOptions.push({
condition: state.id,
label: state.recommendation_label,
isActive: false
});
}
});
}
});
if (workflowOptions.length) {
workflowOptions.push('-');
workflowOptions.push({
label: 'Done',
condition: finalStates
});
workflowOptions.push({
label: this.translate.instant('Undone'),
condition: nonFinalStates
});
}
if (recommendationOptions.length) {
recommendationOptions.push('-');
recommendationOptions.push({
label: this.translate.instant('No recommendation'),
condition: null
});
}
this.flowFilterOptions.options = workflowOptions;
this.recommendationFilterOptions.options = recommendationOptions;
this.updateFilterDefinitions(this.filterOptions);
}
}
/**
* Subscibes to changing Comments, and updates the filter accordingly
*/
private subscribeComments(): void {
this.commentRepo.getViewModelListObservable().subscribe(comments => {
const commentOptions: OsFilterOptions = comments.map(comment => ({
condition: comment.id,
label: comment.name,
isActive: false
}));
if (comments.length) {
commentOptions.push('-');
commentOptions.push({
label: this.translate.instant('No comment'),
condition: null
});
// convert to filter options
if (workflowFilters && workflowFilters.length) {
let workflowOptions: OsFilterOptions = [];
for (const filterDef of workflowFilters) {
workflowOptions.push(filterDef.name);
workflowOptions = workflowOptions.concat(filterDef.filter);
}
// add "done" and "undone"
workflowOptions.push('-');
workflowOptions.push({
label: 'Done',
condition: finalStates
});
workflowOptions.push({
label: this.translate.instant('Undone'),
condition: nonFinalStates
});
this.stateFilterOptions.options = workflowOptions;
}
if (recoFilters && recoFilters.length) {
let recoOptions: OsFilterOptions = [];
for (const filterDef of recoFilters) {
recoOptions.push(filterDef.name);
recoOptions = recoOptions.concat(filterDef.filter);
}
recoOptions.push('-');
recoOptions.push({
label: this.translate.instant('No recommendation'),
condition: null
});
this.recommendationFilterOptions.options = recoOptions;
}
this.setFilterDefinitions();
}
this.motionCommentFilterOptions.options = commentOptions;
this.updateFilterDefinitions(this.filterOptions);
});
}
/**
* Subscibes to changing Tags, and updates the filter accordingly
*/
private subscribeTags(): void {
this.tagRepo.getViewModelListObservable().subscribe(tags => {
const tagOptions: OsFilterOptions = tags.map(tag => ({
condition: tag.id,
label: tag.name,
isActive: false
}));
if (tags.length) {
tagOptions.push('-');
tagOptions.push({
label: this.translate.instant('No tags'),
condition: null
});
}
this.tagFilterOptions.options = tagOptions;
this.updateFilterDefinitions(this.filterOptions);
});
private isWorkflowEnabled(workflowId: number): boolean {
return (
workflowId === this.enabledWorkflows.motion ||
(this.enabledWorkflows.statuteEnabled && workflowId === this.enabledWorkflows.statute)
);
}
}

View File

@ -1,42 +1,74 @@
import { Injectable } from '@angular/core';
import { BaseSortListService, OsSortingDefinition } from 'app/core/ui-services/base-sort-list.service';
import { ViewMotion } from '../models/view-motion';
import { TranslateService } from '@ngx-translate/core';
import { StorageService } from 'app/core/core-services/storage.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { _ } from 'app/core/translate/translation-marker';
import { _ } from 'app/core/translate/translation-marker';
import { BaseSortListService, OsSortingDefinition, OsSortingOption } from 'app/core/ui-services/base-sort-list.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { Deferred } from 'app/core/deferred';
import { StorageService } from 'app/core/core-services/storage.service';
import { ViewMotion } from '../models/view-motion';
/**
* Sorting service for the motion list
*/
@Injectable({
providedIn: 'root'
})
export class MotionSortListService extends BaseSortListService<ViewMotion> {
public sortOptions: OsSortingDefinition<ViewMotion> = {
sortProperty: 'weight',
sortAscending: true,
options: [
{ property: 'weight', label: 'Call list' },
{ property: 'identifier' },
{ property: 'title' },
{ property: 'submitters' },
{ property: 'category' },
{ property: 'motion_block_id', label: 'Motion block' },
{ property: 'state' },
{ property: 'creationDate', label: _('Creation date') },
{ property: 'lastChangeDate', label: _('Last modified') }
]
};
protected name = 'Motion';
/**
* Hold the default motion sorting
*/
private defaultMotionSorting: string;
/**
* Constructor. Sets the default sorting if none is set locally
*
* @param translate
* @param store
* @param config
* To wait until the default motion was loaded once
*/
public constructor(translate: TranslateService, store: StorageService, config: ConfigService) {
super(translate, store);
this.defaultSorting = config.instant<keyof ViewMotion>('motions_motions_sorting');
private readonly defaultSortingLoaded: Deferred<void> = new Deferred();
/**
* Define the sort options
*/
public sortOptions: OsSortingOption<ViewMotion>[] = [
{ property: 'weight', label: 'Call list' },
{ property: 'identifier' },
{ property: 'title' },
{ property: 'submitters' },
{ property: 'category' },
{ property: 'motion_block_id', label: 'Motion block' },
{ property: 'state' },
{ property: 'creationDate', label: _('Creation date') },
{ property: 'lastChangeDate', label: _('Last modified') }
];
/**
* Constructor.
*
* @param translate required by parent
* @param store required by parent
* @param config set the default sorting according to OpenSlides configuration
*/
public constructor(translate: TranslateService, store: StorageService, private config: ConfigService) {
super('Motion', translate, store);
this.config.get<string>('motions_motions_sorting').subscribe(defSortProp => {
if (defSortProp) {
this.defaultMotionSorting = defSortProp;
this.defaultSortingLoaded.resolve();
}
});
}
/**
* Required by parent
*
* @returns the default sorting strategy
*/
protected async getDefaultDefinition(): Promise<OsSortingDefinition<ViewMotion>> {
await this.defaultSortingLoaded;
return {
sortProperty: this.defaultMotionSorting as keyof ViewMotion,
sortAscending: true
};
}
}

View File

@ -25,7 +25,7 @@ import { ViewTag } from '../../models/view-tag';
templateUrl: './tag-list.component.html',
styleUrls: ['./tag-list.component.css']
})
export class TagListComponent extends ListViewBaseComponent<ViewTag, Tag> implements OnInit {
export class TagListComponent extends ListViewBaseComponent<ViewTag, Tag, TagRepositoryService> implements OnInit {
public editTag = false;
public newTag = false;
public selectedTag: ViewTag;
@ -50,7 +50,7 @@ export class TagListComponent extends ListViewBaseComponent<ViewTag, Tag> implem
private repo: TagRepositoryService,
private promptService: PromptService
) {
super(titleService, translate, matSnackBar, route, storage);
super(titleService, translate, matSnackBar, repo, route, storage);
}
/**

View File

@ -55,14 +55,13 @@ interface InfoDialog {
/**
* Component for the user list view.
*
*/
@Component({
selector: 'os-user-list',
templateUrl: './user-list.component.html',
styleUrls: ['./user-list.component.scss']
})
export class UserListComponent extends ListViewBaseComponent<ViewUser, User> implements OnInit {
export class UserListComponent extends ListViewBaseComponent<ViewUser, User, UserRepositoryService> implements OnInit {
/**
* The reference to the template.
*/
@ -154,7 +153,7 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser, User> imp
private userPdf: UserPdfExportService,
private dialog: MatDialog
) {
super(titleService, translate, matSnackBar, route, storage, filterService, sortService);
super(titleService, translate, matSnackBar, repo, route, storage, filterService, sortService);
// enable multiSelect for this listView
this.canMultiSelect = true;

View File

@ -1,107 +1,75 @@
import { Injectable } from '@angular/core';
import { BaseFilterListService, OsFilter } from 'app/core/ui-services/base-filter-list.service';
import { StorageService } from 'app/core/core-services/storage.service';
import { ViewUser } from '../models/view-user';
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
import { StorageService } from 'app/core/core-services/storage.service';
import { TranslateService } from '@ngx-translate/core';
import { ViewUser } from '../models/view-user';
/**
* Filter the user list
*/
@Injectable({
providedIn: 'root'
})
export class UserFilterListService extends BaseFilterListService<ViewUser> {
protected name = 'User';
private userGroupFilterOptions = {
isActive: false,
private userGroupFilterOptions: OsFilter = {
property: 'groups_id',
label: 'Groups',
options: []
};
public staticFilterOptions = [
{
property: 'is_present',
label: 'Presence',
isActive: false,
options: [
{ condition: true, label: this.translate.instant('Is present') },
{ condition: false, label: this.translate.instant('Is not present') }
]
},
{
property: 'is_active',
label: this.translate.instant('Active'),
isActive: false,
options: [
{ condition: true, label: 'Is active' },
{ condition: false, label: this.translate.instant('Is not active') }
]
},
{
property: 'is_committee',
label: this.translate.instant('Committee'),
isActive: false,
options: [
{ condition: true, label: 'Is a committee' },
{ condition: false, label: this.translate.instant('Is not a committee') }
]
},
{
property: 'is_last_email_send',
label: this.translate.instant('Last email send'),
isActive: false,
options: [
{ condition: true, label: this.translate.instant('Got an email') },
{ condition: false, label: this.translate.instant("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);
}
/**
* Contructor. Subscribes to incoming group definitions.
* Constructor.
* Subscribes to incoming group definitions.
*
* @param store
* @param groupRepo
* @param repo
* @param groupRepo to filter by groups
* @param translate marking some translations that are unique here
*
*/
public constructor(
store: StorageService,
private groupRepo: GroupRepositoryService,
repo: UserRepositoryService,
private translate: TranslateService
) {
super(store, repo);
this.subscribeGroups();
public constructor(store: StorageService, groupRepo: GroupRepositoryService, private translate: TranslateService) {
super('User', store);
this.updateFilterForRepo(groupRepo, this.userGroupFilterOptions, this.translate.instant('Default'), [1]);
}
/**
* Updates the filter according to existing groups.
* TODO: Users with only the 'standard' group set appear in the model as items without groups_id. 'Standard' filter is broken
* @returns the filter definition
*/
public subscribeGroups(): void {
this.groupRepo.getViewModelListObservable().subscribe(groups => {
const groupOptions = [];
groups.forEach(group => {
groupOptions.push({
condition: group.id,
label: group.name,
isActive: false
});
});
this.userGroupFilterOptions.options = groupOptions;
this.updateFilterDefinitions(this.filterOptions);
});
protected getFilterDefinitions(): OsFilter[] {
const staticFilterOptions: OsFilter[] = [
{
property: 'is_present',
label: 'Presence',
options: [
{ condition: true, label: this.translate.instant('Is present') },
{ condition: false, label: this.translate.instant('Is not present') }
]
},
{
property: 'is_active',
label: this.translate.instant('Active'),
options: [
{ condition: true, label: 'Is active' },
{ condition: false, label: this.translate.instant('Is not active') }
]
},
{
property: 'is_committee',
label: this.translate.instant('Committee'),
options: [
{ condition: true, label: 'Is a committee' },
{ condition: false, label: this.translate.instant('Is not a committee') }
]
},
{
property: 'is_last_email_send',
label: this.translate.instant('Last email send'),
options: [
{ condition: true, label: this.translate.instant('Got an email') },
{ condition: false, label: this.translate.instant("Didn't get an email") }
]
}
];
return staticFilterOptions.concat(this.userGroupFilterOptions);
}
}

View File

@ -1,40 +1,52 @@
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { BaseSortListService, OsSortingDefinition } from 'app/core/ui-services/base-sort-list.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { BaseSortListService, OsSortingDefinition, OsSortingOption } from 'app/core/ui-services/base-sort-list.service';
import { StorageService } from 'app/core/core-services/storage.service';
import { ViewUser } from '../models/view-user';
/**
* Sorting service for the user list
*/
@Injectable({
providedIn: 'root'
})
export class UserSortListService extends BaseSortListService<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: 'number', label: 'Participant number' },
{ property: 'structure_level', label: 'Structure level' },
{ property: 'comment' }
]
};
protected name = 'User';
/**
* Define the sort options
*/
public sortOptions: OsSortingOption<ViewUser>[] = [
{ 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: 'number', label: 'Participant number' },
{ property: 'structure_level', label: 'Structure level' },
{ property: 'comment' }
// TODO email send?
];
/**
* Constructor. Sets the default sorting if none is set locally
* Constructor.
*
* @param translate
* @param store
* @param config
* @param translate required by parent
* @param store requires by parent
*/
public constructor(translate: TranslateService, store: StorageService, config: ConfigService) {
super(translate, store);
this.defaultSorting = config.instant<keyof ViewUser>('users_sort_by');
public constructor(translate: TranslateService, store: StorageService) {
super('User', translate, store);
}
/**
* Required by parent
*
* @returns the default sorting strategy
*/
public async getDefaultDefinition(): Promise<OsSortingDefinition<ViewUser>> {
return {
sortProperty: 'first_name',
sortAscending: true
};
}
}

View File

@ -197,7 +197,7 @@ def get_config_variables():
input_type="choice",
label="Sort motions by",
choices=(
{"value": "callListWeight", "display_name": "Call list"},
{"value": "weight", "display_name": "Call list"},
{"value": "identifier", "display_name": "Identifier"},
),
weight=335,