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) { if (this.currentUpdateSlot) {
const request = new Deferred(); const request = new Deferred();
this.updateSlotRequests.push(request); this.updateSlotRequests.push(request);
await request.promise; await request;
} }
this.currentUpdateSlot = new UpdateSlot(DS); this.currentUpdateSlot = new UpdateSlot(DS);
return this.currentUpdateSlot; return this.currentUpdateSlot;

View File

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

View File

@ -51,7 +51,7 @@ export class PingService {
isStable.resolve(); 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. // Connects the ping-pong mechanism to the opening and closing of the connection.
this.websocketService.closeEvent.subscribe(() => this.stopPing()); 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 * The promise to wait for
*/ */
@ -28,9 +28,11 @@ export class Deferred<T = void> {
* Creates the promise and overloads the resolve function * Creates the promise and overloads the resolve function
*/ */
public constructor() { public constructor() {
this.promise = new Promise<T>(resolve => { let preResolve: (val?: T) => void;
this.resolve = resolve; super(resolve => {
preResolve = resolve;
}); });
this._resolve = preResolve;
} }
/** /**

View File

@ -1,19 +1,16 @@
import { auditTime } from 'rxjs/operators'; import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { BehaviorSubject, Observable } from 'rxjs';
import { BaseRepository } from 'app/core/repositories/base-repository'; import { BaseModel } from 'app/shared/models/base/base-model';
import { BaseModel } from '../../shared/models/base/base-model'; import { BaseRepository } from '../repositories/base-repository';
import { BaseViewModel } from '../../site/base/base-view-model'; import { BaseViewModel } from '../../site/base/base-view-model';
import { StorageService } from '../core-services/storage.service'; import { StorageService } from '../core-services/storage.service';
/** /**
* Describes the available filters for a listView. * 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 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 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 options a list of available options for a filter
* @param count
*/ */
export interface OsFilter { export interface OsFilter {
property: string; property: string;
@ -37,248 +34,296 @@ export type OsFilterOptions = (OsFilterOption | string)[];
*/ */
export interface OsFilterOption { export interface OsFilterOption {
label: string; label: string;
condition: string | boolean | number | number[]; condition: OsFilterOptionCondition;
isActive?: boolean; 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) * Filter for the list view. List views can subscribe to its' dataService (providing filter definitions)
* and will receive their filtered data as observable * and will receive their filtered data as observable
*/ */
export abstract class BaseFilterListService<V extends BaseViewModel> { export abstract class BaseFilterListService<V extends BaseViewModel> {
/** /**
* stores the currently used raw data to be used for the filter * 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. * The currently used filters.
*/ */
public filterDefinitions: OsFilter[]; 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 * @returns the total count of items before the filter
*/ */
public get totalCount(): number { public get unfilteredCount(): number {
return this.currentRawData ? this.currentRawData.length : 0; 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 * @returns the amount of items that pass the filter service's filters
*/ */
public get filteredCount(): number { 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 the amount of currently active filters
*
* @returns a number of filters
*/ */
public get activeFilterCount(): number { public get activeFilterCount(): number {
if (!this.filterDefinitions || !this.filterDefinitions.length) { return this.filterDefinitions ? this.filterDefinitions.filter(filter => filter.count).length : 0;
return 0;
}
let filters = 0;
for (const filter of this.filterDefinitions) {
if (filter.count) {
filters += 1;
}
}
return filters;
} }
/** /**
* 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) * @returns true if there are defined filters (regardless of current state)
*/ */
public get hasFilterOptions(): boolean { public get hasFilterOptions(): boolean {
return this.filterDefinitions && this.filterDefinitions.length ? true : false; return !!this.filterDefinitions && this.filterDefinitions.length > 0;
} }
/** /**
* Constructor. * 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[]> { public async initFilters(inputData: Observable<V[]>): Promise<void> {
this.repo const storedFilter = await this.store.get<OsFilter[]>('filter_' + this.name);
.getViewModelListObservable()
.pipe(auditTime(10)) if (storedFilter) {
.subscribe(data => { this.filterDefinitions = storedFilter;
this.currentRawData = data; } else {
this.filteredData = this.filterData(data); this.filterDefinitions = this.getFilterDefinitions();
this.filterDataOutput.next(this.filteredData); 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 * Apply a newly created filter
* @param filter *
* @param filterProperty new filter as string
* @param option filter option
*/ */
public addFilterOption(filterName: string, option: OsFilterOption): void { protected addFilterOption(filterProperty: string, option: OsFilterOption): void {
const filter = this.filterDefinitions.find(f => f.property === filterName); const filter = this.filterDefinitions.find(f => f.property === filterProperty);
if (filter) { if (filter) {
const filterOption = filter.options.find( const filterOption = filter.options.find(
o => typeof o !== 'string' && o.condition === option.condition o => typeof o !== 'string' && o.condition === option.condition
) as OsFilterOption; ) as OsFilterOption;
if (filterOption && !filterOption.isActive) { if (filterOption && !filterOption.isActive) {
filterOption.isActive = true; 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. * Remove a filter option.
* *
* @param filterName: The property name of this filter * @param filterName The property name of this filter
* @param option: The option to disable * @param option The option to disable
*/ */
public removeFilterOption(filterName: string, option: OsFilterOption): void { protected removeFilterOption(filterProperty: string, option: OsFilterOption): void {
const filter = this.filterDefinitions.find(f => f.property === filterName); const filter = this.filterDefinitions.find(f => f.property === filterProperty);
if (filter) { if (filter) {
const filterOption = filter.options.find( const filterOption = filter.options.find(
o => typeof o !== 'string' && o.condition === option.condition o => typeof o !== 'string' && o.condition === option.condition
) as OsFilterOption; ) as OsFilterOption;
if (filterOption && filterOption.isActive) { if (filterOption && filterOption.isActive) {
filterOption.isActive = false; filterOption.isActive = false;
filter.count -= 1; if (filter.count) {
this.filteredData = this.filterData(this.currentRawData); filter.count -= 1;
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 (!excluded) { }
filteredData.push(newItem);
}
});
return filteredData;
} }
/** /**
* Checks if a given ViewBaseModel passes the filter. * Checks if a given ViewBaseModel passes the filter.
* *
* @param item * @param item Usually a view model
* @param filter * @param filter The filter to check
* @returns true if the item is to be dispalyed according to the filter * @returns true if the item is to be displayed according to the filter
*/ */
private checkIncluded(item: V, filter: OsFilter): boolean { private checkIncluded(item: V, filter: OsFilter): boolean {
const nullFilter = filter.options.find( 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 * Removes all active options of a given filter, clearing it
*
* @param filter * @param filter
* @param update
*/ */
public clearFilter(filter: OsFilter): void { public clearFilter(filter: OsFilter, update: boolean = true): void {
filter.options.forEach(option => { filter.options.forEach(option => {
if (typeof option === 'object' && option.isActive) { if (typeof option === 'object' && option.isActive) {
this.removeFilterOption(filter.property, option); this.removeFilterOption(filter.property, option);
} }
}); });
if (update) {
this.storeActiveFilters();
}
} }
/** /**
* Removes all filters currently in use from this filterService * Removes all filters currently in use from this filterService
*/ */
public clearAllFilters(): void { public clearAllFilters(): void {
this.filterDefinitions.forEach(filter => { if (this.filterDefinitions && this.filterDefinitions.length) {
this.clearFilter(filter); this.filterDefinitions.forEach(filter => {
}); this.clearFilter(filter, false);
});
this.storeActiveFilters();
}
} }
} }

View File

@ -1,155 +1,191 @@
import { Injectable } from '@angular/core'; import { BehaviorSubject, Subscription, Observable } from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import { BaseViewModel } from '../../site/base/base-view-model';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { BaseViewModel } from '../../site/base/base-view-model';
import { StorageService } from '../core-services/storage.service'; import { StorageService } from '../core-services/storage.service';
/** /**
* Describes the sorting columns of an associated ListView, and their state. * Describes the sorting columns of an associated ListView, and their state.
*/ */
export interface OsSortingDefinition<V> { export interface OsSortingDefinition<V> {
sortProperty: keyof V; sortProperty: keyof V;
sortAscending?: boolean; sortAscending: boolean;
options: OsSortingItem<V>[];
} }
/** /**
* A sorting property (data may be a string, a number, a function, or an object * 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} * 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; property: keyof V;
label?: string; label?: string;
} }
@Injectable({ /**
providedIn: 'root' * Base class for generic sorting purposes
}) */
export abstract class BaseSortListService<V extends BaseViewModel> { 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} * 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 * 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. * The sorting function according to current settings.
*/
protected name: string;
/**
* The sorting function according to current settings. Set via {@link updateSortFn}.
*/ */
private sortFn: (a: V, b: V) => number; 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 * Set the current sorting order
*
* @param ascending ascending sorting if true, descending sorting if false
*/ */
public set ascending(ascending: boolean) { public set ascending(ascending: boolean) {
this.sortOptions.sortAscending = ascending; this.sortDefinition.sortAscending = ascending;
this.updateSortFn(); this.updateSortDefinitions();
this.saveStorageDefinition();
this.doAsyncSorting();
} }
/** /**
* get the current sorting order * @param returns wether current the sorting is ascending or descending
*/ */
public get ascending(): boolean { public get ascending(): boolean {
return this.sortOptions.sortAscending; return this.sortDefinition.sortAscending;
} }
/** /**
* set the property of the viewModel the sorting will be based on. * 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, * If the property stays the same, only the sort direction will be toggled,
* new sortProperty will result in an ascending order. * new sortProperty will result in an ascending order.
*
* @param property a part of a view model
*/ */
public set sortProperty(property: string) { public set sortProperty(property: keyof V) {
if (this.sortOptions.sortProperty === (property as keyof V)) { if (this.sortDefinition.sortProperty === property) {
this.ascending = !this.ascending; this.ascending = !this.ascending;
this.updateSortFn();
} else { } else {
this.sortOptions.sortProperty = property as keyof V; this.sortDefinition.sortProperty = property;
this.sortOptions.sortAscending = true; this.sortDefinition.sortAscending = true;
this.updateSortFn();
this.doAsyncSorting();
} }
this.saveStorageDefinition(); this.updateSortDefinitions();
} }
/** /**
* get the property of the viewModel the sorting is based on. * @returns the current sorting property
*/ */
public get sortProperty(): string { public get sortProperty(): keyof V {
return this.sortOptions ? (this.sortOptions.sortProperty as string) : ''; return this.sortDefinition.sortProperty;
} }
/**
* @returns wether sorting is active or not
*/
public get isActive(): boolean { 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 * 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 { public setSorting(property: keyof V, ascending: boolean): void {
this.sortOptions.sortProperty = property as keyof V; this.sortDefinition.sortProperty = property;
this.sortOptions.sortAscending = ascending; this.sortDefinition.sortAscending = ascending;
this.saveStorageDefinition(); this.updateSortDefinitions();
this.updateSortFn();
this.doAsyncSorting();
} }
/** /**
* Retrieves the currently active icon for an option. * Retrieves the currently active icon for an option.
*
* @param option * @param option
* @returns the name of the sorting icon, fit to material icon ligatures
*/ */
public getSortIcon(option: OsSortingItem<V>): string { public getSortIcon(option: OsSortingOption<V>): string {
if (!this.sortProperty || this.sortProperty !== (option.property as string)) { if (this.sortProperty !== option.property) {
return ''; return '';
} }
return this.ascending ? 'arrow_downward' : 'arrow_upward'; 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) { if (option.label) {
return 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 * Saves the current sorting definitions to the local store
* store
*/ */
private async loadStorageDefinition(): Promise<void> { private updateSortDefinitions(): void {
const sorting: OsSortingDefinition<V> | null = await this.store.get('sorting_' + this.name); this.updateSortedData();
if (sorting) { this.store.set('sorting_' + this.name, this.sortDefinition);
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);
});
} }
/** /**
* Sorts an array of data synchronously, using the currently configured sorting * 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 * @returns the data, sorted with the definitions of this service
*/ */
public sortSync(data: V[]): V[] { 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 * Recreates the sorting function. Is supposed to be called on init and
* every time the sorting (property, ascending/descending) or the language changes * every time the sorting (property, ascending/descending) or the language changes
*/ */
protected updateSortFn(): void { protected updateSortedData(): void {
const property = this.sortProperty as string; if (this.inputData) {
const ascending = this.ascending; const property = this.sortProperty as string;
const intl = new Intl.Collator(this.translate.currentLang); // TODO: observe and update sorting on language change const intl = new Intl.Collator(this.translate.currentLang);
this.outputSubject.next(
this.sortFn = function(itemA: V, itemB: V): number { this.inputData.sort((itemA, itemB) => {
const firstProperty = ascending ? itemA[property] : itemB[property]; const firstProperty = this.ascending ? itemA[property] : itemB[property];
const secondProperty = ascending ? itemB[property] : itemA[property]; const secondProperty = this.ascending ? itemB[property] : itemA[property];
if (typeof firstProperty !== typeof secondProperty) { if (typeof firstProperty !== typeof secondProperty) {
// undefined/null items should always land at the end // 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':
if (!firstProperty) { if (!firstProperty) {
return 1; return 1;
} } else if (!secondProperty) {
return intl.compare(firstProperty, secondProperty); return -1;
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 { } else {
return intl.compare(firstProperty.toString(), secondProperty.toString()); throw new TypeError('sorting of items failed because of mismatched types');
} }
case 'undefined': } else {
return 1; if (
default: (firstProperty === null || firstProperty === undefined) &&
return -1; (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 *ngFor="let filter of service.filterDefinitions">
<mat-expansion-panel-header *ngIf="filter.options && filter.options.length"> <mat-expansion-panel-header *ngIf="filter.options && filter.options.length">
<mat-panel-title> <mat-panel-title>
<mat-icon> <mat-icon>
{{ filter.count ? 'checked' : ''}} {{ filter.count ? 'checked' : '' }}
</mat-icon> </mat-icon>
<span>{{ service.getFilterName(filter) | translate }}</span> <span>{{ service.getFilterName(filter) | translate }}</span>
</mat-panel-title> </mat-panel-title>
@ -12,7 +12,11 @@
<mat-action-list class="filtermenu-expanded"> <mat-action-list class="filtermenu-expanded">
<div *ngFor="let option of filter.options"> <div *ngFor="let option of filter.options">
<div *ngIf="isFilter(option)"> <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 }} {{ option.label | translate }}
</mat-checkbox> </mat-checkbox>
</div> </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); this.dismissed.next(true);
} }
} }
public isFilter(option: OsFilterOption): boolean { public isFilter(option: OsFilterOption): boolean {
return typeof option === 'string' ? false : true; 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module'; import { E2EImportsModule } from 'e2e-imports.module';
import { OsSortBottomSheetComponent } from './os-sort-bottom-sheet.component'; import { SortBottomSheetComponent } from './sort-bottom-sheet.component';
describe('OsSortBottomSheetComponent', () => { describe('SortBottomSheetComponent', () => {
// let component: OsSortBottomSheetComponent<any>; let fixture: ComponentFixture<SortBottomSheetComponent<any>>;
let fixture: ComponentFixture<OsSortBottomSheetComponent<any>>;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@ -14,7 +13,7 @@ describe('OsSortBottomSheetComponent', () => {
})); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(OsSortBottomSheetComponent); fixture = TestBed.createComponent(SortBottomSheetComponent);
// component = fixture.componentInstance; // component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@ -9,17 +9,17 @@ import { BaseViewModel } from 'app/site/base/base-view-model';
* usage: * usage:
* ``` * ```
* @ViewChild('sortBottomSheet') * @ViewChild('sortBottomSheet')
* public sortBottomSheet: OsSortBottomSheetComponent<V>; * public sortBottomSheet: SortBottomSheetComponent<V>;
* ... * ...
* this.bottomSheet.open(OsSortBottomSheetComponent, { data: SortService }); * this.bottomSheet.open(SortBottomSheetComponent, { data: SortService });
* ``` * ```
*/ */
@Component({ @Component({
selector: 'os-sort-bottom-sheet', selector: 'os-sort-bottom-sheet',
templateUrl: './os-sort-bottom-sheet.component.html', templateUrl: './sort-bottom-sheet.component.html',
styleUrls: ['./os-sort-bottom-sheet.component.scss'] 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) * Constructor. Gets a reference to itself (for closing after interaction)
* @param data * @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 { 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'); 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="custom-table-header flex-spaced on-transition-fade">
<div class="filter-count" *ngIf="filterService"> <div class="filter-count" *ngIf="filterService">
<span>{{ displayedCount }}&nbsp;</span><span translate>of</span> <span>{{ displayedCount }}&nbsp;</span><span translate>of</span>
<span>&nbsp;{{ filterService.totalCount }}</span> <span>&nbsp;{{ totalCount }}</span>
<span *ngIf="extraItemInfo">&nbsp;·&nbsp;{{ extraItemInfo }}</span> <span *ngIf="extraItemInfo">&nbsp;·&nbsp;{{ extraItemInfo }}</span>
</div> </div>
<div class="current-filters" *ngIf="filterService && filterService.activeFilterCount"> <div class="current-filters" *ngIf="filterService && filterService.activeFilterCount">
@ -61,12 +61,9 @@
<!-- non-mobile sorting menu --> <!-- non-mobile sorting menu -->
<mat-menu #menu> <mat-menu #menu>
<div *ngIf="hasSorting"> <div *ngIf="hasSorting">
<mat-list-item <mat-list-item *ngFor="let option of sortOptions" (click)="sortOption = option">
*ngFor="let option of sortService.sortOptions.options"
(click)="sortService.sortProperty = option.property"
>
<button mat-menu-item> <button mat-menu-item>
<mat-icon>{{ sortService.getSortIcon(option) }}</mat-icon> <mat-icon>{{ getSortIcon(option) }}</mat-icon>
<span>{{ sortService.getSortLabel(option) | translate }}</span> <span>{{ sortService.getSortLabel(option) | translate }}</span>
</button> </button>
</mat-list-item> </mat-list-item>

View File

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

View File

@ -4,9 +4,9 @@ import { MatBottomSheet } from '@angular/material';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { BaseViewModel } from 'app/site/base/base-view-model'; 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 { 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 { BaseSortListService } from 'app/core/ui-services/base-sort-list.service';
import { ViewportService } from 'app/core/ui-services/viewport.service'; import { ViewportService } from 'app/core/ui-services/viewport.service';
import { BaseFilterListService } from 'app/core/ui-services/base-filter-list.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({ @Component({
selector: 'os-sort-filter-bar', selector: 'os-sort-filter-bar',
templateUrl: './os-sort-filter-bar.component.html', templateUrl: './sort-filter-bar.component.html',
styleUrls: ['./os-sort-filter-bar.component.scss'] 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 * The currently active sorting service for the list view
*/ */
@ -58,6 +58,7 @@ export class OsSortFilterBarComponent<V extends BaseViewModel> {
@Output() @Output()
public searchFieldChange = new EventEmitter<string>(); public searchFieldChange = new EventEmitter<string>();
/** /**
* The filter side drawer * The filter side drawer
*/ */
@ -68,7 +69,7 @@ export class OsSortFilterBarComponent<V extends BaseViewModel> {
* The bottom sheet used to alter sorting in mobile view * The bottom sheet used to alter sorting in mobile view
*/ */
@ViewChild('sortBottomSheet') @ViewChild('sortBottomSheet')
public sortBottomSheet: OsSortBottomSheetComponent<V>; public sortBottomSheet: SortBottomSheetComponent<V>;
/** /**
* The 'opened/active' state of the fulltext filter input field * 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 * Constructor. Also creates a filtermenu component and a bottomSheet
* @param translate * @param translate
@ -106,7 +122,7 @@ export class OsSortFilterBarComponent<V extends BaseViewModel> {
*/ */
public openSortDropDown(): void { public openSortDropDown(): void {
if (this.vp.isMobile) { 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 => { bottomSheetRef.afterDismissed().subscribe(result => {
if (result) { if (result) {
this.sortService.sortProperty = result; this.sortService.sortProperty = result;
@ -136,23 +152,18 @@ export class OsSortFilterBarComponent<V extends BaseViewModel> {
/** /**
* Checks if there is an active FilterService present * Checks if there is an active FilterService present
* @returns wether the filters are present or not
*/ */
public get hasFilters(): boolean { public get hasFilters(): boolean {
if (this.filterService && this.filterService.hasFilterOptions) { return this.filterService && this.filterService.hasFilterOptions;
return true;
}
return false;
} }
/** /**
* Retrieves the currently active icon for an option. * Retrieves the currently active icon for an option.
* @param option * @param option
*/ */
public getSortIcon(option: OsSortingItem<V>): string { public getSortIcon(option: OsSortingOption<V>): string {
if (this.sortService.sortProperty !== option.property) { return this.sortService.getSortIcon(option);
return '';
}
return this.sortService.ascending ? 'arrow_downward' : 'arrow_upward';
} }
/** /**
@ -160,7 +171,7 @@ export class OsSortFilterBarComponent<V extends BaseViewModel> {
* the property is used. * the property is used.
* @param option * @param option
*/ */
public getSortLabel(option: OsSortingItem<V>): string { public getSortLabel(option: OsSortingOption<V>): string {
if (option.label) { if (option.label) {
return 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 { SortingListComponent } from './components/sorting-list/sorting-list.component';
import { SortingTreeComponent } from './components/sorting-tree/sorting-tree.component'; import { SortingTreeComponent } from './components/sorting-tree/sorting-tree.component';
import { ChoiceDialogComponent } from './components/choice-dialog/choice-dialog.component'; import { ChoiceDialogComponent } from './components/choice-dialog/choice-dialog.component';
import { OsSortFilterBarComponent } from './components/os-sort-filter-bar/os-sort-filter-bar.component'; import { SortFilterBarComponent } from './components/sort-filter-bar/sort-filter-bar.component';
import { OsSortBottomSheetComponent } from './components/os-sort-filter-bar/os-sort-bottom-sheet/os-sort-bottom-sheet.component'; import { SortBottomSheetComponent } from './components/sort-filter-bar/sort-bottom-sheet/sort-bottom-sheet.component';
import { FilterMenuComponent } from './components/os-sort-filter-bar/filter-menu/filter-menu.component'; import { FilterMenuComponent } from './components/sort-filter-bar/filter-menu/filter-menu.component';
import { LogoComponent } from './components/logo/logo.component'; import { LogoComponent } from './components/logo/logo.component';
import { C4DialogComponent, CopyrightSignComponent } from './components/copyright-sign/copyright-sign.component'; import { C4DialogComponent, CopyrightSignComponent } from './components/copyright-sign/copyright-sign.component';
import { ProjectorButtonComponent } from './components/projector-button/projector-button.component'; import { ProjectorButtonComponent } from './components/projector-button/projector-button.component';
@ -191,7 +191,7 @@ import { PrecisionPipe } from './pipes/precision.pipe';
SortingListComponent, SortingListComponent,
EditorModule, EditorModule,
SortingTreeComponent, SortingTreeComponent,
OsSortFilterBarComponent, SortFilterBarComponent,
LogoComponent, LogoComponent,
CopyrightSignComponent, CopyrightSignComponent,
C4DialogComponent, C4DialogComponent,
@ -220,8 +220,8 @@ import { PrecisionPipe } from './pipes/precision.pipe';
SortingListComponent, SortingListComponent,
SortingTreeComponent, SortingTreeComponent,
ChoiceDialogComponent, ChoiceDialogComponent,
OsSortFilterBarComponent, SortFilterBarComponent,
OsSortBottomSheetComponent, SortBottomSheetComponent,
FilterMenuComponent, FilterMenuComponent,
LogoComponent, LogoComponent,
CopyrightSignComponent, CopyrightSignComponent,
@ -241,10 +241,10 @@ import { PrecisionPipe } from './pipes/precision.pipe';
SearchValueSelectorComponent, SearchValueSelectorComponent,
SortingListComponent, SortingListComponent,
SortingTreeComponent, SortingTreeComponent,
OsSortFilterBarComponent, SortFilterBarComponent,
OsSortBottomSheetComponent, SortBottomSheetComponent,
DecimalPipe DecimalPipe
], ],
entryComponents: [OsSortBottomSheetComponent, C4DialogComponent] entryComponents: [SortBottomSheetComponent, C4DialogComponent]
}) })
export class SharedModule {} export class SharedModule {}

View File

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

View File

@ -1,32 +1,34 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/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 { BaseFilterListService, OsFilter, OsFilterOption } from 'app/core/ui-services/base-filter-list.service';
import { itemVisibilityChoices } from 'app/shared/models/agenda/item'; import { itemVisibilityChoices } from 'app/shared/models/agenda/item';
import { ViewItem } from '../models/view-item'; import { ViewItem } from '../models/view-item';
import { StorageService } from 'app/core/core-services/storage.service'; import { StorageService } from 'app/core/core-services/storage.service';
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
/**
* Filter the agenda list
*/
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class AgendaFilterListService extends BaseFilterListService<ViewItem> { export class AgendaFilterListService extends BaseFilterListService<ViewItem> {
protected name = 'Agenda';
public filterOptions: OsFilter[] = [];
/** /**
* Constructor. Also creates the dynamic filter options * Constructor. Also creates the dynamic filter options
*
* @param store * @param store
* @param repo
* @param translate Translation service * @param translate Translation service
*/ */
public constructor(store: StorageService, repo: ItemRepositoryService, private translate: TranslateService) { public constructor(store: StorageService, private translate: TranslateService) {
super(store, repo); super('Agenda', store);
this.filterOptions = [ }
/**
* @returns the filter definition
*/
protected getFilterDefinitions(): OsFilter[] {
return [
{ {
label: 'Visibility', label: 'Visibility',
property: 'type', 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 * @override from base filter list service
* Initializes the filterService. Returns the filtered data as Observable *
* @returns the list of ViewItems without the types
*/ */
public filter(): Observable<ViewItem[]> { protected preFilter(viewItems: ViewItem[]): ViewItem[] {
this.repo return viewItems.filter(item => item.type !== undefined);
.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;
} }
/**
* helper function to create options for visibility filters
*
* @returns a list of choices to filter from
*/
private createVisibilityFilterOptions(): OsFilterOption[] { private createVisibilityFilterOptions(): OsFilterOption[] {
const options = []; return itemVisibilityChoices.map(choice => ({
itemVisibilityChoices.forEach(choice => { condition: choice.key as number,
options.push({ label: choice.name
condition: choice.key as number, }));
label: choice.name
});
});
return options;
} }
} }

View File

@ -23,7 +23,9 @@ import { ViewAssignment, AssignmentPhases } from '../../models/view-assignment';
templateUrl: './assignment-list.component.html', templateUrl: './assignment-list.component.html',
styleUrls: ['./assignment-list.component.scss'] 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 * The different phases of an assignment. Info is fetched from server
*/ */
@ -57,7 +59,7 @@ export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignmen
private router: Router, private router: Router,
public operator: OperatorService 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 // activate multiSelect mode for this list view
this.canMultiSelect = true; this.canMultiSelect = true;
} }

View File

@ -1,53 +1,45 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service'; import { BaseFilterListService, OsFilter, OsFilterOption } from 'app/core/ui-services/base-filter-list.service';
import { BaseFilterListService, OsFilter } from 'app/core/ui-services/base-filter-list.service';
import { StorageService } from 'app/core/core-services/storage.service'; import { StorageService } from 'app/core/core-services/storage.service';
import { ViewAssignment, AssignmentPhases } from '../models/view-assignment'; import { ViewAssignment, AssignmentPhases } from '../models/view-assignment';
/**
* Filter service for the assignment list
*/
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class AssignmentFilterListService extends BaseFilterListService<ViewAssignment> { 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 * Constructor. Activates the phase options subscription
* *
* @param store StorageService * @param store StorageService
* @param assignmentRepo Repository * @param translate translate service
* @param constants the openslides constant service to get the assignment options
*/ */
public constructor(store: StorageService, assignmentRepo: AssignmentRepositoryService) { public constructor(store: StorageService) {
super(store, assignmentRepo); super('Assignments', store);
this.createPhaseOptions();
} }
/** /**
* Subscribes to the phases of an assignment that are defined in the server's * @returns the filter definition
* constants
*/ */
private createPhaseOptions(): void { protected getFilterDefinitions(): OsFilter[] {
this.phaseFilter.options = AssignmentPhases.map(ap => { 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 }; 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 { 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'; import { ViewAssignment } from '../models/view-assignment';
/**
* Sorting service for the assignment list
*/
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class AssignmentSortListService extends BaseSortListService<ViewAssignment> { export class AssignmentSortListService extends BaseSortListService<ViewAssignment> {
public sortOptions: OsSortingDefinition<ViewAssignment> = { /**
sortProperty: 'assignment', * Define the sort options
sortAscending: true, */
options: [ public sortOptions: OsSortingOption<ViewAssignment>[] = [
{ property: 'assignment', label: 'Name' }, { property: 'assignment', label: 'Name' },
{ property: 'phase', label: 'Phase' }, { property: 'phase', label: 'Phase' },
{ property: 'candidateAmount', label: 'Number of candidates' } { property: 'candidateAmount', label: 'Number of candidates' }
] ];
};
protected name = 'Assignment'; /**
* 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 { BaseFilterListService } from 'app/core/ui-services/base-filter-list.service';
import { BaseModel } from 'app/shared/models/base/base-model'; import { BaseModel } from 'app/shared/models/base/base-model';
import { StorageService } from 'app/core/core-services/storage.service'; 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 export abstract class ListViewBaseComponent<
implements OnDestroy { 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 * 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 titleService the title serivce
* @param translate the translate service * @param translate the translate service
* @param matSnackBar showing errors * @param matSnackBar showing errors
* @param filterService filter * @param viewModelRepo Repository for the view Model. Do NOT rename to "repo"
* @param sortService sorting * @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( public constructor(
titleService: Title, titleService: Title,
translate: TranslateService, translate: TranslateService,
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
protected viewModelRepo: R,
protected route?: ActivatedRoute, protected route?: ActivatedRoute,
protected storage?: StorageService, protected storage?: StorageService,
public filterService?: BaseFilterListService<V>, protected modelFilterListService?: BaseFilterListService<V>,
public sortService?: BaseSortListService<V> protected modelSortService?: BaseSortListService<V>
) { ) {
super(titleService, translate, matSnackBar); super(titleService, translate, matSnackBar);
this.selectedRows = []; this.selectedRows = [];
@ -114,40 +122,41 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel, M extends B
this.initializePagination(); this.initializePagination();
this.dataSource.paginator._intl.itemsPerPageLabel = this.translate.instant('items per page'); this.dataSource.paginator._intl.itemsPerPageLabel = this.translate.instant('items per page');
} }
if (this.filterService) {
this.onFilter(); // TODO: Add subscription to this.subscriptions
} if (this.modelFilterListService && this.modelSortService) {
if (this.sortService) { // filtering and sorting
this.onSort(); 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 * Standard filtering function. Sufficient for most list views but can be overwritten
*/ */
protected onFilter(): void { protected getModelListObservable(): Observable<V[]> {
if (this.sortService) { return this.viewModelRepo.getViewModelListObservable();
this.subscriptions.push(
this.filterService.filter().subscribe(filteredData => (this.sortService.data = filteredData))
);
} else {
this.filterService.filter().subscribe(filteredData => (this.dataSource.data = filteredData));
}
} }
/** private setDataSource(data: V[]): void {
* Standard sorting function. Sufficient for most list views but can be overwritten // the dataArray needs to be cleared (since angular 7)
*/ // changes are not detected properly anymore
protected onSort(): void { this.dataSource.data = [];
this.subscriptions.push( this.dataSource.data = data;
this.sortService.sort().subscribe(sortedData => {
// the dataArray needs to be cleared (since angular 7) this.checkSelection();
// changes are not detected properly anymore
this.dataSource.data = [];
this.dataSource.data = sortedData;
this.checkSelection();
})
);
} }
public onSortButton(itemProperty: string): void { 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', templateUrl: './history-list.component.html',
styleUrls: ['./history-list.component.scss'] 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 * Subject determine when the custom timestamp subject changes
*/ */
@ -51,7 +52,7 @@ export class HistoryListComponent extends ListViewBaseComponent<ViewHistory, His
private router: Router, private router: Router,
private operator: OperatorService 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', templateUrl: './mediafile-list.component.html',
styleUrls: ['./mediafile-list.component.scss'] 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 * Holds the actions for logos. Updated via an observable
*/ */
@ -108,7 +109,7 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile,
public sortService: MediafilesSortListService, public sortService: MediafilesSortListService,
private operator: OperatorService 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 // enables multiSelection for this listView
this.canMultiSelect = true; this.canMultiSelect = true;

View File

@ -1,65 +1,62 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BaseFilterListService, OsFilter } from 'app/core/ui-services/base-filter-list.service'; 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 { OperatorService } from 'app/core/core-services/operator.service';
import { StorageService } from 'app/core/core-services/storage.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ViewMediafile } from '../models/view-mediafile';
/**
* Filter service for media files
*/
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class MediafileFilterListService extends BaseFilterListService<ViewMediafile> { export class MediafileFilterListService extends BaseFilterListService<ViewMediafile> {
protected name = 'Mediafile';
/** /**
* A filter checking if a file is a pdf or not * Constructor.
*/ * Sets the filter options according to permissions
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
* @param store * @param store
* @param repo
* @param operator * @param operator
* @param translate * @param translate
*/ */
public constructor( public constructor(store: StorageService, private operator: OperatorService, private translate: TranslateService) {
store: StorageService, super('Mediafiles', store);
repo: MediafileRepositoryService,
operator: OperatorService, this.operator.getUserObservable().subscribe(() => {
private translate: TranslateService this.setFilterDefinitions();
) { });
super(store, repo); }
const filterOptions = operator.hasPerms('mediafiles.can_see_hidden')
? [this.hiddenOptions, this.pdfOption] /**
: [this.pdfOption]; * @returns the filter definition
this.updateFilterDefinitions(filterOptions); */
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 { 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'; import { ViewMediafile } from '../models/view-mediafile';
/**
* Sorting service for the mediafile list
*/
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class MediafilesSortListService extends BaseSortListService<ViewMediafile> { export class MediafilesSortListService extends BaseSortListService<ViewMediafile> {
public sortOptions: OsSortingDefinition<ViewMediafile> = { public sortOptions: OsSortingOption<ViewMediafile>[] = [
sortProperty: 'title', { property: 'title' },
sortAscending: true, {
options: [ property: 'type',
{ property: 'title' }, label: this.translate.instant('Type')
{ },
property: 'type', {
label: this.translate.instant('Type') property: 'size',
}, label: this.translate.instant('Size')
{ }
property: 'size', ];
label: this.translate.instant('Size')
} /**
] * Constructor.
}; *
protected name = 'Mediafile'; * @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> <span translate>Follow recommendations for all motions</span>
</button> </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 --> <!-- title column -->
<ng-container matColumnDef="title"> <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"> <mat-cell *matCellDef="let motion">
{{ motion.getTitle() }} {{ motion.getTitle() }}
</mat-cell> </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 { ViewMotion } from 'app/site/motions/models/view-motion';
import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block'; import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
import { StorageService } from 'app/core/core-services/storage.service'; import { StorageService } from 'app/core/core-services/storage.service';
import { Motion } from 'app/shared/models/motions/motion';
/** /**
* Detail component to display one motion block * 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', templateUrl: './motion-block-detail.component.html',
styleUrls: ['./motion-block-detail.component.scss'] 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 * Determines the block id from the given URL
*/ */
public block: ViewMotionBlock; public block: ViewMotionBlock;
/**
* All motions in this block
*/
public motions: ViewMotion[];
/** /**
* Determine the edit mode * Determine the edit mode
*/ */
@ -67,11 +64,11 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion
storage: StorageService, storage: StorageService,
private operator: OperatorService, private operator: OperatorService,
private router: Router, private router: Router,
private repo: MotionBlockRepositoryService, protected repo: MotionBlockRepositoryService,
private motionRepo: MotionRepositoryService, protected motionRepo: MotionRepositoryService,
private promptService: PromptService 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 { public ngOnInit(): void {
super.setTitle('Motion block'); super.setTitle('Motion block');
this.initTable(); this.initTable();
const blockId = parseInt(this.route.snapshot.params.id, 10);
this.blockEditForm = new FormGroup({ this.blockEditForm = new FormGroup({
title: new FormControl('', Validators.required) title: new FormControl('', Validators.required)
}); });
const blockId = +this.route.snapshot.params.id; // pseudo filter
this.block = this.repo.getViewModel(blockId); this.subscriptions.push(
this.repo.getViewModelObservable(blockId).subscribe(newBlock => {
this.repo.getViewModelObservable(blockId).subscribe(newBlock => { if (newBlock) {
// necessary since the subscription can return undefined this.block = newBlock;
if (newBlock) { this.subscriptions.push(
this.block = newBlock; this.repo.getViewMotionsByBlock(this.block.motionBlock).subscribe(viewMotions => {
if (viewMotions && viewMotions.length) {
// set the blocks title in the form this.dataSource.data = viewMotions;
this.blockEditForm.get('title').setValue(this.block.title); } else {
this.dataSource.data = [];
this.repo.getViewMotionsByBlock(this.block.motionBlock).subscribe(newMotions => { }
this.motions = newMotions; })
this.dataSource.data = this.motions; );
}); }
} })
}); );
} }
/** /**
@ -193,8 +191,8 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion
* Following a recommendation implies, that a valid recommendation exists. * Following a recommendation implies, that a valid recommendation exists.
*/ */
public isFollowingProhibited(): boolean { public isFollowingProhibited(): boolean {
if (this.motions) { if (this.dataSource.data) {
return this.motions.every(motion => motion.isInFinalState() || !motion.recommendation_id); return this.dataSource.data.every(motion => motion.isInFinalState() || !motion.recommendation_id);
} else { } else {
return false; return false;
} }

View File

@ -51,10 +51,10 @@
<!-- Table --> <!-- Table -->
<mat-card class="os-card"> <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 --> <!-- Projector column -->
<ng-container matColumnDef="projector"> <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"> <mat-cell *matCellDef="let block">
<os-projector-button [object]="block"></os-projector-button> <os-projector-button [object]="block"></os-projector-button>
</mat-cell> </mat-cell>
@ -62,13 +62,17 @@
<!-- title column --> <!-- title column -->
<ng-container matColumnDef="title"> <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> <mat-cell *matCellDef="let block"> {{ block.title }} </mat-cell>
</ng-container> </ng-container>
<!-- amount column --> <!-- amount column -->
<ng-container matColumnDef="amount"> <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"> <mat-cell *matCellDef="let block">
<span class="os-amount-chip">{{ getMotionAmount(block.motionBlock) }}</span> <span class="os-amount-chip">{{ getMotionAmount(block.motionBlock) }}</span>
</mat-cell> </mat-cell>
@ -76,7 +80,9 @@
<!-- menu --> <!-- menu -->
<ng-container matColumnDef="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"> <mat-cell *matCellDef="let block">
<button <button
*ngIf="canEdit" *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 { ListViewBaseComponent } from 'app/site/base/list-view-base';
import { MotionBlock } from 'app/shared/models/motions/motion-block'; import { MotionBlock } from 'app/shared/models/motions/motion-block';
import { MotionBlockRepositoryService } from 'app/core/repositories/motions/motion-block-repository.service'; 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 { OperatorService } from 'app/core/core-services/operator.service';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
import { StorageService } from 'app/core/core-services/storage.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', templateUrl: './motion-block-list.component.html',
styleUrls: ['./motion-block-list.component.scss'] 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 * Holds the create form
*/ */
@ -88,9 +91,10 @@ export class MotionBlockListComponent extends ListViewBaseComponent<ViewMotionBl
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private promptService: PromptService, private promptService: PromptService,
private itemRepo: ItemRepositoryService, 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({ this.createBlockForm = this.formBuilder.group({
title: ['', Validators.required], title: ['', Validators.required],
@ -105,14 +109,7 @@ export class MotionBlockListComponent extends ListViewBaseComponent<ViewMotionBl
public ngOnInit(): void { public ngOnInit(): void {
super.setTitle('Motion blocks'); super.setTitle('Motion blocks');
this.initTable(); this.initTable();
this.items = this.itemRepo.getViewModelListBehaviorSubject(); 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)); this.agendaRepo.getDefaultAgendaVisibility().subscribe(visibility => (this.defaultVisibility = visibility));
} }

View File

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

View File

@ -20,7 +20,8 @@ import { StorageService } from 'app/core/core-services/storage.service';
templateUrl: './workflow-list.component.html', templateUrl: './workflow-list.component.html',
styleUrls: ['./workflow-list.component.scss'] 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 * Holds the new workflow title
*/ */
@ -51,10 +52,10 @@ export class WorkflowListComponent extends ListViewBaseComponent<ViewWorkflow, W
storage: StorageService, storage: StorageService,
private dialog: MatDialog, private dialog: MatDialog,
private router: Router, private router: Router,
private workflowRepo: WorkflowRepositoryService, protected workflowRepo: WorkflowRepositoryService,
private promptService: PromptService 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 { 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 { ViewMotion } from '../models/view-motion';
import { CategoryRepositoryService } from 'app/core/repositories/motions/category-repository.service'; import { CategoryRepositoryService } from 'app/core/repositories/motions/category-repository.service';
import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-repository.service'; import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-repository.service';
import { StorageService } from 'app/core/core-services/storage.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 { MotionBlockRepositoryService } from 'app/core/repositories/motions/motion-block-repository.service';
import { MotionCommentSectionRepositoryService } from 'app/core/repositories/motions/motion-comment-section-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 { OperatorService } from 'app/core/core-services/operator.service';
import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.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({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class MotionFilterListService extends BaseFilterListService<ViewMotion> { 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 * Listen to the configuration for change in defined/used workflows
*/ */
private enabledWorkflows = { statuteEnabled: false, statute: null, motion: null }; 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 = { public categoryFilterOptions: OsFilter = {
property: 'category', property: 'category',
options: [] options: []
@ -98,7 +81,6 @@ export class MotionFilterListService extends BaseFilterListService<ViewMotion> {
{ {
property: 'star', property: 'star',
label: this.translate.instant('Favorites'), label: this.translate.instant('Favorites'),
isActive: false,
options: [ options: [
{ {
condition: true, condition: true,
@ -113,7 +95,6 @@ export class MotionFilterListService extends BaseFilterListService<ViewMotion> {
{ {
property: 'hasNotes', property: 'hasNotes',
label: this.translate.instant('Personal notes'), label: this.translate.instant('Personal notes'),
isActive: false,
options: [ options: [
{ {
condition: true, condition: true,
@ -132,216 +113,175 @@ export class MotionFilterListService extends BaseFilterListService<ViewMotion> {
* the available filters * the available filters
* *
* @param store The browser's storage; required for fetching filters from any previous sessions * @param store The browser's storage; required for fetching filters from any previous sessions
* @param workflowRepo Subscribing to filters by states/Recommendation * @param categoryRepo to filter by Categories
* @param categoryRepo Subscribing to filters by Categories * @param motionBlockRepo to filter by MotionBlock
* @param motionBlockRepo Subscribing to filters by MotionBlock * @param commentRepo to filter by motion comments
* @param commentRepo subycribing filter by presense of comment * @param tagRepo to filter by tags
* @param workflowRepo Subscribing to filters by states and recommendation
* @param translate Translation service * @param translate Translation service
* @param config the current configuration (to determine which workflow filters to offer ) * @param operator
* @param motionRepo the motion's own repository, required by the parent
*/ */
public constructor( public constructor(
store: StorageService, store: StorageService,
categoryRepo: CategoryRepositoryService,
motionBlockRepo: MotionBlockRepositoryService,
commentRepo: MotionCommentSectionRepositoryService,
tagRepo: TagRepositoryService,
private workflowRepo: WorkflowRepositoryService, private workflowRepo: WorkflowRepositoryService,
private categoryRepo: CategoryRepositoryService,
private motionBlockRepo: MotionBlockRepositoryService,
private commentRepo: MotionCommentSectionRepositoryService,
private translate: TranslateService, private translate: TranslateService,
private config: ConfigService,
motionRepo: MotionRepositoryService,
private operator: OperatorService, 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.subscribeWorkflows();
this.subscribeCategories();
this.subscribeMotionBlocks();
this.subscribeComments();
this.subscribeTags();
this.operator.getUserObservable().subscribe(() => { 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 { protected getFilterDefinitions(): OsFilter[] {
this.motionBlockRepo.getViewModelListObservable().subscribe(motionBlocks => { let filterDefinitions = [
const motionBlockOptions: OsFilterOptions = motionBlocks.map(mb => ({ this.stateFilterOptions,
condition: mb.id, this.categoryFilterOptions,
label: mb.title, this.motionBlockFilterOptions,
isActive: false this.recommendationFilterOptions,
})); this.motionCommentFilterOptions,
if (motionBlocks.length) { this.tagFilterOptions
motionBlockOptions.push('-'); ];
motionBlockOptions.push({
condition: null, if (!this.operator.isAnonymous) {
label: this.translate.instant('No motion block set'), filterDefinitions = filterDefinitions.concat(this.personalNoteFilterOptions);
isActive: false }
}); return filterDefinitions;
}
this.motionBlockFilterOptions.options = motionBlockOptions;
this.updateFilterDefinitions(this.filterOptions);
});
} }
/** /**
* Subscibes to changing Categories, and updates the filter accordingly * Subscribes to changing Workflows, and updates the state and recommendation filters 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
*/ */
private subscribeWorkflows(): void { private subscribeWorkflows(): void {
this.workflowRepo.getViewModelListObservable().subscribe(workflows => { this.workflowRepo.getViewModelListObservable().subscribe(workflows => {
this.currentWorkflows = workflows; if (workflows && workflows.length) {
this.updateWorkflows(); const workflowFilters: WorkflowFilterDesc[] = [];
}); const recoFilters: WorkflowFilterDesc[] = [];
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();
});
}
/** const finalStates: number[] = [];
* Helper to show only filter for workflows that are included in to currently const nonFinalStates: number[] = [];
* set config options
*/ // get all relevant information
private updateWorkflows(): void { for (const workflow of workflows) {
const workflowOptions: OsFilterOptions = []; if (this.isWorkflowEnabled(workflow.id)) {
const finalStates: number[] = []; workflowFilters.push({
const nonFinalStates: number[] = []; name: workflow.name,
const recommendationOptions: OsFilterOptions = []; filter: []
if (!this.currentWorkflows) { });
return;
} recoFilters.push({
this.currentWorkflows.forEach(workflow => { name: workflow.name,
if ( filter: []
workflow.id === this.enabledWorkflows.motion || });
(this.enabledWorkflows.statuteEnabled && workflow.id === this.enabledWorkflows.statute)
) { for (const state of workflow.states) {
workflowOptions.push(workflow.name); if (
recommendationOptions.push(workflow.name); this.operator.hasPerms('motions.can_manage', 'motions.can_manage_metadata') &&
workflow.states.forEach(state => { state.restriction
// filter out restricted states for unpriviledged users ) {
if ( // sort final and non final states
this.operator.hasPerms('motions.can_manage', 'motions.can_manage_metadata') || state.isFinalState ? finalStates.push(state.id) : nonFinalStates.push(state.id);
state.restriction.length === 0
) { workflowFilters[workflowFilters.length - 1].filter.push({
if (state.isFinalState) { condition: state.id,
finalStates.push(state.id); label: state.name
} else { });
nonFinalStates.push(state.id);
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);
}
/** // convert to filter options
* Subscibes to changing Comments, and updates the filter accordingly if (workflowFilters && workflowFilters.length) {
*/ let workflowOptions: OsFilterOptions = [];
private subscribeComments(): void { for (const filterDef of workflowFilters) {
this.commentRepo.getViewModelListObservable().subscribe(comments => { workflowOptions.push(filterDef.name);
const commentOptions: OsFilterOptions = comments.map(comment => ({ workflowOptions = workflowOptions.concat(filterDef.filter);
condition: comment.id, }
label: comment.name,
isActive: false // add "done" and "undone"
})); workflowOptions.push('-');
if (comments.length) { workflowOptions.push({
commentOptions.push('-'); label: 'Done',
commentOptions.push({ condition: finalStates
label: this.translate.instant('No comment'), });
condition: null
}); 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);
}); });
} }
/** private isWorkflowEnabled(workflowId: number): boolean {
* Subscibes to changing Tags, and updates the filter accordingly return (
*/ workflowId === this.enabledWorkflows.motion ||
private subscribeTags(): void { (this.enabledWorkflows.statuteEnabled && workflowId === this.enabledWorkflows.statute)
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);
});
} }
} }

View File

@ -1,42 +1,74 @@
import { Injectable } from '@angular/core'; 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 { 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({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class MotionSortListService extends BaseSortListService<ViewMotion> { export class MotionSortListService extends BaseSortListService<ViewMotion> {
public sortOptions: OsSortingDefinition<ViewMotion> = { /**
sortProperty: 'weight', * Hold the default motion sorting
sortAscending: true, */
options: [ private defaultMotionSorting: string;
{ 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';
/** /**
* Constructor. Sets the default sorting if none is set locally * To wait until the default motion was loaded once
*
* @param translate
* @param store
* @param config
*/ */
public constructor(translate: TranslateService, store: StorageService, config: ConfigService) { private readonly defaultSortingLoaded: Deferred<void> = new Deferred();
super(translate, store);
this.defaultSorting = config.instant<keyof ViewMotion>('motions_motions_sorting'); /**
* 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', templateUrl: './tag-list.component.html',
styleUrls: ['./tag-list.component.css'] 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 editTag = false;
public newTag = false; public newTag = false;
public selectedTag: ViewTag; public selectedTag: ViewTag;
@ -50,7 +50,7 @@ export class TagListComponent extends ListViewBaseComponent<ViewTag, Tag> implem
private repo: TagRepositoryService, private repo: TagRepositoryService,
private promptService: PromptService 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 for the user list view.
*
*/ */
@Component({ @Component({
selector: 'os-user-list', selector: 'os-user-list',
templateUrl: './user-list.component.html', templateUrl: './user-list.component.html',
styleUrls: ['./user-list.component.scss'] 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. * The reference to the template.
*/ */
@ -154,7 +153,7 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser, User> imp
private userPdf: UserPdfExportService, private userPdf: UserPdfExportService,
private dialog: MatDialog 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 // enable multiSelect for this listView
this.canMultiSelect = true; this.canMultiSelect = true;

View File

@ -1,107 +1,75 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BaseFilterListService, OsFilter } from 'app/core/ui-services/base-filter-list.service'; 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 { 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 { TranslateService } from '@ngx-translate/core';
import { ViewUser } from '../models/view-user';
/**
* Filter the user list
*/
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class UserFilterListService extends BaseFilterListService<ViewUser> { export class UserFilterListService extends BaseFilterListService<ViewUser> {
protected name = 'User'; private userGroupFilterOptions: OsFilter = {
private userGroupFilterOptions = {
isActive: false,
property: 'groups_id', property: 'groups_id',
label: 'Groups', label: 'Groups',
options: [] 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 * Constructor.
* generated dynamically, as the options change with the datastore * Subscribes to incoming group definitions.
*/
public get filterOptions(): OsFilter[] {
return [this.userGroupFilterOptions].concat(this.staticFilterOptions);
}
/**
* Contructor. Subscribes to incoming group definitions.
* *
* @param store * @param store
* @param groupRepo * @param groupRepo to filter by groups
* @param repo
* @param translate marking some translations that are unique here * @param translate marking some translations that are unique here
*
*/ */
public constructor( public constructor(store: StorageService, groupRepo: GroupRepositoryService, private translate: TranslateService) {
store: StorageService, super('User', store);
private groupRepo: GroupRepositoryService, this.updateFilterForRepo(groupRepo, this.userGroupFilterOptions, this.translate.instant('Default'), [1]);
repo: UserRepositoryService,
private translate: TranslateService
) {
super(store, repo);
this.subscribeGroups();
} }
/** /**
* Updates the filter according to existing groups. * @returns the filter definition
* TODO: Users with only the 'standard' group set appear in the model as items without groups_id. 'Standard' filter is broken
*/ */
public subscribeGroups(): void { protected getFilterDefinitions(): OsFilter[] {
this.groupRepo.getViewModelListObservable().subscribe(groups => { const staticFilterOptions: OsFilter[] = [
const groupOptions = []; {
groups.forEach(group => { property: 'is_present',
groupOptions.push({ label: 'Presence',
condition: group.id, options: [
label: group.name, { condition: true, label: this.translate.instant('Is present') },
isActive: false { condition: false, label: this.translate.instant('Is not present') }
}); ]
}); },
this.userGroupFilterOptions.options = groupOptions; {
this.updateFilterDefinitions(this.filterOptions); 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 { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/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 { ConfigService } from 'app/core/ui-services/config.service';
import { StorageService } from 'app/core/core-services/storage.service'; import { StorageService } from 'app/core/core-services/storage.service';
import { ViewUser } from '../models/view-user'; import { ViewUser } from '../models/view-user';
/**
* Sorting service for the user list
*/
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class UserSortListService extends BaseSortListService<ViewUser> { export class UserSortListService extends BaseSortListService<ViewUser> {
public sortOptions: OsSortingDefinition<ViewUser> = { /**
sortProperty: 'first_name', * Define the sort options
sortAscending: true, */
options: [ public sortOptions: OsSortingOption<ViewUser>[] = [
{ property: 'first_name', label: 'Given name' }, { property: 'first_name', label: 'Given name' },
{ property: 'last_name', label: 'Surname' }, { property: 'last_name', label: 'Surname' },
{ property: 'is_present', label: 'Presence' }, { property: 'is_present', label: 'Presence' },
{ property: 'is_active', label: 'Is active' }, { property: 'is_active', label: 'Is active' },
{ property: 'is_committee', label: 'Is committee' }, { property: 'is_committee', label: 'Is committee' },
{ property: 'number', label: 'Participant number' }, { property: 'number', label: 'Participant number' },
{ property: 'structure_level', label: 'Structure level' }, { property: 'structure_level', label: 'Structure level' },
{ property: 'comment' } { property: 'comment' }
] // TODO email send?
}; ];
protected name = 'User';
/** /**
* Constructor. Sets the default sorting if none is set locally * Constructor.
* *
* @param translate * @param translate required by parent
* @param store * @param store requires by parent
* @param config
*/ */
public constructor(translate: TranslateService, store: StorageService, config: ConfigService) { public constructor(translate: TranslateService, store: StorageService) {
super(translate, store); super('User', translate, store);
this.defaultSorting = config.instant<keyof ViewUser>('users_sort_by'); }
/**
* 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", input_type="choice",
label="Sort motions by", label="Sort motions by",
choices=( choices=(
{"value": "callListWeight", "display_name": "Call list"}, {"value": "weight", "display_name": "Call list"},
{"value": "identifier", "display_name": "Identifier"}, {"value": "identifier", "display_name": "Identifier"},
), ),
weight=335, weight=335,