Merge pull request #4671 from tsiegleauq/rework-sort-filter
Rework sort and filter
This commit is contained in:
commit
7ab040823d
@ -136,7 +136,7 @@ export class DataStoreUpdateManagerService {
|
||||
if (this.currentUpdateSlot) {
|
||||
const request = new Deferred();
|
||||
this.updateSlotRequests.push(request);
|
||||
await request.promise;
|
||||
await request;
|
||||
}
|
||||
this.currentUpdateSlot = new UpdateSlot(DS);
|
||||
return this.currentUpdateSlot;
|
||||
|
@ -120,7 +120,7 @@ export class OperatorService implements OnAfterAppsLoaded {
|
||||
private readonly _loaded: Deferred<void> = new Deferred();
|
||||
|
||||
public get loaded(): Promise<void> {
|
||||
return this._loaded.promise;
|
||||
return this._loaded;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -51,7 +51,7 @@ export class PingService {
|
||||
isStable.resolve();
|
||||
});
|
||||
|
||||
await Promise.all([gotConstants.promise, isStable.promise]);
|
||||
await Promise.all([gotConstants, isStable]);
|
||||
|
||||
// Connects the ping-pong mechanism to the opening and closing of the connection.
|
||||
this.websocketService.closeEvent.subscribe(() => this.stopPing());
|
||||
|
@ -13,7 +13,7 @@
|
||||
* //
|
||||
* ```
|
||||
*/
|
||||
export class Deferred<T = void> {
|
||||
export class Deferred<T = void> extends Promise<T> {
|
||||
/**
|
||||
* The promise to wait for
|
||||
*/
|
||||
@ -28,9 +28,11 @@ export class Deferred<T = void> {
|
||||
* Creates the promise and overloads the resolve function
|
||||
*/
|
||||
public constructor() {
|
||||
this.promise = new Promise<T>(resolve => {
|
||||
this.resolve = resolve;
|
||||
let preResolve: (val?: T) => void;
|
||||
super(resolve => {
|
||||
preResolve = resolve;
|
||||
});
|
||||
this._resolve = preResolve;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,19 +1,16 @@
|
||||
import { auditTime } from 'rxjs/operators';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||
|
||||
import { BaseRepository } from 'app/core/repositories/base-repository';
|
||||
import { BaseModel } from '../../shared/models/base/base-model';
|
||||
import { BaseModel } from 'app/shared/models/base/base-model';
|
||||
import { BaseRepository } from '../repositories/base-repository';
|
||||
import { BaseViewModel } from '../../site/base/base-view-model';
|
||||
import { StorageService } from '../core-services/storage.service';
|
||||
|
||||
/**
|
||||
* Describes the available filters for a listView.
|
||||
* @param isActive: the current state of the filter
|
||||
* @param property: the ViewModel's property or method to filter by
|
||||
* @param label: An optional, different label (if not present, the property will be used)
|
||||
* @param condition: The conditions to be met for a successful display of data. These will
|
||||
* be updated by the {@link filterMenu}
|
||||
* @param options a list of available options for a filter
|
||||
* @param count
|
||||
*/
|
||||
export interface OsFilter {
|
||||
property: string;
|
||||
@ -37,248 +34,296 @@ export type OsFilterOptions = (OsFilterOption | string)[];
|
||||
*/
|
||||
export interface OsFilterOption {
|
||||
label: string;
|
||||
condition: string | boolean | number | number[];
|
||||
condition: OsFilterOptionCondition;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the type of a filter condition
|
||||
*/
|
||||
type OsFilterOptionCondition = string | boolean | number | number[];
|
||||
|
||||
/**
|
||||
* Filter for the list view. List views can subscribe to its' dataService (providing filter definitions)
|
||||
* and will receive their filtered data as observable
|
||||
*/
|
||||
|
||||
export abstract class BaseFilterListService<V extends BaseViewModel> {
|
||||
/**
|
||||
* stores the currently used raw data to be used for the filter
|
||||
*/
|
||||
protected currentRawData: V[];
|
||||
private inputData: V[];
|
||||
|
||||
/**
|
||||
* Subscription for the inputData list.
|
||||
* Acts as an semaphore for new filtered data
|
||||
*/
|
||||
protected inputDataSubscription: Subscription;
|
||||
|
||||
/**
|
||||
* The currently used filters.
|
||||
*/
|
||||
public filterDefinitions: OsFilter[];
|
||||
|
||||
/**
|
||||
* The observable output for the filtered data
|
||||
*/
|
||||
public filterDataOutput = new BehaviorSubject<V[]>([]);
|
||||
|
||||
protected filteredData: V[];
|
||||
|
||||
protected name: string;
|
||||
|
||||
/**
|
||||
* @returns the total count of items before the filter
|
||||
*/
|
||||
public get totalCount(): number {
|
||||
return this.currentRawData ? this.currentRawData.length : 0;
|
||||
public get unfilteredCount(): number {
|
||||
return this.inputData ? this.inputData.length : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The observable output for the filtered data
|
||||
*/
|
||||
private readonly outputSubject = new BehaviorSubject<V[]>([]);
|
||||
|
||||
/**
|
||||
* @return Observable data for the filtered output subject
|
||||
*/
|
||||
public get outputObservable(): Observable<V[]> {
|
||||
return this.outputSubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the amount of items that pass the filter service's filters
|
||||
*/
|
||||
public get filteredCount(): number {
|
||||
return this.filteredData ? this.filteredData.length : 0;
|
||||
return this.outputSubject.getValue().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the amount of filters currently in use by this filter Service
|
||||
*
|
||||
* @returns a number of filters
|
||||
* @returns the amount of currently active filters
|
||||
*/
|
||||
public get activeFilterCount(): number {
|
||||
if (!this.filterDefinitions || !this.filterDefinitions.length) {
|
||||
return 0;
|
||||
}
|
||||
let filters = 0;
|
||||
for (const filter of this.filterDefinitions) {
|
||||
if (filter.count) {
|
||||
filters += 1;
|
||||
}
|
||||
}
|
||||
return filters;
|
||||
return this.filterDefinitions ? this.filterDefinitions.filter(filter => filter.count).length : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Boolean indicationg if there are any filters described in this service
|
||||
* Boolean indicating if there are any filters described in this service
|
||||
*
|
||||
* @returns true if there are defined filters (regardless of current state)
|
||||
*/
|
||||
public get hasFilterOptions(): boolean {
|
||||
return this.filterDefinitions && this.filterDefinitions.length ? true : false;
|
||||
return !!this.filterDefinitions && this.filterDefinitions.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param name the name of the filter service
|
||||
* @param store storage service, to read saved filter variables
|
||||
*/
|
||||
public constructor(protected store: StorageService, protected repo: BaseRepository<V, BaseModel>) {}
|
||||
public constructor(protected name: string, private store: StorageService) {}
|
||||
|
||||
/**
|
||||
* Initializes the filterService. Returns the filtered data as Observable
|
||||
* Initializes the filterService.
|
||||
*
|
||||
* @param inputData Observable array with ViewModels
|
||||
*/
|
||||
public filter(): Observable<V[]> {
|
||||
this.repo
|
||||
.getViewModelListObservable()
|
||||
.pipe(auditTime(10))
|
||||
.subscribe(data => {
|
||||
this.currentRawData = data;
|
||||
this.filteredData = this.filterData(data);
|
||||
this.filterDataOutput.next(this.filteredData);
|
||||
public async initFilters(inputData: Observable<V[]>): Promise<void> {
|
||||
const storedFilter = await this.store.get<OsFilter[]>('filter_' + this.name);
|
||||
|
||||
if (storedFilter) {
|
||||
this.filterDefinitions = storedFilter;
|
||||
} else {
|
||||
this.filterDefinitions = this.getFilterDefinitions();
|
||||
this.storeActiveFilters();
|
||||
}
|
||||
|
||||
if (this.inputDataSubscription) {
|
||||
this.inputDataSubscription.unsubscribe();
|
||||
this.inputDataSubscription = null;
|
||||
}
|
||||
this.inputDataSubscription = inputData.subscribe(data => {
|
||||
this.inputData = data;
|
||||
this.updateFilteredData();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce children implement a method that returns actual filter definitions
|
||||
*/
|
||||
protected abstract getFilterDefinitions(): OsFilter[];
|
||||
|
||||
/**
|
||||
* Takes the filter definition from children and using {@link getFilterDefinitions}
|
||||
* and sets/updates {@link filterDefinitions}
|
||||
*/
|
||||
public setFilterDefinitions(): void {
|
||||
if (this.filterDefinitions) {
|
||||
const newDefinitions = this.getFilterDefinitions();
|
||||
this.store.get('filter_' + this.name).then((storedDefinition: OsFilter[]) => {
|
||||
for (const newDef of newDefinitions) {
|
||||
let count = 0;
|
||||
const matchingExistingFilter = storedDefinition.find(oldDef => oldDef.property === newDef.property);
|
||||
for (const option of newDef.options) {
|
||||
if (typeof option === 'object') {
|
||||
if (matchingExistingFilter && matchingExistingFilter.options) {
|
||||
const existingOption = matchingExistingFilter.options.find(
|
||||
o =>
|
||||
typeof o !== 'string' &&
|
||||
JSON.stringify(o.condition) === JSON.stringify(option.condition)
|
||||
) as OsFilterOption;
|
||||
if (existingOption) {
|
||||
option.isActive = existingOption.isActive;
|
||||
}
|
||||
if (option.isActive) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
newDef.count = count;
|
||||
}
|
||||
|
||||
this.filterDefinitions = newDefinitions;
|
||||
this.storeActiveFilters();
|
||||
});
|
||||
this.loadStorageDefinition(this.filterDefinitions);
|
||||
return this.filterDataOutput;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get the `viewModelListObservable` of a given repository object and creates dynamic filters for them
|
||||
*
|
||||
* @param repo repository to create dynamic filters from
|
||||
* @param filter the OSFilter for the filter property
|
||||
* @param noneOptionLabel The label of the non option, if set
|
||||
* @param exexcludeIds Set if certain ID's should be excluded from filtering
|
||||
*/
|
||||
protected updateFilterForRepo(
|
||||
repo: BaseRepository<BaseViewModel, BaseModel>,
|
||||
filter: OsFilter,
|
||||
noneOptionLabel?: string,
|
||||
excludeIds?: number[]
|
||||
): void {
|
||||
repo.getViewModelListObservable().subscribe(viewModel => {
|
||||
if (viewModel && viewModel.length) {
|
||||
let filterProperties: (OsFilterOption | string)[];
|
||||
|
||||
filterProperties = viewModel
|
||||
.filter(model => (excludeIds && excludeIds.length ? !excludeIds.includes(model.id) : true))
|
||||
.map(model => {
|
||||
return {
|
||||
condition: model.id,
|
||||
label: model.getTitle()
|
||||
};
|
||||
});
|
||||
|
||||
filterProperties.push('-');
|
||||
filterProperties.push({
|
||||
condition: null,
|
||||
label: noneOptionLabel
|
||||
});
|
||||
|
||||
filter.options = filterProperties;
|
||||
this.setFilterDefinitions();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the filtered data and store the current filter options
|
||||
*/
|
||||
public storeActiveFilters(): void {
|
||||
this.updateFilteredData();
|
||||
this.store.set('filter_' + this.name, this.filterDefinitions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies current filters in {@link filterDefinitions} to the {@link inputData} list
|
||||
* and publishes the filtered data to the observable {@link outputSubject}
|
||||
*/
|
||||
private updateFilteredData(): void {
|
||||
let filteredData: V[];
|
||||
if (!this.inputData) {
|
||||
filteredData = [];
|
||||
} else {
|
||||
const preFilteredList = this.preFilter(this.inputData);
|
||||
if (preFilteredList) {
|
||||
this.inputData = preFilteredList;
|
||||
}
|
||||
|
||||
if (!this.filterDefinitions || !this.filterDefinitions.length) {
|
||||
filteredData = this.inputData;
|
||||
} else {
|
||||
filteredData = this.inputData.filter(item =>
|
||||
this.filterDefinitions.every(filter => !filter.count || this.checkIncluded(item, filter))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.outputSubject.next(filteredData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Had to be overwritten by children if required
|
||||
* Adds the possibility to filter the inputData before the user applied filter
|
||||
*
|
||||
* @param rawInputData will be set to {@link this.inputData}
|
||||
* @returns should be a filtered version of `rawInputData`. Returns void if unused
|
||||
*/
|
||||
protected preFilter(rawInputData: V[]): V[] | void {}
|
||||
|
||||
/**
|
||||
* Toggles a filter option, to be called after a checkbox state has changed.
|
||||
*
|
||||
* @param filterName a filter name as string
|
||||
* @param option filter option
|
||||
*/
|
||||
public toggleFilterOption(filterName: string, option: OsFilterOption): void {
|
||||
option.isActive ? this.removeFilterOption(filterName, option) : this.addFilterOption(filterName, option);
|
||||
this.storeActiveFilters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a newly created filter
|
||||
* @param filter
|
||||
*
|
||||
* @param filterProperty new filter as string
|
||||
* @param option filter option
|
||||
*/
|
||||
public addFilterOption(filterName: string, option: OsFilterOption): void {
|
||||
const filter = this.filterDefinitions.find(f => f.property === filterName);
|
||||
protected addFilterOption(filterProperty: string, option: OsFilterOption): void {
|
||||
const filter = this.filterDefinitions.find(f => f.property === filterProperty);
|
||||
if (filter) {
|
||||
const filterOption = filter.options.find(
|
||||
o => typeof o !== 'string' && o.condition === option.condition
|
||||
) as OsFilterOption;
|
||||
if (filterOption && !filterOption.isActive) {
|
||||
filterOption.isActive = true;
|
||||
filter.count += 1;
|
||||
if (!filter.count) {
|
||||
filter.count = 1;
|
||||
} else {
|
||||
filter.count += 1;
|
||||
}
|
||||
}
|
||||
if (filter.count === 1) {
|
||||
this.filteredData = this.filterData(this.filteredData);
|
||||
} else {
|
||||
this.filteredData = this.filterData(this.currentRawData);
|
||||
}
|
||||
this.filterDataOutput.next(this.filteredData);
|
||||
this.setStorageDefinition();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a filter option.
|
||||
*
|
||||
* @param filterName: The property name of this filter
|
||||
* @param option: The option to disable
|
||||
* @param filterName The property name of this filter
|
||||
* @param option The option to disable
|
||||
*/
|
||||
public removeFilterOption(filterName: string, option: OsFilterOption): void {
|
||||
const filter = this.filterDefinitions.find(f => f.property === filterName);
|
||||
protected removeFilterOption(filterProperty: string, option: OsFilterOption): void {
|
||||
const filter = this.filterDefinitions.find(f => f.property === filterProperty);
|
||||
if (filter) {
|
||||
const filterOption = filter.options.find(
|
||||
o => typeof o !== 'string' && o.condition === option.condition
|
||||
) as OsFilterOption;
|
||||
if (filterOption && filterOption.isActive) {
|
||||
filterOption.isActive = false;
|
||||
filter.count -= 1;
|
||||
this.filteredData = this.filterData(this.currentRawData);
|
||||
this.filterDataOutput.next(this.filteredData);
|
||||
this.setStorageDefinition();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles a filter option, to be called after a checkbox state has changed.
|
||||
* @param filterName
|
||||
* @param option
|
||||
*/
|
||||
public toggleFilterOption(filterName: string, option: OsFilterOption): void {
|
||||
option.isActive ? this.removeFilterOption(filterName, option) : this.addFilterOption(filterName, option);
|
||||
}
|
||||
|
||||
public updateFilterDefinitions(filters: OsFilter[]): void {
|
||||
this.loadStorageDefinition(filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the currently saved filter definition from the StorageService,
|
||||
* check their match with current definitions and set the current filter
|
||||
* @param definitions: Currently defined Filter definitions
|
||||
*/
|
||||
protected loadStorageDefinition(definitions: OsFilter[]): void {
|
||||
if (!definitions || !definitions.length) {
|
||||
return;
|
||||
}
|
||||
const me = this;
|
||||
this.store.get('filter_' + this.name).then(
|
||||
function(storedData: { name: string; data: OsFilter[] }): void {
|
||||
const storedFilters = storedData && storedData.data ? storedData.data : [];
|
||||
definitions.forEach(definedFilter => {
|
||||
const matchingStoreFilter = storedFilters.find(f => f.property === definedFilter.property);
|
||||
let count = 0;
|
||||
definedFilter.options.forEach(option => {
|
||||
if (typeof option === 'string') {
|
||||
return;
|
||||
}
|
||||
if (matchingStoreFilter && matchingStoreFilter.options) {
|
||||
const storedOption = matchingStoreFilter.options.find(
|
||||
o =>
|
||||
typeof o !== 'string' &&
|
||||
(o.condition === option.condition ||
|
||||
(Array.isArray(o.condition) &&
|
||||
Array.isArray(option.condition) &&
|
||||
o.label === option.label))
|
||||
) as OsFilterOption;
|
||||
if (storedOption) {
|
||||
option.isActive = storedOption.isActive;
|
||||
}
|
||||
}
|
||||
if (option.isActive) {
|
||||
count += 1;
|
||||
}
|
||||
});
|
||||
definedFilter.count = count;
|
||||
});
|
||||
me.filterDefinitions = definitions;
|
||||
me.filteredData = me.filterData(me.currentRawData);
|
||||
me.filterDataOutput.next(me.filteredData);
|
||||
},
|
||||
function(error: any): void {
|
||||
me.filteredData = me.filterData(me.currentRawData);
|
||||
me.filterDataOutput.next(me.filteredData);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current filter definitions via StorageService
|
||||
*/
|
||||
private setStorageDefinition(): void {
|
||||
this.store.set('filter_' + this.name, {
|
||||
name: 'filter_' + this.name,
|
||||
data: this.filterDefinitions
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an array of data and applies current filters
|
||||
*/
|
||||
protected filterData(data: V[]): V[] {
|
||||
const filteredData = [];
|
||||
if (!data) {
|
||||
return filteredData;
|
||||
}
|
||||
if (!this.filterDefinitions || !this.filterDefinitions.length) {
|
||||
return data;
|
||||
}
|
||||
data.forEach(newItem => {
|
||||
let excluded = false;
|
||||
for (const filter of this.filterDefinitions) {
|
||||
if (filter.count && !this.checkIncluded(newItem, filter)) {
|
||||
excluded = true;
|
||||
break;
|
||||
if (filter.count) {
|
||||
filter.count -= 1;
|
||||
}
|
||||
}
|
||||
if (!excluded) {
|
||||
filteredData.push(newItem);
|
||||
}
|
||||
});
|
||||
return filteredData;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given ViewBaseModel passes the filter.
|
||||
*
|
||||
* @param item
|
||||
* @param filter
|
||||
* @returns true if the item is to be dispalyed according to the filter
|
||||
* @param item Usually a view model
|
||||
* @param filter The filter to check
|
||||
* @returns true if the item is to be displayed according to the filter
|
||||
*/
|
||||
private checkIncluded(item: V, filter: OsFilter): boolean {
|
||||
const nullFilter = filter.options.find(
|
||||
@ -380,22 +425,30 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
|
||||
|
||||
/**
|
||||
* Removes all active options of a given filter, clearing it
|
||||
*
|
||||
* @param filter
|
||||
* @param update
|
||||
*/
|
||||
public clearFilter(filter: OsFilter): void {
|
||||
public clearFilter(filter: OsFilter, update: boolean = true): void {
|
||||
filter.options.forEach(option => {
|
||||
if (typeof option === 'object' && option.isActive) {
|
||||
this.removeFilterOption(filter.property, option);
|
||||
}
|
||||
});
|
||||
if (update) {
|
||||
this.storeActiveFilters();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all filters currently in use from this filterService
|
||||
*/
|
||||
public clearAllFilters(): void {
|
||||
this.filterDefinitions.forEach(filter => {
|
||||
this.clearFilter(filter);
|
||||
});
|
||||
if (this.filterDefinitions && this.filterDefinitions.length) {
|
||||
this.filterDefinitions.forEach(filter => {
|
||||
this.clearFilter(filter, false);
|
||||
});
|
||||
this.storeActiveFilters();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,155 +1,191 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { BaseViewModel } from '../../site/base/base-view-model';
|
||||
import { BehaviorSubject, Subscription, Observable } from 'rxjs';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { BaseViewModel } from '../../site/base/base-view-model';
|
||||
import { StorageService } from '../core-services/storage.service';
|
||||
|
||||
/**
|
||||
* Describes the sorting columns of an associated ListView, and their state.
|
||||
*/
|
||||
export interface OsSortingDefinition<V> {
|
||||
sortProperty: keyof V;
|
||||
sortAscending?: boolean;
|
||||
options: OsSortingItem<V>[];
|
||||
sortAscending: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A sorting property (data may be a string, a number, a function, or an object
|
||||
* with a toString method) to sort after. Sorting will be done in {@link filterData}
|
||||
*/
|
||||
export interface OsSortingItem<V> {
|
||||
export interface OsSortingOption<V> {
|
||||
property: keyof V;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
/**
|
||||
* Base class for generic sorting purposes
|
||||
*/
|
||||
export abstract class BaseSortListService<V extends BaseViewModel> {
|
||||
/**
|
||||
* Observable output that submits the newly sorted data each time a sorting has been done
|
||||
*/
|
||||
public sortedData = new BehaviorSubject<V[]>([]);
|
||||
|
||||
/**
|
||||
* The data to be sorted. See also the setter for {@link data}
|
||||
*/
|
||||
private unsortedData: V[];
|
||||
private inputData: V[];
|
||||
|
||||
/**
|
||||
* Subscription for the inputData list.
|
||||
* Acts as an semaphore for new filtered data
|
||||
*/
|
||||
private inputDataSubscription: Subscription | null;
|
||||
|
||||
/**
|
||||
* Observable output that submits the newly sorted data each time a sorting has been done
|
||||
*/
|
||||
private outputSubject = new BehaviorSubject<V[]>([]);
|
||||
|
||||
/**
|
||||
* @returns the sorted output subject as observable
|
||||
*/
|
||||
public get outputObservable(): Observable<V[]> {
|
||||
return this.outputSubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* The current sorting definitions
|
||||
*/
|
||||
public sortOptions: OsSortingDefinition<V>;
|
||||
private sortDefinition: OsSortingDefinition<V>;
|
||||
|
||||
/**
|
||||
* used for the key in the StorageService to save/load the correct sorting definitions.
|
||||
*/
|
||||
protected name: string;
|
||||
|
||||
/**
|
||||
* The sorting function according to current settings. Set via {@link updateSortFn}.
|
||||
* The sorting function according to current settings.
|
||||
*/
|
||||
private sortFn: (a: V, b: V) => number;
|
||||
|
||||
/**
|
||||
* default sorting to use if the client was not initialized before
|
||||
*/
|
||||
protected defaultSorting: keyof V = 'id';
|
||||
|
||||
/**
|
||||
* Constructor. Does nothing. TranslateService is used for localeCompeare.
|
||||
*/
|
||||
public constructor(protected translate: TranslateService, private store: StorageService) {}
|
||||
|
||||
/**
|
||||
* Put an array of data that you want sorted.
|
||||
*/
|
||||
public set data(data: V[]) {
|
||||
this.unsortedData = data;
|
||||
this.doAsyncSorting();
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the sorting properties, and returns an observable with sorted data
|
||||
* @param name arbitrary name, used to save/load correct saved settings from StorageService
|
||||
* @param definitions The definitions of the possible options
|
||||
*/
|
||||
public sort(): BehaviorSubject<V[]> {
|
||||
this.loadStorageDefinition();
|
||||
this.updateSortFn();
|
||||
return this.sortedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current sorting order
|
||||
*
|
||||
* @param ascending ascending sorting if true, descending sorting if false
|
||||
*/
|
||||
public set ascending(ascending: boolean) {
|
||||
this.sortOptions.sortAscending = ascending;
|
||||
this.updateSortFn();
|
||||
this.saveStorageDefinition();
|
||||
this.doAsyncSorting();
|
||||
this.sortDefinition.sortAscending = ascending;
|
||||
this.updateSortDefinitions();
|
||||
}
|
||||
|
||||
/**
|
||||
* get the current sorting order
|
||||
* @param returns wether current the sorting is ascending or descending
|
||||
*/
|
||||
public get ascending(): boolean {
|
||||
return this.sortOptions.sortAscending;
|
||||
return this.sortDefinition.sortAscending;
|
||||
}
|
||||
|
||||
/**
|
||||
* set the property of the viewModel the sorting will be based on.
|
||||
* If the property stays the same, only the sort direction will be toggled,
|
||||
* new sortProperty will result in an ascending order.
|
||||
*
|
||||
* @param property a part of a view model
|
||||
*/
|
||||
public set sortProperty(property: string) {
|
||||
if (this.sortOptions.sortProperty === (property as keyof V)) {
|
||||
public set sortProperty(property: keyof V) {
|
||||
if (this.sortDefinition.sortProperty === property) {
|
||||
this.ascending = !this.ascending;
|
||||
this.updateSortFn();
|
||||
} else {
|
||||
this.sortOptions.sortProperty = property as keyof V;
|
||||
this.sortOptions.sortAscending = true;
|
||||
this.updateSortFn();
|
||||
this.doAsyncSorting();
|
||||
this.sortDefinition.sortProperty = property;
|
||||
this.sortDefinition.sortAscending = true;
|
||||
}
|
||||
this.saveStorageDefinition();
|
||||
this.updateSortDefinitions();
|
||||
}
|
||||
|
||||
/**
|
||||
* get the property of the viewModel the sorting is based on.
|
||||
* @returns the current sorting property
|
||||
*/
|
||||
public get sortProperty(): string {
|
||||
return this.sortOptions ? (this.sortOptions.sortProperty as string) : '';
|
||||
public get sortProperty(): keyof V {
|
||||
return this.sortDefinition.sortProperty;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns wether sorting is active or not
|
||||
*/
|
||||
public get isActive(): boolean {
|
||||
return this.sortOptions && this.sortOptions.options.length > 0;
|
||||
return this.sortDefinition && this.sortOptions.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce children to implement sortOptions
|
||||
*/
|
||||
public abstract sortOptions: OsSortingOption<V>[];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param name the name of the sort view, required for store access
|
||||
* @param translate required for language sensitive comparing
|
||||
* @param store to save and load sorting preferences
|
||||
*/
|
||||
public constructor(protected name: string, protected translate: TranslateService, private store: StorageService) {}
|
||||
|
||||
/**
|
||||
* Enforce children to implement a method that returns the fault sorting
|
||||
*/
|
||||
protected abstract async getDefaultDefinition(): Promise<OsSortingDefinition<V>>;
|
||||
|
||||
/**
|
||||
* Defines the sorting properties, and returns an observable with sorted data
|
||||
*
|
||||
* @param name arbitrary name, used to save/load correct saved settings from StorageService
|
||||
* @param definitions The definitions of the possible options
|
||||
*/
|
||||
public async initSorting(inputObservable: Observable<V[]>): Promise<void> {
|
||||
if (this.inputDataSubscription) {
|
||||
this.inputDataSubscription.unsubscribe();
|
||||
this.inputDataSubscription = null;
|
||||
}
|
||||
|
||||
if (!this.sortDefinition) {
|
||||
this.sortDefinition = await this.store.get<OsSortingDefinition<V> | null>('sorting_' + this.name);
|
||||
if (this.sortDefinition && this.sortDefinition.sortProperty) {
|
||||
this.updateSortedData();
|
||||
} else {
|
||||
this.sortDefinition = await this.getDefaultDefinition();
|
||||
this.updateSortDefinitions();
|
||||
}
|
||||
}
|
||||
|
||||
this.inputDataSubscription = inputObservable.subscribe(data => {
|
||||
this.inputData = data;
|
||||
this.updateSortedData();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the property and the sorting direction at the same time
|
||||
* @param property
|
||||
* @param ascending
|
||||
*
|
||||
* @param property a sorting property of a view model
|
||||
* @param ascending ascending or descending
|
||||
*/
|
||||
public setSorting(property: string, ascending: boolean): void {
|
||||
this.sortOptions.sortProperty = property as keyof V;
|
||||
this.sortOptions.sortAscending = ascending;
|
||||
this.saveStorageDefinition();
|
||||
this.updateSortFn();
|
||||
this.doAsyncSorting();
|
||||
public setSorting(property: keyof V, ascending: boolean): void {
|
||||
this.sortDefinition.sortProperty = property;
|
||||
this.sortDefinition.sortAscending = ascending;
|
||||
this.updateSortDefinitions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the currently active icon for an option.
|
||||
*
|
||||
* @param option
|
||||
* @returns the name of the sorting icon, fit to material icon ligatures
|
||||
*/
|
||||
public getSortIcon(option: OsSortingItem<V>): string {
|
||||
if (!this.sortProperty || this.sortProperty !== (option.property as string)) {
|
||||
public getSortIcon(option: OsSortingOption<V>): string {
|
||||
if (this.sortProperty !== option.property) {
|
||||
return '';
|
||||
}
|
||||
return this.ascending ? 'arrow_downward' : 'arrow_upward';
|
||||
}
|
||||
|
||||
public getSortLabel(option: OsSortingItem<V>): string {
|
||||
/**
|
||||
* Determines and returns an untranslated sorting label as string
|
||||
*
|
||||
* @param option The sorting option to a ViewModel
|
||||
* @returns a sorting label as string
|
||||
*/
|
||||
public getSortLabel(option: OsSortingOption<V>): string {
|
||||
if (option.label) {
|
||||
return option.label;
|
||||
}
|
||||
@ -158,51 +194,17 @@ export abstract class BaseSortListService<V extends BaseViewModel> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the currently saved sorting definition from the borwser's
|
||||
* store
|
||||
* Saves the current sorting definitions to the local store
|
||||
*/
|
||||
private async loadStorageDefinition(): Promise<void> {
|
||||
const sorting: OsSortingDefinition<V> | null = await this.store.get('sorting_' + this.name);
|
||||
if (sorting) {
|
||||
if (sorting.sortProperty) {
|
||||
this.sortOptions.sortProperty = sorting.sortProperty;
|
||||
if (sorting.sortAscending !== undefined) {
|
||||
this.sortOptions.sortAscending = sorting.sortAscending;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.sortOptions.sortProperty = this.defaultSorting;
|
||||
this.sortOptions.sortAscending = true;
|
||||
}
|
||||
this.updateSortFn();
|
||||
this.doAsyncSorting();
|
||||
}
|
||||
|
||||
/**
|
||||
* SSaves the current sorting definitions to the local store
|
||||
*/
|
||||
private saveStorageDefinition(): void {
|
||||
this.store.set('sorting_' + this.name, {
|
||||
sortProperty: this.sortProperty,
|
||||
ascending: this.ascending
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* starts sorting, and
|
||||
*/
|
||||
private doAsyncSorting(): Promise<void> {
|
||||
const me = this;
|
||||
return new Promise(function(): void {
|
||||
const data = me.unsortedData.sort(me.sortFn);
|
||||
me.sortedData.next(data);
|
||||
});
|
||||
private updateSortDefinitions(): void {
|
||||
this.updateSortedData();
|
||||
this.store.set('sorting_' + this.name, this.sortDefinition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts an array of data synchronously, using the currently configured sorting
|
||||
*
|
||||
* @param data
|
||||
* @param data Array of ViewModels
|
||||
* @returns the data, sorted with the definitions of this service
|
||||
*/
|
||||
public sortSync(data: V[]): V[] {
|
||||
@ -213,60 +215,62 @@ export abstract class BaseSortListService<V extends BaseViewModel> {
|
||||
* Recreates the sorting function. Is supposed to be called on init and
|
||||
* every time the sorting (property, ascending/descending) or the language changes
|
||||
*/
|
||||
protected updateSortFn(): void {
|
||||
const property = this.sortProperty as string;
|
||||
const ascending = this.ascending;
|
||||
const intl = new Intl.Collator(this.translate.currentLang); // TODO: observe and update sorting on language change
|
||||
|
||||
this.sortFn = function(itemA: V, itemB: V): number {
|
||||
const firstProperty = ascending ? itemA[property] : itemB[property];
|
||||
const secondProperty = ascending ? itemB[property] : itemA[property];
|
||||
if (typeof firstProperty !== typeof secondProperty) {
|
||||
// undefined/null items should always land at the end
|
||||
if (!firstProperty) {
|
||||
return 1;
|
||||
} else if (!secondProperty) {
|
||||
return -1;
|
||||
} else {
|
||||
throw new TypeError('sorting of items failed because of mismatched types');
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
(firstProperty === null || firstProperty === undefined) &&
|
||||
(secondProperty === null || secondProperty === undefined)
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
switch (typeof firstProperty) {
|
||||
case 'boolean':
|
||||
if (firstProperty === false && secondProperty === true) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
case 'number':
|
||||
return firstProperty > secondProperty ? 1 : -1;
|
||||
case 'string':
|
||||
protected updateSortedData(): void {
|
||||
if (this.inputData) {
|
||||
const property = this.sortProperty as string;
|
||||
const intl = new Intl.Collator(this.translate.currentLang);
|
||||
this.outputSubject.next(
|
||||
this.inputData.sort((itemA, itemB) => {
|
||||
const firstProperty = this.ascending ? itemA[property] : itemB[property];
|
||||
const secondProperty = this.ascending ? itemB[property] : itemA[property];
|
||||
if (typeof firstProperty !== typeof secondProperty) {
|
||||
// undefined/null items should always land at the end
|
||||
if (!firstProperty) {
|
||||
return 1;
|
||||
}
|
||||
return intl.compare(firstProperty, secondProperty);
|
||||
case 'function':
|
||||
const a = firstProperty();
|
||||
const b = secondProperty();
|
||||
return intl.compare(a, b);
|
||||
case 'object':
|
||||
if (firstProperty instanceof Date) {
|
||||
return firstProperty > secondProperty ? 1 : -1;
|
||||
} else if (!secondProperty) {
|
||||
return -1;
|
||||
} else {
|
||||
return intl.compare(firstProperty.toString(), secondProperty.toString());
|
||||
throw new TypeError('sorting of items failed because of mismatched types');
|
||||
}
|
||||
case 'undefined':
|
||||
return 1;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
if (
|
||||
(firstProperty === null || firstProperty === undefined) &&
|
||||
(secondProperty === null || secondProperty === undefined)
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
switch (typeof firstProperty) {
|
||||
case 'boolean':
|
||||
if (firstProperty === false && secondProperty === true) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
case 'number':
|
||||
return firstProperty > secondProperty ? 1 : -1;
|
||||
case 'string':
|
||||
if (!firstProperty) {
|
||||
return 1;
|
||||
}
|
||||
return intl.compare(firstProperty, secondProperty);
|
||||
case 'function':
|
||||
const a = firstProperty();
|
||||
const b = secondProperty();
|
||||
return intl.compare(a, b);
|
||||
case 'object':
|
||||
if (firstProperty instanceof Date) {
|
||||
return firstProperty > secondProperty ? 1 : -1;
|
||||
} else {
|
||||
return intl.compare(firstProperty.toString(), secondProperty.toString());
|
||||
}
|
||||
case 'undefined':
|
||||
return 1;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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>
|
@ -1,9 +1,9 @@
|
||||
<mat-accordion (keyup)=checkKeyEvent($event)>
|
||||
<mat-accordion (keyup)="checkKeyEvent($event)">
|
||||
<mat-expansion-panel *ngFor="let filter of service.filterDefinitions">
|
||||
<mat-expansion-panel-header *ngIf="filter.options && filter.options.length">
|
||||
<mat-panel-title>
|
||||
<mat-icon>
|
||||
{{ filter.count ? 'checked' : ''}}
|
||||
{{ filter.count ? 'checked' : '' }}
|
||||
</mat-icon>
|
||||
<span>{{ service.getFilterName(filter) | translate }}</span>
|
||||
</mat-panel-title>
|
||||
@ -12,7 +12,11 @@
|
||||
<mat-action-list class="filtermenu-expanded">
|
||||
<div *ngFor="let option of filter.options">
|
||||
<div *ngIf="isFilter(option)">
|
||||
<mat-checkbox [checked]="option.isActive" (change)="service.toggleFilterOption(filter.property, option)">
|
||||
<mat-checkbox
|
||||
class="filter-title"
|
||||
[checked]="option.isActive"
|
||||
(change)="service.toggleFilterOption(filter.property, option)"
|
||||
>
|
||||
{{ option.label | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
@ -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;
|
||||
}
|
@ -58,6 +58,7 @@ export class FilterMenuComponent implements OnInit {
|
||||
this.dismissed.next(true);
|
||||
}
|
||||
}
|
||||
|
||||
public isFilter(option: OsFilterOption): boolean {
|
||||
return typeof option === 'string' ? false : true;
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
<mat-nav-list class="main-nav">
|
||||
<mat-list-item *ngFor="let option of this.data.sortOptions" (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>
|
@ -1,11 +1,10 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
import { OsSortBottomSheetComponent } from './os-sort-bottom-sheet.component';
|
||||
import { SortBottomSheetComponent } from './sort-bottom-sheet.component';
|
||||
|
||||
describe('OsSortBottomSheetComponent', () => {
|
||||
// let component: OsSortBottomSheetComponent<any>;
|
||||
let fixture: ComponentFixture<OsSortBottomSheetComponent<any>>;
|
||||
describe('SortBottomSheetComponent', () => {
|
||||
let fixture: ComponentFixture<SortBottomSheetComponent<any>>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@ -14,7 +13,7 @@ describe('OsSortBottomSheetComponent', () => {
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(OsSortBottomSheetComponent);
|
||||
fixture = TestBed.createComponent(SortBottomSheetComponent);
|
||||
// component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
@ -9,17 +9,17 @@ import { BaseViewModel } from 'app/site/base/base-view-model';
|
||||
* usage:
|
||||
* ```
|
||||
* @ViewChild('sortBottomSheet')
|
||||
* public sortBottomSheet: OsSortBottomSheetComponent<V>;
|
||||
* public sortBottomSheet: SortBottomSheetComponent<V>;
|
||||
* ...
|
||||
* this.bottomSheet.open(OsSortBottomSheetComponent, { data: SortService });
|
||||
* this.bottomSheet.open(SortBottomSheetComponent, { data: SortService });
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-sort-bottom-sheet',
|
||||
templateUrl: './os-sort-bottom-sheet.component.html',
|
||||
styleUrls: ['./os-sort-bottom-sheet.component.scss']
|
||||
templateUrl: './sort-bottom-sheet.component.html',
|
||||
styleUrls: ['./sort-bottom-sheet.component.scss']
|
||||
})
|
||||
export class OsSortBottomSheetComponent<V extends BaseViewModel> implements OnInit {
|
||||
export class SortBottomSheetComponent<V extends BaseViewModel> implements OnInit {
|
||||
/**
|
||||
* Constructor. Gets a reference to itself (for closing after interaction)
|
||||
* @param data
|
||||
@ -31,10 +31,10 @@ export class OsSortBottomSheetComponent<V extends BaseViewModel> implements OnIn
|
||||
) {}
|
||||
|
||||
/**
|
||||
* init fucntion. Closes inmediately if no sorting is available.
|
||||
* init function. Closes immediately if no sorting is available.
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
if (!this.data || !this.data.sortOptions || !this.data.sortOptions.options.length) {
|
||||
if (!this.data || !this.data.sortOptions || !this.data.sortOptions.length) {
|
||||
throw new Error('No sorting available for a sorting list');
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
<div class="custom-table-header flex-spaced on-transition-fade">
|
||||
<div class="filter-count" *ngIf="filterService">
|
||||
<span>{{ displayedCount }} </span><span translate>of</span>
|
||||
<span> {{ filterService.totalCount }}</span>
|
||||
<span> {{ totalCount }}</span>
|
||||
<span *ngIf="extraItemInfo"> · {{ extraItemInfo }}</span>
|
||||
</div>
|
||||
<div class="current-filters" *ngIf="filterService && filterService.activeFilterCount">
|
||||
@ -61,12 +61,9 @@
|
||||
<!-- non-mobile sorting menu -->
|
||||
<mat-menu #menu>
|
||||
<div *ngIf="hasSorting">
|
||||
<mat-list-item
|
||||
*ngFor="let option of sortService.sortOptions.options"
|
||||
(click)="sortService.sortProperty = option.property"
|
||||
>
|
||||
<mat-list-item *ngFor="let option of sortOptions" (click)="sortOption = option">
|
||||
<button mat-menu-item>
|
||||
<mat-icon>{{ sortService.getSortIcon(option) }}</mat-icon>
|
||||
<mat-icon>{{ getSortIcon(option) }}</mat-icon>
|
||||
<span>{{ sortService.getSortLabel(option) | translate }}</span>
|
||||
</button>
|
||||
</mat-list-item>
|
@ -1,10 +1,10 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
import { OsSortFilterBarComponent } from './os-sort-filter-bar.component';
|
||||
import { SortFilterBarComponent } from './sort-filter-bar.component';
|
||||
|
||||
describe('OsSortFilterBarComponent', () => {
|
||||
let component: OsSortFilterBarComponent<any>;
|
||||
let component: SortFilterBarComponent<any>;
|
||||
let fixture: ComponentFixture<any>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
@ -14,7 +14,7 @@ describe('OsSortFilterBarComponent', () => {
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(OsSortFilterBarComponent);
|
||||
fixture = TestBed.createComponent(SortFilterBarComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
@ -4,9 +4,9 @@ import { MatBottomSheet } from '@angular/material';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { BaseViewModel } from 'app/site/base/base-view-model';
|
||||
import { OsSortBottomSheetComponent } from './os-sort-bottom-sheet/os-sort-bottom-sheet.component';
|
||||
import { SortBottomSheetComponent } from './sort-bottom-sheet/sort-bottom-sheet.component';
|
||||
import { FilterMenuComponent } from './filter-menu/filter-menu.component';
|
||||
import { OsSortingItem } from 'app/core/ui-services/base-sort-list.service';
|
||||
import { OsSortingOption } from 'app/core/ui-services/base-sort-list.service';
|
||||
import { BaseSortListService } from 'app/core/ui-services/base-sort-list.service';
|
||||
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||
import { BaseFilterListService } from 'app/core/ui-services/base-filter-list.service';
|
||||
@ -27,10 +27,10 @@ import { BaseFilterListService } from 'app/core/ui-services/base-filter-list.ser
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-sort-filter-bar',
|
||||
templateUrl: './os-sort-filter-bar.component.html',
|
||||
styleUrls: ['./os-sort-filter-bar.component.scss']
|
||||
templateUrl: './sort-filter-bar.component.html',
|
||||
styleUrls: ['./sort-filter-bar.component.scss']
|
||||
})
|
||||
export class OsSortFilterBarComponent<V extends BaseViewModel> {
|
||||
export class SortFilterBarComponent<V extends BaseViewModel> {
|
||||
/**
|
||||
* The currently active sorting service for the list view
|
||||
*/
|
||||
@ -58,6 +58,7 @@ export class OsSortFilterBarComponent<V extends BaseViewModel> {
|
||||
|
||||
@Output()
|
||||
public searchFieldChange = new EventEmitter<string>();
|
||||
|
||||
/**
|
||||
* The filter side drawer
|
||||
*/
|
||||
@ -68,7 +69,7 @@ export class OsSortFilterBarComponent<V extends BaseViewModel> {
|
||||
* The bottom sheet used to alter sorting in mobile view
|
||||
*/
|
||||
@ViewChild('sortBottomSheet')
|
||||
public sortBottomSheet: OsSortBottomSheetComponent<V>;
|
||||
public sortBottomSheet: SortBottomSheetComponent<V>;
|
||||
|
||||
/**
|
||||
* The 'opened/active' state of the fulltext filter input field
|
||||
@ -87,6 +88,21 @@ export class OsSortFilterBarComponent<V extends BaseViewModel> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the total count of potential filters
|
||||
*/
|
||||
public get totalCount(): number {
|
||||
return this.filterService.unfilteredCount;
|
||||
}
|
||||
|
||||
public get sortOptions(): any {
|
||||
return this.sortService.sortOptions;
|
||||
}
|
||||
|
||||
public set sortOption(option: OsSortingOption<V>) {
|
||||
this.sortService.sortProperty = option.property;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor. Also creates a filtermenu component and a bottomSheet
|
||||
* @param translate
|
||||
@ -106,7 +122,7 @@ export class OsSortFilterBarComponent<V extends BaseViewModel> {
|
||||
*/
|
||||
public openSortDropDown(): void {
|
||||
if (this.vp.isMobile) {
|
||||
const bottomSheetRef = this.bottomSheet.open(OsSortBottomSheetComponent, { data: this.sortService });
|
||||
const bottomSheetRef = this.bottomSheet.open(SortBottomSheetComponent, { data: this.sortService });
|
||||
bottomSheetRef.afterDismissed().subscribe(result => {
|
||||
if (result) {
|
||||
this.sortService.sortProperty = result;
|
||||
@ -136,23 +152,18 @@ export class OsSortFilterBarComponent<V extends BaseViewModel> {
|
||||
|
||||
/**
|
||||
* Checks if there is an active FilterService present
|
||||
* @returns wether the filters are present or not
|
||||
*/
|
||||
public get hasFilters(): boolean {
|
||||
if (this.filterService && this.filterService.hasFilterOptions) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return this.filterService && this.filterService.hasFilterOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the currently active icon for an option.
|
||||
* @param option
|
||||
*/
|
||||
public getSortIcon(option: OsSortingItem<V>): string {
|
||||
if (this.sortService.sortProperty !== option.property) {
|
||||
return '';
|
||||
}
|
||||
return this.sortService.ascending ? 'arrow_downward' : 'arrow_upward';
|
||||
public getSortIcon(option: OsSortingOption<V>): string {
|
||||
return this.sortService.getSortIcon(option);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -160,7 +171,7 @@ export class OsSortFilterBarComponent<V extends BaseViewModel> {
|
||||
* the property is used.
|
||||
* @param option
|
||||
*/
|
||||
public getSortLabel(option: OsSortingItem<V>): string {
|
||||
public getSortLabel(option: OsSortingOption<V>): string {
|
||||
if (option.label) {
|
||||
return option.label;
|
||||
}
|
@ -68,9 +68,9 @@ import { PromptDialogComponent } from './components/prompt-dialog/prompt-dialog.
|
||||
import { SortingListComponent } from './components/sorting-list/sorting-list.component';
|
||||
import { SortingTreeComponent } from './components/sorting-tree/sorting-tree.component';
|
||||
import { ChoiceDialogComponent } from './components/choice-dialog/choice-dialog.component';
|
||||
import { OsSortFilterBarComponent } from './components/os-sort-filter-bar/os-sort-filter-bar.component';
|
||||
import { OsSortBottomSheetComponent } from './components/os-sort-filter-bar/os-sort-bottom-sheet/os-sort-bottom-sheet.component';
|
||||
import { FilterMenuComponent } from './components/os-sort-filter-bar/filter-menu/filter-menu.component';
|
||||
import { SortFilterBarComponent } from './components/sort-filter-bar/sort-filter-bar.component';
|
||||
import { SortBottomSheetComponent } from './components/sort-filter-bar/sort-bottom-sheet/sort-bottom-sheet.component';
|
||||
import { FilterMenuComponent } from './components/sort-filter-bar/filter-menu/filter-menu.component';
|
||||
import { LogoComponent } from './components/logo/logo.component';
|
||||
import { C4DialogComponent, CopyrightSignComponent } from './components/copyright-sign/copyright-sign.component';
|
||||
import { ProjectorButtonComponent } from './components/projector-button/projector-button.component';
|
||||
@ -191,7 +191,7 @@ import { PrecisionPipe } from './pipes/precision.pipe';
|
||||
SortingListComponent,
|
||||
EditorModule,
|
||||
SortingTreeComponent,
|
||||
OsSortFilterBarComponent,
|
||||
SortFilterBarComponent,
|
||||
LogoComponent,
|
||||
CopyrightSignComponent,
|
||||
C4DialogComponent,
|
||||
@ -220,8 +220,8 @@ import { PrecisionPipe } from './pipes/precision.pipe';
|
||||
SortingListComponent,
|
||||
SortingTreeComponent,
|
||||
ChoiceDialogComponent,
|
||||
OsSortFilterBarComponent,
|
||||
OsSortBottomSheetComponent,
|
||||
SortFilterBarComponent,
|
||||
SortBottomSheetComponent,
|
||||
FilterMenuComponent,
|
||||
LogoComponent,
|
||||
CopyrightSignComponent,
|
||||
@ -241,10 +241,10 @@ import { PrecisionPipe } from './pipes/precision.pipe';
|
||||
SearchValueSelectorComponent,
|
||||
SortingListComponent,
|
||||
SortingTreeComponent,
|
||||
OsSortFilterBarComponent,
|
||||
OsSortBottomSheetComponent,
|
||||
SortFilterBarComponent,
|
||||
SortBottomSheetComponent,
|
||||
DecimalPipe
|
||||
],
|
||||
entryComponents: [OsSortBottomSheetComponent, C4DialogComponent]
|
||||
entryComponents: [SortBottomSheetComponent, C4DialogComponent]
|
||||
})
|
||||
export class SharedModule {}
|
||||
|
@ -30,7 +30,8 @@ import { StorageService } from 'app/core/core-services/storage.service';
|
||||
templateUrl: './agenda-list.component.html',
|
||||
styleUrls: ['./agenda-list.component.scss']
|
||||
})
|
||||
export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item> implements OnInit {
|
||||
export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item, ItemRepositoryService>
|
||||
implements OnInit {
|
||||
/**
|
||||
* Determine the display columns in desktop view
|
||||
*/
|
||||
@ -106,7 +107,7 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item> i
|
||||
private agendaPdfService: AgendaPdfService,
|
||||
private pdfService: PdfDocumentService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar, route, storage, filterService);
|
||||
super(titleService, translate, matSnackBar, repo, route, storage, filterService);
|
||||
|
||||
// activate multiSelect mode for this listview
|
||||
this.canMultiSelect = true;
|
||||
@ -125,14 +126,6 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem, Item> i
|
||||
this.setFulltextFilter();
|
||||
}
|
||||
|
||||
protected onFilter(): void {
|
||||
this.filterService.filter().subscribe(newAgendaItems => {
|
||||
newAgendaItems.sort((a, b) => a.weight - b.weight);
|
||||
this.dataSource.data = newAgendaItems;
|
||||
this.checkSelection();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Links to the content object.
|
||||
*
|
||||
|
@ -1,32 +1,34 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { auditTime, map } from 'rxjs/operators';
|
||||
|
||||
import { BaseFilterListService, OsFilter, OsFilterOption } from 'app/core/ui-services/base-filter-list.service';
|
||||
import { itemVisibilityChoices } from 'app/shared/models/agenda/item';
|
||||
import { ViewItem } from '../models/view-item';
|
||||
import { StorageService } from 'app/core/core-services/storage.service';
|
||||
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
|
||||
|
||||
/**
|
||||
* Filter the agenda list
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AgendaFilterListService extends BaseFilterListService<ViewItem> {
|
||||
protected name = 'Agenda';
|
||||
|
||||
public filterOptions: OsFilter[] = [];
|
||||
|
||||
/**
|
||||
* Constructor. Also creates the dynamic filter options
|
||||
*
|
||||
* @param store
|
||||
* @param repo
|
||||
* @param translate Translation service
|
||||
*/
|
||||
public constructor(store: StorageService, repo: ItemRepositoryService, private translate: TranslateService) {
|
||||
super(store, repo);
|
||||
this.filterOptions = [
|
||||
public constructor(store: StorageService, private translate: TranslateService) {
|
||||
super('Agenda', store);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the filter definition
|
||||
*/
|
||||
protected getFilterDefinitions(): OsFilter[] {
|
||||
return [
|
||||
{
|
||||
label: 'Visibility',
|
||||
property: 'type',
|
||||
@ -41,37 +43,26 @@ export class AgendaFilterListService extends BaseFilterListService<ViewItem> {
|
||||
]
|
||||
}
|
||||
];
|
||||
this.updateFilterDefinitions(this.filterOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @override from base filter list service: Added custom filtering of items
|
||||
* Initializes the filterService. Returns the filtered data as Observable
|
||||
* @override from base filter list service
|
||||
*
|
||||
* @returns the list of ViewItems without the types
|
||||
*/
|
||||
public filter(): Observable<ViewItem[]> {
|
||||
this.repo
|
||||
.getViewModelListObservable()
|
||||
.pipe(auditTime(10))
|
||||
// Exclude items that are just there to provide a list of speakers. They have many
|
||||
// restricted fields and must not be shown in the agenda!
|
||||
.pipe(map(itemList => itemList.filter(item => item.type !== undefined)))
|
||||
.subscribe(data => {
|
||||
this.currentRawData = data;
|
||||
this.filteredData = this.filterData(data);
|
||||
this.filterDataOutput.next(this.filteredData);
|
||||
});
|
||||
this.loadStorageDefinition(this.filterDefinitions);
|
||||
return this.filterDataOutput;
|
||||
protected preFilter(viewItems: ViewItem[]): ViewItem[] {
|
||||
return viewItems.filter(item => item.type !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* helper function to create options for visibility filters
|
||||
*
|
||||
* @returns a list of choices to filter from
|
||||
*/
|
||||
private createVisibilityFilterOptions(): OsFilterOption[] {
|
||||
const options = [];
|
||||
itemVisibilityChoices.forEach(choice => {
|
||||
options.push({
|
||||
condition: choice.key as number,
|
||||
label: choice.name
|
||||
});
|
||||
});
|
||||
return options;
|
||||
return itemVisibilityChoices.map(choice => ({
|
||||
condition: choice.key as number,
|
||||
label: choice.name
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,9 @@ import { ViewAssignment, AssignmentPhases } from '../../models/view-assignment';
|
||||
templateUrl: './assignment-list.component.html',
|
||||
styleUrls: ['./assignment-list.component.scss']
|
||||
})
|
||||
export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignment, Assignment> implements OnInit {
|
||||
export class AssignmentListComponent
|
||||
extends ListViewBaseComponent<ViewAssignment, Assignment, AssignmentRepositoryService>
|
||||
implements OnInit {
|
||||
/**
|
||||
* The different phases of an assignment. Info is fetched from server
|
||||
*/
|
||||
@ -57,7 +59,7 @@ export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignmen
|
||||
private router: Router,
|
||||
public operator: OperatorService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar, route, storage, filterService, sortService);
|
||||
super(titleService, translate, matSnackBar, repo, route, storage, filterService, sortService);
|
||||
// activate multiSelect mode for this list view
|
||||
this.canMultiSelect = true;
|
||||
}
|
||||
|
@ -1,53 +1,45 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
|
||||
import { BaseFilterListService, OsFilter } from 'app/core/ui-services/base-filter-list.service';
|
||||
import { BaseFilterListService, OsFilter, OsFilterOption } from 'app/core/ui-services/base-filter-list.service';
|
||||
import { StorageService } from 'app/core/core-services/storage.service';
|
||||
import { ViewAssignment, AssignmentPhases } from '../models/view-assignment';
|
||||
|
||||
/**
|
||||
* Filter service for the assignment list
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AssignmentFilterListService extends BaseFilterListService<ViewAssignment> {
|
||||
protected name = 'Assignment';
|
||||
|
||||
/**
|
||||
* Getter for the current filter options
|
||||
*
|
||||
* @returns filter definitions to use
|
||||
*/
|
||||
public get filterOptions(): OsFilter[] {
|
||||
return [this.phaseFilter];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter for assignment phases. Defined in the servers' constants
|
||||
*/
|
||||
public phaseFilter: OsFilter = {
|
||||
property: 'phase',
|
||||
options: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor. Activates the phase options subscription
|
||||
*
|
||||
* @param store StorageService
|
||||
* @param assignmentRepo Repository
|
||||
* @param constants the openslides constant service to get the assignment options
|
||||
* @param translate translate service
|
||||
*/
|
||||
public constructor(store: StorageService, assignmentRepo: AssignmentRepositoryService) {
|
||||
super(store, assignmentRepo);
|
||||
this.createPhaseOptions();
|
||||
public constructor(store: StorageService) {
|
||||
super('Assignments', store);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to the phases of an assignment that are defined in the server's
|
||||
* constants
|
||||
* @returns the filter definition
|
||||
*/
|
||||
private createPhaseOptions(): void {
|
||||
this.phaseFilter.options = AssignmentPhases.map(ap => {
|
||||
protected getFilterDefinitions(): OsFilter[] {
|
||||
return [
|
||||
{
|
||||
label: 'Phase',
|
||||
property: 'phase',
|
||||
options: this.createPhaseOptions()
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates options for assignment phases
|
||||
*/
|
||||
private createPhaseOptions(): OsFilterOption[] {
|
||||
return AssignmentPhases.map(ap => {
|
||||
return { label: ap.display_name, condition: ap.value, isActive: false };
|
||||
});
|
||||
this.updateFilterDefinitions(this.filterOptions);
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,46 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { BaseSortListService, OsSortingDefinition } from 'app/core/ui-services/base-sort-list.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { BaseSortListService, OsSortingDefinition, OsSortingOption } from 'app/core/ui-services/base-sort-list.service';
|
||||
import { StorageService } from 'app/core/core-services/storage.service';
|
||||
import { ViewAssignment } from '../models/view-assignment';
|
||||
|
||||
/**
|
||||
* Sorting service for the assignment list
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AssignmentSortListService extends BaseSortListService<ViewAssignment> {
|
||||
public sortOptions: OsSortingDefinition<ViewAssignment> = {
|
||||
sortProperty: 'assignment',
|
||||
sortAscending: true,
|
||||
options: [
|
||||
{ property: 'assignment', label: 'Name' },
|
||||
{ property: 'phase', label: 'Phase' },
|
||||
{ property: 'candidateAmount', label: 'Number of candidates' }
|
||||
]
|
||||
};
|
||||
protected name = 'Assignment';
|
||||
/**
|
||||
* Define the sort options
|
||||
*/
|
||||
public sortOptions: OsSortingOption<ViewAssignment>[] = [
|
||||
{ property: 'assignment', label: 'Name' },
|
||||
{ property: 'phase', label: 'Phase' },
|
||||
{ property: 'candidateAmount', label: 'Number of candidates' }
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param translate required by parent
|
||||
* @param storage required by parent
|
||||
*/
|
||||
public constructor(translate: TranslateService, storage: StorageService) {
|
||||
super('Assignment', translate, storage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Required by parent
|
||||
*
|
||||
* @returns the default sorting strategy
|
||||
*/
|
||||
public async getDefaultDefinition(): Promise<OsSortingDefinition<ViewAssignment>> {
|
||||
return {
|
||||
sortProperty: 'assignment',
|
||||
sortAscending: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -10,9 +10,14 @@ import { BaseSortListService } from 'app/core/ui-services/base-sort-list.service
|
||||
import { BaseFilterListService } from 'app/core/ui-services/base-filter-list.service';
|
||||
import { BaseModel } from 'app/shared/models/base/base-model';
|
||||
import { StorageService } from 'app/core/core-services/storage.service';
|
||||
import { BaseRepository } from 'app/core/repositories/base-repository';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export abstract class ListViewBaseComponent<V extends BaseViewModel, M extends BaseModel> extends BaseViewComponent
|
||||
implements OnDestroy {
|
||||
export abstract class ListViewBaseComponent<
|
||||
V extends BaseViewModel,
|
||||
M extends BaseModel,
|
||||
R extends BaseRepository<V, M>
|
||||
> extends BaseViewComponent implements OnDestroy {
|
||||
/**
|
||||
* The data source for a table. Requires to be initialized with a BaseViewModel
|
||||
*/
|
||||
@ -76,21 +81,24 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel, M extends B
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for list view bases
|
||||
* @param titleService the title serivce
|
||||
* @param translate the translate service
|
||||
* @param matSnackBar showing errors
|
||||
* @param filterService filter
|
||||
* @param sortService sorting
|
||||
* @param viewModelRepo Repository for the view Model. Do NOT rename to "repo"
|
||||
* @param route Access the current route
|
||||
* @param storage Access the store
|
||||
* @param modelFilterListService filter do NOT rename to "filterListService"
|
||||
* @param modelSortService sorting do NOT rename to "sortService"
|
||||
*/
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
translate: TranslateService,
|
||||
matSnackBar: MatSnackBar,
|
||||
protected viewModelRepo: R,
|
||||
protected route?: ActivatedRoute,
|
||||
protected storage?: StorageService,
|
||||
public filterService?: BaseFilterListService<V>,
|
||||
public sortService?: BaseSortListService<V>
|
||||
protected modelFilterListService?: BaseFilterListService<V>,
|
||||
protected modelSortService?: BaseSortListService<V>
|
||||
) {
|
||||
super(titleService, translate, matSnackBar);
|
||||
this.selectedRows = [];
|
||||
@ -114,40 +122,41 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel, M extends B
|
||||
this.initializePagination();
|
||||
this.dataSource.paginator._intl.itemsPerPageLabel = this.translate.instant('items per page');
|
||||
}
|
||||
if (this.filterService) {
|
||||
this.onFilter();
|
||||
}
|
||||
if (this.sortService) {
|
||||
this.onSort();
|
||||
|
||||
// TODO: Add subscription to this.subscriptions
|
||||
if (this.modelFilterListService && this.modelSortService) {
|
||||
// filtering and sorting
|
||||
this.modelFilterListService.initFilters(this.getModelListObservable());
|
||||
this.modelSortService.initSorting(this.modelFilterListService.outputObservable);
|
||||
this.modelSortService.outputObservable.subscribe(data => this.setDataSource(data));
|
||||
} else if (this.modelFilterListService) {
|
||||
// only filter service
|
||||
this.modelFilterListService.initFilters(this.getModelListObservable());
|
||||
this.modelFilterListService.outputObservable.subscribe(data => this.setDataSource(data));
|
||||
} else if (this.modelSortService) {
|
||||
// only sorting
|
||||
this.modelSortService.initSorting(this.getModelListObservable());
|
||||
this.modelSortService.outputObservable.subscribe(data => this.setDataSource(data));
|
||||
} else {
|
||||
// none of both
|
||||
this.getModelListObservable().subscribe(data => this.setDataSource(data));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard filtering function. Sufficient for most list views but can be overwritten
|
||||
*/
|
||||
protected onFilter(): void {
|
||||
if (this.sortService) {
|
||||
this.subscriptions.push(
|
||||
this.filterService.filter().subscribe(filteredData => (this.sortService.data = filteredData))
|
||||
);
|
||||
} else {
|
||||
this.filterService.filter().subscribe(filteredData => (this.dataSource.data = filteredData));
|
||||
}
|
||||
protected getModelListObservable(): Observable<V[]> {
|
||||
return this.viewModelRepo.getViewModelListObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard sorting function. Sufficient for most list views but can be overwritten
|
||||
*/
|
||||
protected onSort(): void {
|
||||
this.subscriptions.push(
|
||||
this.sortService.sort().subscribe(sortedData => {
|
||||
// the dataArray needs to be cleared (since angular 7)
|
||||
// changes are not detected properly anymore
|
||||
this.dataSource.data = [];
|
||||
this.dataSource.data = sortedData;
|
||||
this.checkSelection();
|
||||
})
|
||||
);
|
||||
private setDataSource(data: V[]): void {
|
||||
// the dataArray needs to be cleared (since angular 7)
|
||||
// changes are not detected properly anymore
|
||||
this.dataSource.data = [];
|
||||
this.dataSource.data = data;
|
||||
|
||||
this.checkSelection();
|
||||
}
|
||||
|
||||
public onSortButton(itemProperty: string): void {
|
||||
|
@ -25,7 +25,8 @@ import { langToLocale } from 'app/shared/utils/lang-to-locale';
|
||||
templateUrl: './history-list.component.html',
|
||||
styleUrls: ['./history-list.component.scss']
|
||||
})
|
||||
export class HistoryListComponent extends ListViewBaseComponent<ViewHistory, History> implements OnInit {
|
||||
export class HistoryListComponent extends ListViewBaseComponent<ViewHistory, History, HistoryRepositoryService>
|
||||
implements OnInit {
|
||||
/**
|
||||
* Subject determine when the custom timestamp subject changes
|
||||
*/
|
||||
@ -51,7 +52,7 @@ export class HistoryListComponent extends ListViewBaseComponent<ViewHistory, His
|
||||
private router: Router,
|
||||
private operator: OperatorService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar);
|
||||
super(titleService, translate, matSnackBar, repo);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -26,7 +26,8 @@ import { StorageService } from 'app/core/core-services/storage.service';
|
||||
templateUrl: './mediafile-list.component.html',
|
||||
styleUrls: ['./mediafile-list.component.scss']
|
||||
})
|
||||
export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile, Mediafile> implements OnInit {
|
||||
export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile, Mediafile, MediafileRepositoryService>
|
||||
implements OnInit {
|
||||
/**
|
||||
* Holds the actions for logos. Updated via an observable
|
||||
*/
|
||||
@ -108,7 +109,7 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile,
|
||||
public sortService: MediafilesSortListService,
|
||||
private operator: OperatorService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar, route, storage, filterService, sortService);
|
||||
super(titleService, translate, matSnackBar, repo, route, storage, filterService, sortService);
|
||||
|
||||
// enables multiSelection for this listView
|
||||
this.canMultiSelect = true;
|
||||
|
@ -1,65 +1,62 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { BaseFilterListService, OsFilter } from 'app/core/ui-services/base-filter-list.service';
|
||||
import { ViewMediafile } from '../models/view-mediafile';
|
||||
import { StorageService } from 'app/core/core-services/storage.service';
|
||||
import { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service';
|
||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||
import { StorageService } from 'app/core/core-services/storage.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ViewMediafile } from '../models/view-mediafile';
|
||||
|
||||
/**
|
||||
* Filter service for media files
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MediafileFilterListService extends BaseFilterListService<ViewMediafile> {
|
||||
protected name = 'Mediafile';
|
||||
|
||||
/**
|
||||
* A filter checking if a file is a pdf or not
|
||||
*/
|
||||
public pdfOption: OsFilter = {
|
||||
property: 'type',
|
||||
label: 'PDF',
|
||||
options: [
|
||||
{
|
||||
condition: 'application/pdf',
|
||||
label: this.translate.instant('Is PDF file')
|
||||
},
|
||||
{
|
||||
condition: null,
|
||||
label: this.translate.instant('Is no PDF file')
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* A filter checking if a file is hidden. Only included if the operator has permission to see hidden files
|
||||
*/
|
||||
public hiddenOptions: OsFilter = {
|
||||
property: 'is_hidden',
|
||||
label: this.translate.instant('Visibility'),
|
||||
options: [
|
||||
{ condition: true, label: this.translate.instant('is hidden') },
|
||||
{ condition: false, label: this.translate.instant('is not hidden') }
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor. Sets the filter options according to permissions
|
||||
* Constructor.
|
||||
* Sets the filter options according to permissions
|
||||
*
|
||||
* @param store
|
||||
* @param repo
|
||||
* @param operator
|
||||
* @param translate
|
||||
*/
|
||||
public constructor(
|
||||
store: StorageService,
|
||||
repo: MediafileRepositoryService,
|
||||
operator: OperatorService,
|
||||
private translate: TranslateService
|
||||
) {
|
||||
super(store, repo);
|
||||
const filterOptions = operator.hasPerms('mediafiles.can_see_hidden')
|
||||
? [this.hiddenOptions, this.pdfOption]
|
||||
: [this.pdfOption];
|
||||
this.updateFilterDefinitions(filterOptions);
|
||||
public constructor(store: StorageService, private operator: OperatorService, private translate: TranslateService) {
|
||||
super('Mediafiles', store);
|
||||
|
||||
this.operator.getUserObservable().subscribe(() => {
|
||||
this.setFilterDefinitions();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the filter definition
|
||||
*/
|
||||
protected getFilterDefinitions(): OsFilter[] {
|
||||
const pdfOption: OsFilter = {
|
||||
property: 'type',
|
||||
label: 'PDF',
|
||||
options: [
|
||||
{
|
||||
condition: 'application/pdf',
|
||||
label: this.translate.instant('Is PDF file')
|
||||
},
|
||||
{
|
||||
condition: null,
|
||||
label: this.translate.instant('Is no PDF file')
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const hiddenOptions: OsFilter = {
|
||||
property: 'is_hidden',
|
||||
label: this.translate.instant('Visibility'),
|
||||
options: [
|
||||
{ condition: true, label: this.translate.instant('is hidden') },
|
||||
{ condition: false, label: this.translate.instant('is not hidden') }
|
||||
]
|
||||
};
|
||||
|
||||
return this.operator.hasPerms('mediafiles.can_see_hidden') ? [hiddenOptions, pdfOption] : [pdfOption];
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +1,48 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { BaseSortListService, OsSortingDefinition } from 'app/core/ui-services/base-sort-list.service';
|
||||
import { BaseSortListService, OsSortingDefinition, OsSortingOption } from 'app/core/ui-services/base-sort-list.service';
|
||||
import { StorageService } from 'app/core/core-services/storage.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ViewMediafile } from '../models/view-mediafile';
|
||||
|
||||
/**
|
||||
* Sorting service for the mediafile list
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MediafilesSortListService extends BaseSortListService<ViewMediafile> {
|
||||
public sortOptions: OsSortingDefinition<ViewMediafile> = {
|
||||
sortProperty: 'title',
|
||||
sortAscending: true,
|
||||
options: [
|
||||
{ property: 'title' },
|
||||
{
|
||||
property: 'type',
|
||||
label: this.translate.instant('Type')
|
||||
},
|
||||
{
|
||||
property: 'size',
|
||||
label: this.translate.instant('Size')
|
||||
}
|
||||
]
|
||||
};
|
||||
protected name = 'Mediafile';
|
||||
public sortOptions: OsSortingOption<ViewMediafile>[] = [
|
||||
{ property: 'title' },
|
||||
{
|
||||
property: 'type',
|
||||
label: this.translate.instant('Type')
|
||||
},
|
||||
{
|
||||
property: 'size',
|
||||
label: this.translate.instant('Size')
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param translate required by parent
|
||||
* @param store required by parent
|
||||
*/
|
||||
public constructor(translate: TranslateService, store: StorageService) {
|
||||
super('Mediafiles', translate, store);
|
||||
}
|
||||
|
||||
/**
|
||||
* Required by parent
|
||||
*
|
||||
* @returns the default sorting strategy
|
||||
*/
|
||||
public async getDefaultDefinition(): Promise<OsSortingDefinition<ViewMediafile>> {
|
||||
return {
|
||||
sortProperty: 'title',
|
||||
sortAscending: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -41,10 +41,10 @@
|
||||
<span translate>Follow recommendations for all motions</span>
|
||||
</button>
|
||||
|
||||
<table class="os-headed-listview-table on-transition-fade" mat-table [dataSource]="dataSource" matSort>
|
||||
<table class="os-headed-listview-table on-transition-fade" mat-table [dataSource]="dataSource">
|
||||
<!-- title column -->
|
||||
<ng-container matColumnDef="title">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header> <span translate>Motion</span> </mat-header-cell>
|
||||
<mat-header-cell *matHeaderCellDef> <span translate>Motion</span> </mat-header-cell>
|
||||
<mat-cell *matCellDef="let motion">
|
||||
{{ motion.getTitle() }}
|
||||
</mat-cell>
|
||||
|
@ -15,6 +15,7 @@ import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { ViewMotion } from 'app/site/motions/models/view-motion';
|
||||
import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
|
||||
import { StorageService } from 'app/core/core-services/storage.service';
|
||||
import { Motion } from 'app/shared/models/motions/motion';
|
||||
|
||||
/**
|
||||
* Detail component to display one motion block
|
||||
@ -24,17 +25,13 @@ import { StorageService } from 'app/core/core-services/storage.service';
|
||||
templateUrl: './motion-block-detail.component.html',
|
||||
styleUrls: ['./motion-block-detail.component.scss']
|
||||
})
|
||||
export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion, MotionBlock> implements OnInit {
|
||||
export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion, Motion, MotionRepositoryService>
|
||||
implements OnInit {
|
||||
/**
|
||||
* Determines the block id from the given URL
|
||||
*/
|
||||
public block: ViewMotionBlock;
|
||||
|
||||
/**
|
||||
* All motions in this block
|
||||
*/
|
||||
public motions: ViewMotion[];
|
||||
|
||||
/**
|
||||
* Determine the edit mode
|
||||
*/
|
||||
@ -67,11 +64,11 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion
|
||||
storage: StorageService,
|
||||
private operator: OperatorService,
|
||||
private router: Router,
|
||||
private repo: MotionBlockRepositoryService,
|
||||
private motionRepo: MotionRepositoryService,
|
||||
protected repo: MotionBlockRepositoryService,
|
||||
protected motionRepo: MotionRepositoryService,
|
||||
private promptService: PromptService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar, route, storage);
|
||||
super(titleService, translate, matSnackBar, motionRepo, route, storage);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -81,28 +78,29 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion
|
||||
public ngOnInit(): void {
|
||||
super.setTitle('Motion block');
|
||||
this.initTable();
|
||||
const blockId = parseInt(this.route.snapshot.params.id, 10);
|
||||
|
||||
this.blockEditForm = new FormGroup({
|
||||
title: new FormControl('', Validators.required)
|
||||
});
|
||||
|
||||
const blockId = +this.route.snapshot.params.id;
|
||||
this.block = this.repo.getViewModel(blockId);
|
||||
|
||||
this.repo.getViewModelObservable(blockId).subscribe(newBlock => {
|
||||
// necessary since the subscription can return undefined
|
||||
if (newBlock) {
|
||||
this.block = newBlock;
|
||||
|
||||
// set the blocks title in the form
|
||||
this.blockEditForm.get('title').setValue(this.block.title);
|
||||
|
||||
this.repo.getViewMotionsByBlock(this.block.motionBlock).subscribe(newMotions => {
|
||||
this.motions = newMotions;
|
||||
this.dataSource.data = this.motions;
|
||||
});
|
||||
}
|
||||
});
|
||||
// pseudo filter
|
||||
this.subscriptions.push(
|
||||
this.repo.getViewModelObservable(blockId).subscribe(newBlock => {
|
||||
if (newBlock) {
|
||||
this.block = newBlock;
|
||||
this.subscriptions.push(
|
||||
this.repo.getViewMotionsByBlock(this.block.motionBlock).subscribe(viewMotions => {
|
||||
if (viewMotions && viewMotions.length) {
|
||||
this.dataSource.data = viewMotions;
|
||||
} else {
|
||||
this.dataSource.data = [];
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -193,8 +191,8 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent<ViewMotion
|
||||
* Following a recommendation implies, that a valid recommendation exists.
|
||||
*/
|
||||
public isFollowingProhibited(): boolean {
|
||||
if (this.motions) {
|
||||
return this.motions.every(motion => motion.isInFinalState() || !motion.recommendation_id);
|
||||
if (this.dataSource.data) {
|
||||
return this.dataSource.data.every(motion => motion.isInFinalState() || !motion.recommendation_id);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
@ -51,10 +51,10 @@
|
||||
|
||||
<!-- Table -->
|
||||
<mat-card class="os-card">
|
||||
<table class="os-headed-listview-table on-transition-fade" mat-table [dataSource]="dataSource" matSort>
|
||||
<table class="os-headed-listview-table on-transition-fade" mat-table [dataSource]="dataSource">
|
||||
<!-- Projector column -->
|
||||
<ng-container matColumnDef="projector">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Projector</mat-header-cell>
|
||||
<mat-header-cell *matHeaderCellDef></mat-header-cell>
|
||||
<mat-cell *matCellDef="let block">
|
||||
<os-projector-button [object]="block"></os-projector-button>
|
||||
</mat-cell>
|
||||
@ -62,13 +62,17 @@
|
||||
|
||||
<!-- title column -->
|
||||
<ng-container matColumnDef="title">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header> <span translate>Title</span> </mat-header-cell>
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
<span translate>Title</span>
|
||||
</mat-header-cell>
|
||||
<mat-cell *matCellDef="let block"> {{ block.title }} </mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- amount column -->
|
||||
<ng-container matColumnDef="amount">
|
||||
<mat-header-cell *matHeaderCellDef> <span translate>Motions</span> </mat-header-cell>
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
<span translate>Motions</span>
|
||||
</mat-header-cell>
|
||||
<mat-cell *matCellDef="let block">
|
||||
<span class="os-amount-chip">{{ getMotionAmount(block.motionBlock) }}</span>
|
||||
</mat-cell>
|
||||
@ -76,7 +80,9 @@
|
||||
|
||||
<!-- menu -->
|
||||
<ng-container matColumnDef="menu">
|
||||
<mat-header-cell *matHeaderCellDef>Menu</mat-header-cell>
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
<span translate>Menu</span>
|
||||
</mat-header-cell>
|
||||
<mat-cell *matCellDef="let block">
|
||||
<button
|
||||
*ngIf="canEdit"
|
||||
|
@ -12,6 +12,7 @@ import { itemVisibilityChoices } from 'app/shared/models/agenda/item';
|
||||
import { ListViewBaseComponent } from 'app/site/base/list-view-base';
|
||||
import { MotionBlock } from 'app/shared/models/motions/motion-block';
|
||||
import { MotionBlockRepositoryService } from 'app/core/repositories/motions/motion-block-repository.service';
|
||||
import { MotionBlockSortService } from 'app/site/motions/services/motion-block-sort.service';
|
||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { StorageService } from 'app/core/core-services/storage.service';
|
||||
@ -26,7 +27,9 @@ import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
|
||||
templateUrl: './motion-block-list.component.html',
|
||||
styleUrls: ['./motion-block-list.component.scss']
|
||||
})
|
||||
export class MotionBlockListComponent extends ListViewBaseComponent<ViewMotionBlock, MotionBlock> implements OnInit {
|
||||
export class MotionBlockListComponent
|
||||
extends ListViewBaseComponent<ViewMotionBlock, MotionBlock, MotionBlockRepositoryService>
|
||||
implements OnInit {
|
||||
/**
|
||||
* Holds the create form
|
||||
*/
|
||||
@ -88,9 +91,10 @@ export class MotionBlockListComponent extends ListViewBaseComponent<ViewMotionBl
|
||||
private formBuilder: FormBuilder,
|
||||
private promptService: PromptService,
|
||||
private itemRepo: ItemRepositoryService,
|
||||
private operator: OperatorService
|
||||
private operator: OperatorService,
|
||||
sortService: MotionBlockSortService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar, route, storage);
|
||||
super(titleService, translate, matSnackBar, repo, route, storage, null, sortService);
|
||||
|
||||
this.createBlockForm = this.formBuilder.group({
|
||||
title: ['', Validators.required],
|
||||
@ -105,14 +109,7 @@ export class MotionBlockListComponent extends ListViewBaseComponent<ViewMotionBl
|
||||
public ngOnInit(): void {
|
||||
super.setTitle('Motion blocks');
|
||||
this.initTable();
|
||||
|
||||
this.items = this.itemRepo.getViewModelListBehaviorSubject();
|
||||
|
||||
this.repo.getViewModelListObservable().subscribe(newMotionblocks => {
|
||||
newMotionblocks.sort((a, b) => (a > b ? 1 : -1));
|
||||
this.dataSource.data = newMotionblocks;
|
||||
});
|
||||
|
||||
this.agendaRepo.getDefaultAgendaVisibility().subscribe(visibility => (this.defaultVisibility = visibility));
|
||||
}
|
||||
|
||||
|
@ -66,7 +66,8 @@ interface InfoDialog {
|
||||
templateUrl: './motion-list.component.html',
|
||||
styleUrls: ['./motion-list.component.scss']
|
||||
})
|
||||
export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motion> implements OnInit {
|
||||
export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motion, MotionRepositoryService>
|
||||
implements OnInit {
|
||||
/**
|
||||
* Reference to the dialog for quick editing meta information.
|
||||
*/
|
||||
@ -130,15 +131,15 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
|
||||
matSnackBar: MatSnackBar,
|
||||
route: ActivatedRoute,
|
||||
storage: StorageService,
|
||||
filterService: MotionFilterListService,
|
||||
sortService: MotionSortListService,
|
||||
public filterService: MotionFilterListService,
|
||||
public sortService: MotionSortListService,
|
||||
private router: Router,
|
||||
private configService: ConfigService,
|
||||
private tagRepo: TagRepositoryService,
|
||||
private motionBlockRepo: MotionBlockRepositoryService,
|
||||
private categoryRepo: CategoryRepositoryService,
|
||||
private workflowRepo: WorkflowRepositoryService,
|
||||
private motionRepo: MotionRepositoryService,
|
||||
protected motionRepo: MotionRepositoryService,
|
||||
private motionCsvExport: MotionCsvExportService,
|
||||
private operator: OperatorService,
|
||||
private pdfExport: MotionPdfExportService,
|
||||
@ -148,7 +149,7 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
|
||||
public perms: LocalPermissionsService,
|
||||
private motionXlsxExport: MotionXlsxExportService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar, route, storage, filterService, sortService);
|
||||
super(titleService, translate, matSnackBar, motionRepo, route, storage, filterService, sortService);
|
||||
|
||||
// enable multiSelect for this listView
|
||||
this.canMultiSelect = true;
|
||||
@ -357,23 +358,6 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
|
||||
return false;
|
||||
}
|
||||
filter = filter ? filter.toLowerCase() : '';
|
||||
if (
|
||||
data.recommendation &&
|
||||
this.translate
|
||||
.instant(data.recommendation.recommendation_label)
|
||||
.toLowerCase()
|
||||
.includes(filter)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
this.translate
|
||||
.instant(data.state.name)
|
||||
.toLowerCase()
|
||||
.includes(filter)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (data.submitters.length && data.submitters.find(user => user.full_name.toLowerCase().includes(filter))) {
|
||||
return true;
|
||||
}
|
||||
@ -387,6 +371,24 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
this.getStateLabel(data) &&
|
||||
this.getStateLabel(data)
|
||||
.toLocaleLowerCase()
|
||||
.includes(filter)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
this.getRecommendationLabel(data) &&
|
||||
this.getRecommendationLabel(data)
|
||||
.toLocaleLowerCase()
|
||||
.includes(filter)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const dataid = '' + data.id;
|
||||
if (dataid.includes(filter)) {
|
||||
return true;
|
||||
|
@ -20,7 +20,8 @@ import { StorageService } from 'app/core/core-services/storage.service';
|
||||
templateUrl: './workflow-list.component.html',
|
||||
styleUrls: ['./workflow-list.component.scss']
|
||||
})
|
||||
export class WorkflowListComponent extends ListViewBaseComponent<ViewWorkflow, Workflow> implements OnInit {
|
||||
export class WorkflowListComponent extends ListViewBaseComponent<ViewWorkflow, Workflow, WorkflowRepositoryService>
|
||||
implements OnInit {
|
||||
/**
|
||||
* Holds the new workflow title
|
||||
*/
|
||||
@ -51,10 +52,10 @@ export class WorkflowListComponent extends ListViewBaseComponent<ViewWorkflow, W
|
||||
storage: StorageService,
|
||||
private dialog: MatDialog,
|
||||
private router: Router,
|
||||
private workflowRepo: WorkflowRepositoryService,
|
||||
protected workflowRepo: WorkflowRepositoryService,
|
||||
private promptService: PromptService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar, route, storage);
|
||||
super(titleService, translate, matSnackBar, workflowRepo, route, storage);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
@ -2,69 +2,52 @@ import { Injectable } from '@angular/core';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { BaseFilterListService, OsFilter, OsFilterOptions } from 'app/core/ui-services/base-filter-list.service';
|
||||
import {
|
||||
BaseFilterListService,
|
||||
OsFilter,
|
||||
OsFilterOptions,
|
||||
OsFilterOption
|
||||
} from 'app/core/ui-services/base-filter-list.service';
|
||||
import { ViewMotion } from '../models/view-motion';
|
||||
import { CategoryRepositoryService } from 'app/core/repositories/motions/category-repository.service';
|
||||
import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-repository.service';
|
||||
import { StorageService } from 'app/core/core-services/storage.service';
|
||||
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
|
||||
import { MotionBlockRepositoryService } from 'app/core/repositories/motions/motion-block-repository.service';
|
||||
import { MotionCommentSectionRepositoryService } from 'app/core/repositories/motions/motion-comment-section-repository.service';
|
||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
import { ViewWorkflow } from '../models/view-workflow';
|
||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||
import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service';
|
||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
|
||||
/**
|
||||
* Filter description to easier parse dynamically occurring workflows
|
||||
*/
|
||||
interface WorkflowFilterDesc {
|
||||
name: string;
|
||||
filter: OsFilterOption[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the motion list
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MotionFilterListService extends BaseFilterListService<ViewMotion> {
|
||||
protected name = 'Motion';
|
||||
|
||||
/**
|
||||
* getter for the filterOptions. Note that in this case, the options are
|
||||
* generated dynamically, as the options change with the datastore
|
||||
*/
|
||||
public get filterOptions(): OsFilter[] {
|
||||
let filterOptions = [
|
||||
this.flowFilterOptions,
|
||||
this.categoryFilterOptions,
|
||||
this.motionBlockFilterOptions,
|
||||
this.recommendationFilterOptions,
|
||||
this.motionCommentFilterOptions,
|
||||
this.tagFilterOptions
|
||||
];
|
||||
|
||||
if (!this.operator.isAnonymous) {
|
||||
filterOptions = filterOptions.concat(this.personalNoteFilterOptions);
|
||||
}
|
||||
return filterOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter definitions for the workflow filter. Options will be generated by
|
||||
* getFilterOptions (as the workflows available may change)
|
||||
*/
|
||||
public flowFilterOptions: OsFilter = {
|
||||
property: 'state',
|
||||
label: 'State',
|
||||
options: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Listen to the configuration for change in defined/used workflows
|
||||
*/
|
||||
private enabledWorkflows = { statuteEnabled: false, statute: null, motion: null };
|
||||
|
||||
/**
|
||||
* storage for currently used workflows
|
||||
* Filter definitions for the workflow filter. Options will be generated by
|
||||
* getFilterOptions (as the workflows available may change)
|
||||
*/
|
||||
private currentWorkflows: ViewWorkflow[];
|
||||
public stateFilterOptions: OsFilter = {
|
||||
property: 'state',
|
||||
label: 'State',
|
||||
options: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter definitions for the category filter. Options will be generated by
|
||||
* getFilterOptions (as the categories available may change)
|
||||
*/
|
||||
public categoryFilterOptions: OsFilter = {
|
||||
property: 'category',
|
||||
options: []
|
||||
@ -98,7 +81,6 @@ export class MotionFilterListService extends BaseFilterListService<ViewMotion> {
|
||||
{
|
||||
property: 'star',
|
||||
label: this.translate.instant('Favorites'),
|
||||
isActive: false,
|
||||
options: [
|
||||
{
|
||||
condition: true,
|
||||
@ -113,7 +95,6 @@ export class MotionFilterListService extends BaseFilterListService<ViewMotion> {
|
||||
{
|
||||
property: 'hasNotes',
|
||||
label: this.translate.instant('Personal notes'),
|
||||
isActive: false,
|
||||
options: [
|
||||
{
|
||||
condition: true,
|
||||
@ -132,216 +113,175 @@ export class MotionFilterListService extends BaseFilterListService<ViewMotion> {
|
||||
* the available filters
|
||||
*
|
||||
* @param store The browser's storage; required for fetching filters from any previous sessions
|
||||
* @param workflowRepo Subscribing to filters by states/Recommendation
|
||||
* @param categoryRepo Subscribing to filters by Categories
|
||||
* @param motionBlockRepo Subscribing to filters by MotionBlock
|
||||
* @param commentRepo subycribing filter by presense of comment
|
||||
* @param categoryRepo to filter by Categories
|
||||
* @param motionBlockRepo to filter by MotionBlock
|
||||
* @param commentRepo to filter by motion comments
|
||||
* @param tagRepo to filter by tags
|
||||
* @param workflowRepo Subscribing to filters by states and recommendation
|
||||
* @param translate Translation service
|
||||
* @param config the current configuration (to determine which workflow filters to offer )
|
||||
* @param motionRepo the motion's own repository, required by the parent
|
||||
* @param operator
|
||||
*/
|
||||
public constructor(
|
||||
store: StorageService,
|
||||
categoryRepo: CategoryRepositoryService,
|
||||
motionBlockRepo: MotionBlockRepositoryService,
|
||||
commentRepo: MotionCommentSectionRepositoryService,
|
||||
tagRepo: TagRepositoryService,
|
||||
private workflowRepo: WorkflowRepositoryService,
|
||||
private categoryRepo: CategoryRepositoryService,
|
||||
private motionBlockRepo: MotionBlockRepositoryService,
|
||||
private commentRepo: MotionCommentSectionRepositoryService,
|
||||
private translate: TranslateService,
|
||||
private config: ConfigService,
|
||||
motionRepo: MotionRepositoryService,
|
||||
private operator: OperatorService,
|
||||
private tagRepo: TagRepositoryService
|
||||
private config: ConfigService
|
||||
) {
|
||||
super(store, motionRepo);
|
||||
super('Motion', store);
|
||||
this.getWorkflowConfig();
|
||||
|
||||
this.updateFilterForRepo(categoryRepo, this.categoryFilterOptions, this.translate.instant('No category set'));
|
||||
|
||||
this.updateFilterForRepo(
|
||||
motionBlockRepo,
|
||||
this.motionBlockFilterOptions,
|
||||
this.translate.instant('No motion block set')
|
||||
);
|
||||
|
||||
this.updateFilterForRepo(commentRepo, this.motionCommentFilterOptions, this.translate.instant('No comment'));
|
||||
|
||||
this.updateFilterForRepo(tagRepo, this.tagFilterOptions, this.translate.instant('No tags'));
|
||||
|
||||
this.subscribeWorkflows();
|
||||
this.subscribeCategories();
|
||||
this.subscribeMotionBlocks();
|
||||
this.subscribeComments();
|
||||
this.subscribeTags();
|
||||
|
||||
this.operator.getUserObservable().subscribe(() => {
|
||||
this.updateFilterDefinitions(this.filterOptions);
|
||||
this.setFilterDefinitions();
|
||||
});
|
||||
}
|
||||
|
||||
private getWorkflowConfig(): void {
|
||||
this.config.get<string>('motions_statute_amendments_workflow').subscribe(id => {
|
||||
this.enabledWorkflows.statute = +id;
|
||||
});
|
||||
|
||||
this.config.get<string>('motions_workflow').subscribe(id => {
|
||||
this.enabledWorkflows.motion = +id;
|
||||
});
|
||||
|
||||
this.config.get<boolean>('motions_statutes_enabled').subscribe(bool => {
|
||||
this.enabledWorkflows.statuteEnabled = bool;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscibes to changing MotionBlocks, and updates the filter accordingly
|
||||
* @returns the filter definition
|
||||
*/
|
||||
private subscribeMotionBlocks(): void {
|
||||
this.motionBlockRepo.getViewModelListObservable().subscribe(motionBlocks => {
|
||||
const motionBlockOptions: OsFilterOptions = motionBlocks.map(mb => ({
|
||||
condition: mb.id,
|
||||
label: mb.title,
|
||||
isActive: false
|
||||
}));
|
||||
if (motionBlocks.length) {
|
||||
motionBlockOptions.push('-');
|
||||
motionBlockOptions.push({
|
||||
condition: null,
|
||||
label: this.translate.instant('No motion block set'),
|
||||
isActive: false
|
||||
});
|
||||
}
|
||||
this.motionBlockFilterOptions.options = motionBlockOptions;
|
||||
this.updateFilterDefinitions(this.filterOptions);
|
||||
});
|
||||
protected getFilterDefinitions(): OsFilter[] {
|
||||
let filterDefinitions = [
|
||||
this.stateFilterOptions,
|
||||
this.categoryFilterOptions,
|
||||
this.motionBlockFilterOptions,
|
||||
this.recommendationFilterOptions,
|
||||
this.motionCommentFilterOptions,
|
||||
this.tagFilterOptions
|
||||
];
|
||||
|
||||
if (!this.operator.isAnonymous) {
|
||||
filterDefinitions = filterDefinitions.concat(this.personalNoteFilterOptions);
|
||||
}
|
||||
return filterDefinitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscibes to changing Categories, and updates the filter accordingly
|
||||
*/
|
||||
private subscribeCategories(): void {
|
||||
this.categoryRepo.getViewModelListObservable().subscribe(categories => {
|
||||
const categoryOptions: OsFilterOptions = categories.map(cat => ({
|
||||
condition: cat.id,
|
||||
label: cat.prefixedName,
|
||||
isActive: false
|
||||
}));
|
||||
if (categories.length) {
|
||||
categoryOptions.push('-');
|
||||
categoryOptions.push({
|
||||
label: this.translate.instant('No category set'),
|
||||
condition: null
|
||||
});
|
||||
}
|
||||
this.categoryFilterOptions.options = categoryOptions;
|
||||
this.updateFilterDefinitions(this.filterOptions);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscibes to changing Workflows, and updates the state and recommendation filters accordingly
|
||||
* Only subscribes to workflows that are enabled in the config as motion or statute paragraph workflow
|
||||
* Subscribes to changing Workflows, and updates the state and recommendation filters accordingly.
|
||||
*/
|
||||
private subscribeWorkflows(): void {
|
||||
this.workflowRepo.getViewModelListObservable().subscribe(workflows => {
|
||||
this.currentWorkflows = workflows;
|
||||
this.updateWorkflows();
|
||||
});
|
||||
this.config.get<string>('motions_statute_amendments_workflow').subscribe(id => {
|
||||
this.enabledWorkflows.statute = +id;
|
||||
this.updateWorkflows();
|
||||
});
|
||||
this.config.get<string>('motions_workflow').subscribe(id => {
|
||||
this.enabledWorkflows.motion = +id;
|
||||
this.updateWorkflows();
|
||||
});
|
||||
this.config.get<boolean>('motions_statutes_enabled').subscribe(bool => {
|
||||
this.enabledWorkflows.statuteEnabled = bool;
|
||||
this.updateWorkflows();
|
||||
});
|
||||
}
|
||||
if (workflows && workflows.length) {
|
||||
const workflowFilters: WorkflowFilterDesc[] = [];
|
||||
const recoFilters: WorkflowFilterDesc[] = [];
|
||||
|
||||
/**
|
||||
* Helper to show only filter for workflows that are included in to currently
|
||||
* set config options
|
||||
*/
|
||||
private updateWorkflows(): void {
|
||||
const workflowOptions: OsFilterOptions = [];
|
||||
const finalStates: number[] = [];
|
||||
const nonFinalStates: number[] = [];
|
||||
const recommendationOptions: OsFilterOptions = [];
|
||||
if (!this.currentWorkflows) {
|
||||
return;
|
||||
}
|
||||
this.currentWorkflows.forEach(workflow => {
|
||||
if (
|
||||
workflow.id === this.enabledWorkflows.motion ||
|
||||
(this.enabledWorkflows.statuteEnabled && workflow.id === this.enabledWorkflows.statute)
|
||||
) {
|
||||
workflowOptions.push(workflow.name);
|
||||
recommendationOptions.push(workflow.name);
|
||||
workflow.states.forEach(state => {
|
||||
// filter out restricted states for unpriviledged users
|
||||
if (
|
||||
this.operator.hasPerms('motions.can_manage', 'motions.can_manage_metadata') ||
|
||||
state.restriction.length === 0
|
||||
) {
|
||||
if (state.isFinalState) {
|
||||
finalStates.push(state.id);
|
||||
} else {
|
||||
nonFinalStates.push(state.id);
|
||||
const finalStates: number[] = [];
|
||||
const nonFinalStates: number[] = [];
|
||||
|
||||
// get all relevant information
|
||||
for (const workflow of workflows) {
|
||||
if (this.isWorkflowEnabled(workflow.id)) {
|
||||
workflowFilters.push({
|
||||
name: workflow.name,
|
||||
filter: []
|
||||
});
|
||||
|
||||
recoFilters.push({
|
||||
name: workflow.name,
|
||||
filter: []
|
||||
});
|
||||
|
||||
for (const state of workflow.states) {
|
||||
if (
|
||||
this.operator.hasPerms('motions.can_manage', 'motions.can_manage_metadata') &&
|
||||
state.restriction
|
||||
) {
|
||||
// sort final and non final states
|
||||
state.isFinalState ? finalStates.push(state.id) : nonFinalStates.push(state.id);
|
||||
|
||||
workflowFilters[workflowFilters.length - 1].filter.push({
|
||||
condition: state.id,
|
||||
label: state.name
|
||||
});
|
||||
|
||||
if (state.recommendation_label) {
|
||||
recoFilters[workflowFilters.length - 1].filter.push({
|
||||
condition: state.id,
|
||||
label: state.recommendation_label
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
workflowOptions.push({
|
||||
condition: state.id,
|
||||
label: state.name,
|
||||
isActive: false
|
||||
});
|
||||
}
|
||||
if (state.recommendation_label) {
|
||||
recommendationOptions.push({
|
||||
condition: state.id,
|
||||
label: state.recommendation_label,
|
||||
isActive: false
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if (workflowOptions.length) {
|
||||
workflowOptions.push('-');
|
||||
workflowOptions.push({
|
||||
label: 'Done',
|
||||
condition: finalStates
|
||||
});
|
||||
workflowOptions.push({
|
||||
label: this.translate.instant('Undone'),
|
||||
condition: nonFinalStates
|
||||
});
|
||||
}
|
||||
if (recommendationOptions.length) {
|
||||
recommendationOptions.push('-');
|
||||
recommendationOptions.push({
|
||||
label: this.translate.instant('No recommendation'),
|
||||
condition: null
|
||||
});
|
||||
}
|
||||
this.flowFilterOptions.options = workflowOptions;
|
||||
this.recommendationFilterOptions.options = recommendationOptions;
|
||||
this.updateFilterDefinitions(this.filterOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscibes to changing Comments, and updates the filter accordingly
|
||||
*/
|
||||
private subscribeComments(): void {
|
||||
this.commentRepo.getViewModelListObservable().subscribe(comments => {
|
||||
const commentOptions: OsFilterOptions = comments.map(comment => ({
|
||||
condition: comment.id,
|
||||
label: comment.name,
|
||||
isActive: false
|
||||
}));
|
||||
if (comments.length) {
|
||||
commentOptions.push('-');
|
||||
commentOptions.push({
|
||||
label: this.translate.instant('No comment'),
|
||||
condition: null
|
||||
});
|
||||
// convert to filter options
|
||||
if (workflowFilters && workflowFilters.length) {
|
||||
let workflowOptions: OsFilterOptions = [];
|
||||
for (const filterDef of workflowFilters) {
|
||||
workflowOptions.push(filterDef.name);
|
||||
workflowOptions = workflowOptions.concat(filterDef.filter);
|
||||
}
|
||||
|
||||
// add "done" and "undone"
|
||||
workflowOptions.push('-');
|
||||
workflowOptions.push({
|
||||
label: 'Done',
|
||||
condition: finalStates
|
||||
});
|
||||
|
||||
workflowOptions.push({
|
||||
label: this.translate.instant('Undone'),
|
||||
condition: nonFinalStates
|
||||
});
|
||||
|
||||
this.stateFilterOptions.options = workflowOptions;
|
||||
}
|
||||
|
||||
if (recoFilters && recoFilters.length) {
|
||||
let recoOptions: OsFilterOptions = [];
|
||||
|
||||
for (const filterDef of recoFilters) {
|
||||
recoOptions.push(filterDef.name);
|
||||
recoOptions = recoOptions.concat(filterDef.filter);
|
||||
}
|
||||
|
||||
recoOptions.push('-');
|
||||
recoOptions.push({
|
||||
label: this.translate.instant('No recommendation'),
|
||||
condition: null
|
||||
});
|
||||
this.recommendationFilterOptions.options = recoOptions;
|
||||
}
|
||||
this.setFilterDefinitions();
|
||||
}
|
||||
this.motionCommentFilterOptions.options = commentOptions;
|
||||
this.updateFilterDefinitions(this.filterOptions);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscibes to changing Tags, and updates the filter accordingly
|
||||
*/
|
||||
private subscribeTags(): void {
|
||||
this.tagRepo.getViewModelListObservable().subscribe(tags => {
|
||||
const tagOptions: OsFilterOptions = tags.map(tag => ({
|
||||
condition: tag.id,
|
||||
label: tag.name,
|
||||
isActive: false
|
||||
}));
|
||||
if (tags.length) {
|
||||
tagOptions.push('-');
|
||||
tagOptions.push({
|
||||
label: this.translate.instant('No tags'),
|
||||
condition: null
|
||||
});
|
||||
}
|
||||
this.tagFilterOptions.options = tagOptions;
|
||||
this.updateFilterDefinitions(this.filterOptions);
|
||||
});
|
||||
private isWorkflowEnabled(workflowId: number): boolean {
|
||||
return (
|
||||
workflowId === this.enabledWorkflows.motion ||
|
||||
(this.enabledWorkflows.statuteEnabled && workflowId === this.enabledWorkflows.statute)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,42 +1,74 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { BaseSortListService, OsSortingDefinition } from 'app/core/ui-services/base-sort-list.service';
|
||||
import { ViewMotion } from '../models/view-motion';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { StorageService } from 'app/core/core-services/storage.service';
|
||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
import { _ } from 'app/core/translate/translation-marker';
|
||||
|
||||
import { _ } from 'app/core/translate/translation-marker';
|
||||
import { BaseSortListService, OsSortingDefinition, OsSortingOption } from 'app/core/ui-services/base-sort-list.service';
|
||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
import { Deferred } from 'app/core/deferred';
|
||||
import { StorageService } from 'app/core/core-services/storage.service';
|
||||
import { ViewMotion } from '../models/view-motion';
|
||||
|
||||
/**
|
||||
* Sorting service for the motion list
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MotionSortListService extends BaseSortListService<ViewMotion> {
|
||||
public sortOptions: OsSortingDefinition<ViewMotion> = {
|
||||
sortProperty: 'weight',
|
||||
sortAscending: true,
|
||||
options: [
|
||||
{ property: 'weight', label: 'Call list' },
|
||||
{ property: 'identifier' },
|
||||
{ property: 'title' },
|
||||
{ property: 'submitters' },
|
||||
{ property: 'category' },
|
||||
{ property: 'motion_block_id', label: 'Motion block' },
|
||||
{ property: 'state' },
|
||||
{ property: 'creationDate', label: _('Creation date') },
|
||||
{ property: 'lastChangeDate', label: _('Last modified') }
|
||||
]
|
||||
};
|
||||
protected name = 'Motion';
|
||||
/**
|
||||
* Hold the default motion sorting
|
||||
*/
|
||||
private defaultMotionSorting: string;
|
||||
|
||||
/**
|
||||
* Constructor. Sets the default sorting if none is set locally
|
||||
*
|
||||
* @param translate
|
||||
* @param store
|
||||
* @param config
|
||||
* To wait until the default motion was loaded once
|
||||
*/
|
||||
public constructor(translate: TranslateService, store: StorageService, config: ConfigService) {
|
||||
super(translate, store);
|
||||
this.defaultSorting = config.instant<keyof ViewMotion>('motions_motions_sorting');
|
||||
private readonly defaultSortingLoaded: Deferred<void> = new Deferred();
|
||||
|
||||
/**
|
||||
* Define the sort options
|
||||
*/
|
||||
public sortOptions: OsSortingOption<ViewMotion>[] = [
|
||||
{ property: 'weight', label: 'Call list' },
|
||||
{ property: 'identifier' },
|
||||
{ property: 'title' },
|
||||
{ property: 'submitters' },
|
||||
{ property: 'category' },
|
||||
{ property: 'motion_block_id', label: 'Motion block' },
|
||||
{ property: 'state' },
|
||||
{ property: 'creationDate', label: _('Creation date') },
|
||||
{ property: 'lastChangeDate', label: _('Last modified') }
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param translate required by parent
|
||||
* @param store required by parent
|
||||
* @param config set the default sorting according to OpenSlides configuration
|
||||
*/
|
||||
public constructor(translate: TranslateService, store: StorageService, private config: ConfigService) {
|
||||
super('Motion', translate, store);
|
||||
|
||||
this.config.get<string>('motions_motions_sorting').subscribe(defSortProp => {
|
||||
if (defSortProp) {
|
||||
this.defaultMotionSorting = defSortProp;
|
||||
this.defaultSortingLoaded.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Required by parent
|
||||
*
|
||||
* @returns the default sorting strategy
|
||||
*/
|
||||
protected async getDefaultDefinition(): Promise<OsSortingDefinition<ViewMotion>> {
|
||||
await this.defaultSortingLoaded;
|
||||
return {
|
||||
sortProperty: this.defaultMotionSorting as keyof ViewMotion,
|
||||
sortAscending: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ import { ViewTag } from '../../models/view-tag';
|
||||
templateUrl: './tag-list.component.html',
|
||||
styleUrls: ['./tag-list.component.css']
|
||||
})
|
||||
export class TagListComponent extends ListViewBaseComponent<ViewTag, Tag> implements OnInit {
|
||||
export class TagListComponent extends ListViewBaseComponent<ViewTag, Tag, TagRepositoryService> implements OnInit {
|
||||
public editTag = false;
|
||||
public newTag = false;
|
||||
public selectedTag: ViewTag;
|
||||
@ -50,7 +50,7 @@ export class TagListComponent extends ListViewBaseComponent<ViewTag, Tag> implem
|
||||
private repo: TagRepositoryService,
|
||||
private promptService: PromptService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar, route, storage);
|
||||
super(titleService, translate, matSnackBar, repo, route, storage);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -55,14 +55,13 @@ interface InfoDialog {
|
||||
|
||||
/**
|
||||
* Component for the user list view.
|
||||
*
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-user-list',
|
||||
templateUrl: './user-list.component.html',
|
||||
styleUrls: ['./user-list.component.scss']
|
||||
})
|
||||
export class UserListComponent extends ListViewBaseComponent<ViewUser, User> implements OnInit {
|
||||
export class UserListComponent extends ListViewBaseComponent<ViewUser, User, UserRepositoryService> implements OnInit {
|
||||
/**
|
||||
* The reference to the template.
|
||||
*/
|
||||
@ -154,7 +153,7 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser, User> imp
|
||||
private userPdf: UserPdfExportService,
|
||||
private dialog: MatDialog
|
||||
) {
|
||||
super(titleService, translate, matSnackBar, route, storage, filterService, sortService);
|
||||
super(titleService, translate, matSnackBar, repo, route, storage, filterService, sortService);
|
||||
|
||||
// enable multiSelect for this listView
|
||||
this.canMultiSelect = true;
|
||||
|
@ -1,107 +1,75 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { BaseFilterListService, OsFilter } from 'app/core/ui-services/base-filter-list.service';
|
||||
import { StorageService } from 'app/core/core-services/storage.service';
|
||||
import { ViewUser } from '../models/view-user';
|
||||
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
|
||||
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
||||
import { StorageService } from 'app/core/core-services/storage.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ViewUser } from '../models/view-user';
|
||||
|
||||
/**
|
||||
* Filter the user list
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class UserFilterListService extends BaseFilterListService<ViewUser> {
|
||||
protected name = 'User';
|
||||
|
||||
private userGroupFilterOptions = {
|
||||
isActive: false,
|
||||
private userGroupFilterOptions: OsFilter = {
|
||||
property: 'groups_id',
|
||||
label: 'Groups',
|
||||
options: []
|
||||
};
|
||||
|
||||
public staticFilterOptions = [
|
||||
{
|
||||
property: 'is_present',
|
||||
label: 'Presence',
|
||||
isActive: false,
|
||||
options: [
|
||||
{ condition: true, label: this.translate.instant('Is present') },
|
||||
{ condition: false, label: this.translate.instant('Is not present') }
|
||||
]
|
||||
},
|
||||
{
|
||||
property: 'is_active',
|
||||
label: this.translate.instant('Active'),
|
||||
isActive: false,
|
||||
options: [
|
||||
{ condition: true, label: 'Is active' },
|
||||
{ condition: false, label: this.translate.instant('Is not active') }
|
||||
]
|
||||
},
|
||||
{
|
||||
property: 'is_committee',
|
||||
label: this.translate.instant('Committee'),
|
||||
isActive: false,
|
||||
options: [
|
||||
{ condition: true, label: 'Is a committee' },
|
||||
{ condition: false, label: this.translate.instant('Is not a committee') }
|
||||
]
|
||||
},
|
||||
{
|
||||
property: 'is_last_email_send',
|
||||
label: this.translate.instant('Last email send'),
|
||||
isActive: false,
|
||||
options: [
|
||||
{ condition: true, label: this.translate.instant('Got an email') },
|
||||
{ condition: false, label: this.translate.instant("Didn't get an email") }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* getter for the filterOptions. Note that in this case, the options are
|
||||
* generated dynamically, as the options change with the datastore
|
||||
*/
|
||||
public get filterOptions(): OsFilter[] {
|
||||
return [this.userGroupFilterOptions].concat(this.staticFilterOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contructor. Subscribes to incoming group definitions.
|
||||
* Constructor.
|
||||
* Subscribes to incoming group definitions.
|
||||
*
|
||||
* @param store
|
||||
* @param groupRepo
|
||||
* @param repo
|
||||
* @param groupRepo to filter by groups
|
||||
* @param translate marking some translations that are unique here
|
||||
*
|
||||
*/
|
||||
public constructor(
|
||||
store: StorageService,
|
||||
private groupRepo: GroupRepositoryService,
|
||||
repo: UserRepositoryService,
|
||||
private translate: TranslateService
|
||||
) {
|
||||
super(store, repo);
|
||||
this.subscribeGroups();
|
||||
public constructor(store: StorageService, groupRepo: GroupRepositoryService, private translate: TranslateService) {
|
||||
super('User', store);
|
||||
this.updateFilterForRepo(groupRepo, this.userGroupFilterOptions, this.translate.instant('Default'), [1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the filter according to existing groups.
|
||||
* TODO: Users with only the 'standard' group set appear in the model as items without groups_id. 'Standard' filter is broken
|
||||
* @returns the filter definition
|
||||
*/
|
||||
public subscribeGroups(): void {
|
||||
this.groupRepo.getViewModelListObservable().subscribe(groups => {
|
||||
const groupOptions = [];
|
||||
groups.forEach(group => {
|
||||
groupOptions.push({
|
||||
condition: group.id,
|
||||
label: group.name,
|
||||
isActive: false
|
||||
});
|
||||
});
|
||||
this.userGroupFilterOptions.options = groupOptions;
|
||||
this.updateFilterDefinitions(this.filterOptions);
|
||||
});
|
||||
protected getFilterDefinitions(): OsFilter[] {
|
||||
const staticFilterOptions: OsFilter[] = [
|
||||
{
|
||||
property: 'is_present',
|
||||
label: 'Presence',
|
||||
options: [
|
||||
{ condition: true, label: this.translate.instant('Is present') },
|
||||
{ condition: false, label: this.translate.instant('Is not present') }
|
||||
]
|
||||
},
|
||||
{
|
||||
property: 'is_active',
|
||||
label: this.translate.instant('Active'),
|
||||
options: [
|
||||
{ condition: true, label: 'Is active' },
|
||||
{ condition: false, label: this.translate.instant('Is not active') }
|
||||
]
|
||||
},
|
||||
{
|
||||
property: 'is_committee',
|
||||
label: this.translate.instant('Committee'),
|
||||
options: [
|
||||
{ condition: true, label: 'Is a committee' },
|
||||
{ condition: false, label: this.translate.instant('Is not a committee') }
|
||||
]
|
||||
},
|
||||
{
|
||||
property: 'is_last_email_send',
|
||||
label: this.translate.instant('Last email send'),
|
||||
options: [
|
||||
{ condition: true, label: this.translate.instant('Got an email') },
|
||||
{ condition: false, label: this.translate.instant("Didn't get an email") }
|
||||
]
|
||||
}
|
||||
];
|
||||
return staticFilterOptions.concat(this.userGroupFilterOptions);
|
||||
}
|
||||
}
|
||||
|
@ -1,40 +1,52 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { BaseSortListService, OsSortingDefinition } from 'app/core/ui-services/base-sort-list.service';
|
||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
import { BaseSortListService, OsSortingDefinition, OsSortingOption } from 'app/core/ui-services/base-sort-list.service';
|
||||
import { StorageService } from 'app/core/core-services/storage.service';
|
||||
import { ViewUser } from '../models/view-user';
|
||||
|
||||
/**
|
||||
* Sorting service for the user list
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class UserSortListService extends BaseSortListService<ViewUser> {
|
||||
public sortOptions: OsSortingDefinition<ViewUser> = {
|
||||
sortProperty: 'first_name',
|
||||
sortAscending: true,
|
||||
options: [
|
||||
{ property: 'first_name', label: 'Given name' },
|
||||
{ property: 'last_name', label: 'Surname' },
|
||||
{ property: 'is_present', label: 'Presence' },
|
||||
{ property: 'is_active', label: 'Is active' },
|
||||
{ property: 'is_committee', label: 'Is committee' },
|
||||
{ property: 'number', label: 'Participant number' },
|
||||
{ property: 'structure_level', label: 'Structure level' },
|
||||
{ property: 'comment' }
|
||||
]
|
||||
};
|
||||
protected name = 'User';
|
||||
/**
|
||||
* Define the sort options
|
||||
*/
|
||||
public sortOptions: OsSortingOption<ViewUser>[] = [
|
||||
{ property: 'first_name', label: 'Given name' },
|
||||
{ property: 'last_name', label: 'Surname' },
|
||||
{ property: 'is_present', label: 'Presence' },
|
||||
{ property: 'is_active', label: 'Is active' },
|
||||
{ property: 'is_committee', label: 'Is committee' },
|
||||
{ property: 'number', label: 'Participant number' },
|
||||
{ property: 'structure_level', label: 'Structure level' },
|
||||
{ property: 'comment' }
|
||||
// TODO email send?
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor. Sets the default sorting if none is set locally
|
||||
* Constructor.
|
||||
*
|
||||
* @param translate
|
||||
* @param store
|
||||
* @param config
|
||||
* @param translate required by parent
|
||||
* @param store requires by parent
|
||||
*/
|
||||
public constructor(translate: TranslateService, store: StorageService, config: ConfigService) {
|
||||
super(translate, store);
|
||||
this.defaultSorting = config.instant<keyof ViewUser>('users_sort_by');
|
||||
public constructor(translate: TranslateService, store: StorageService) {
|
||||
super('User', translate, store);
|
||||
}
|
||||
|
||||
/**
|
||||
* Required by parent
|
||||
*
|
||||
* @returns the default sorting strategy
|
||||
*/
|
||||
public async getDefaultDefinition(): Promise<OsSortingDefinition<ViewUser>> {
|
||||
return {
|
||||
sortProperty: 'first_name',
|
||||
sortAscending: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -197,7 +197,7 @@ def get_config_variables():
|
||||
input_type="choice",
|
||||
label="Sort motions by",
|
||||
choices=(
|
||||
{"value": "callListWeight", "display_name": "Call list"},
|
||||
{"value": "weight", "display_name": "Call list"},
|
||||
{"value": "identifier", "display_name": "Identifier"},
|
||||
),
|
||||
weight=335,
|
||||
|
Loading…
Reference in New Issue
Block a user