From 850fcbe24320c27678cc18bec19d099d17b7fe15 Mon Sep 17 00:00:00 2001 From: Sean Engelhardt Date: Tue, 7 May 2019 16:25:39 +0200 Subject: [PATCH] Rework sort and filter More observable based, more scaleable filtering based on an old branch of @FinnStutzenstein. cleans some functions and provides some differend features. --- .../data-store-update-manager.service.ts | 2 +- .../core/core-services/operator.service.ts | 2 +- .../app/core/core-services/ping.service.ts | 2 +- client/src/app/core/deferred.ts | 8 +- .../ui-services/base-filter-list.service.ts | 401 ++++++++++-------- .../ui-services/base-sort-list.service.ts | 344 +++++++-------- .../filter-menu/filter-menu.component.scss | 13 - .../os-sort-bottom-sheet.component.html | 8 - .../filter-menu/filter-menu.component.html | 10 +- .../filter-menu/filter-menu.component.scss | 30 ++ .../filter-menu/filter-menu.component.spec.ts | 0 .../filter-menu/filter-menu.component.ts | 1 + .../sort-bottom-sheet.component.html | 8 + .../sort-bottom-sheet.component.scss} | 0 .../sort-bottom-sheet.component.spec.ts} | 9 +- .../sort-bottom-sheet.component.ts} | 14 +- .../sort-filter-bar.component.html} | 9 +- .../sort-filter-bar.component.scss} | 0 .../sort-filter-bar.component.spec.ts} | 6 +- .../sort-filter-bar.component.ts} | 45 +- client/src/app/shared/shared.module.ts | 18 +- .../agenda-list/agenda-list.component.ts | 13 +- .../services/agenda-filter-list.service.ts | 63 ++- .../assignment-list.component.ts | 6 +- .../services/assignment-filter.service.ts | 54 +-- .../services/assignment-sort-list.service.ts | 48 ++- client/src/app/site/base/list-view-base.ts | 75 ++-- .../history-list/history-list.component.ts | 5 +- .../mediafile-list.component.ts | 5 +- .../services/mediafile-filter.service.ts | 93 ++-- .../services/mediafiles-sort-list.service.ts | 56 ++- .../motion-block-detail.component.html | 4 +- .../motion-block-detail.component.ts | 54 ++- .../motion-block-list.component.html | 16 +- .../motion-block-list.component.ts | 17 +- .../motion-list/motion-list.component.ts | 46 +- .../workflow-list/workflow-list.component.ts | 7 +- .../motion-block-sort.service.spec.ts | 17 + .../services/motion-block-sort.service.ts | 24 ++ .../services/motion-filter-list.service.ts | 392 ++++++++--------- .../services/motion-sort-list.service.ts | 90 ++-- .../components/tag-list/tag-list.component.ts | 4 +- .../user-list/user-list.component.ts | 5 +- .../services/user-filter-list.service.ts | 130 +++--- .../users/services/user-sort-list.service.ts | 60 +-- openslides/motions/config_variables.py | 2 +- 46 files changed, 1167 insertions(+), 1049 deletions(-) delete mode 100644 client/src/app/shared/components/os-sort-filter-bar/filter-menu/filter-menu.component.scss delete mode 100644 client/src/app/shared/components/os-sort-filter-bar/os-sort-bottom-sheet/os-sort-bottom-sheet.component.html rename client/src/app/shared/components/{os-sort-filter-bar => sort-filter-bar}/filter-menu/filter-menu.component.html (75%) create mode 100644 client/src/app/shared/components/sort-filter-bar/filter-menu/filter-menu.component.scss rename client/src/app/shared/components/{os-sort-filter-bar => sort-filter-bar}/filter-menu/filter-menu.component.spec.ts (100%) rename client/src/app/shared/components/{os-sort-filter-bar => sort-filter-bar}/filter-menu/filter-menu.component.ts (99%) create mode 100644 client/src/app/shared/components/sort-filter-bar/sort-bottom-sheet/sort-bottom-sheet.component.html rename client/src/app/shared/components/{os-sort-filter-bar/os-sort-bottom-sheet/os-sort-bottom-sheet.component.scss => sort-filter-bar/sort-bottom-sheet/sort-bottom-sheet.component.scss} (100%) rename client/src/app/shared/components/{os-sort-filter-bar/os-sort-bottom-sheet/os-sort-bottom-sheet.component.spec.ts => sort-filter-bar/sort-bottom-sheet/sort-bottom-sheet.component.spec.ts} (60%) rename client/src/app/shared/components/{os-sort-filter-bar/os-sort-bottom-sheet/os-sort-bottom-sheet.component.ts => sort-filter-bar/sort-bottom-sheet/sort-bottom-sheet.component.ts} (73%) rename client/src/app/shared/components/{os-sort-filter-bar/os-sort-filter-bar.component.html => sort-filter-bar/sort-filter-bar.component.html} (91%) rename client/src/app/shared/components/{os-sort-filter-bar/os-sort-filter-bar.component.scss => sort-filter-bar/sort-filter-bar.component.scss} (100%) rename client/src/app/shared/components/{os-sort-filter-bar/os-sort-filter-bar.component.spec.ts => sort-filter-bar/sort-filter-bar.component.spec.ts} (74%) rename client/src/app/shared/components/{os-sort-filter-bar/os-sort-filter-bar.component.ts => sort-filter-bar/sort-filter-bar.component.ts} (80%) create mode 100644 client/src/app/site/motions/services/motion-block-sort.service.spec.ts create mode 100644 client/src/app/site/motions/services/motion-block-sort.service.ts diff --git a/client/src/app/core/core-services/data-store-update-manager.service.ts b/client/src/app/core/core-services/data-store-update-manager.service.ts index 1429ba0bb..f1f2d394e 100644 --- a/client/src/app/core/core-services/data-store-update-manager.service.ts +++ b/client/src/app/core/core-services/data-store-update-manager.service.ts @@ -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; diff --git a/client/src/app/core/core-services/operator.service.ts b/client/src/app/core/core-services/operator.service.ts index 7e2d4a675..7d72004c2 100644 --- a/client/src/app/core/core-services/operator.service.ts +++ b/client/src/app/core/core-services/operator.service.ts @@ -120,7 +120,7 @@ export class OperatorService implements OnAfterAppsLoaded { private readonly _loaded: Deferred = new Deferred(); public get loaded(): Promise { - return this._loaded.promise; + return this._loaded; } /** diff --git a/client/src/app/core/core-services/ping.service.ts b/client/src/app/core/core-services/ping.service.ts index b684c99fd..a5fe18544 100644 --- a/client/src/app/core/core-services/ping.service.ts +++ b/client/src/app/core/core-services/ping.service.ts @@ -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()); diff --git a/client/src/app/core/deferred.ts b/client/src/app/core/deferred.ts index b4602b541..82be23512 100644 --- a/client/src/app/core/deferred.ts +++ b/client/src/app/core/deferred.ts @@ -13,7 +13,7 @@ * // * ``` */ -export class Deferred { +export class Deferred extends Promise { /** * The promise to wait for */ @@ -28,9 +28,11 @@ export class Deferred { * Creates the promise and overloads the resolve function */ public constructor() { - this.promise = new Promise(resolve => { - this.resolve = resolve; + let preResolve: (val?: T) => void; + super(resolve => { + preResolve = resolve; }); + this._resolve = preResolve; } /** diff --git a/client/src/app/core/ui-services/base-filter-list.service.ts b/client/src/app/core/ui-services/base-filter-list.service.ts index 79e022ba7..441263023 100644 --- a/client/src/app/core/ui-services/base-filter-list.service.ts +++ b/client/src/app/core/ui-services/base-filter-list.service.ts @@ -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 { /** * 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([]); - - 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([]); + + /** + * @return Observable data for the filtered output subject + */ + public get outputObservable(): Observable { + 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) {} + 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 { - 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): Promise { + const storedFilter = await this.store.get('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, + 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 { /** * 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(); + } } } diff --git a/client/src/app/core/ui-services/base-sort-list.service.ts b/client/src/app/core/ui-services/base-sort-list.service.ts index 832ee06a1..7122f63ba 100644 --- a/client/src/app/core/ui-services/base-sort-list.service.ts +++ b/client/src/app/core/ui-services/base-sort-list.service.ts @@ -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 { sortProperty: keyof V; - sortAscending?: boolean; - options: OsSortingItem[]; + 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 { +export interface OsSortingOption { property: keyof V; label?: string; } -@Injectable({ - providedIn: 'root' -}) +/** + * Base class for generic sorting purposes + */ export abstract class BaseSortListService { - /** - * Observable output that submits the newly sorted data each time a sorting has been done - */ - public sortedData = new BehaviorSubject([]); - /** * 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([]); + + /** + * @returns the sorted output subject as observable + */ + public get outputObservable(): Observable { + return this.outputSubject.asObservable(); + } /** * The current sorting definitions */ - public sortOptions: OsSortingDefinition; + private sortDefinition: OsSortingDefinition; /** - * 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 { - 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[]; + + /** + * 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>; + + /** + * 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): Promise { + if (this.inputDataSubscription) { + this.inputDataSubscription.unsubscribe(); + this.inputDataSubscription = null; + } + + if (!this.sortDefinition) { + this.sortDefinition = await this.store.get | 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): string { - if (!this.sortProperty || this.sortProperty !== (option.property as string)) { + public getSortIcon(option: OsSortingOption): string { + if (this.sortProperty !== option.property) { return ''; } return this.ascending ? 'arrow_downward' : 'arrow_upward'; } - public getSortLabel(option: OsSortingItem): 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): string { if (option.label) { return option.label; } @@ -158,51 +194,17 @@ export abstract class BaseSortListService { } /** - * Retrieve the currently saved sorting definition from the borwser's - * store + * Saves the current sorting definitions to the local store */ - private async loadStorageDefinition(): Promise { - const sorting: OsSortingDefinition | 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 { - 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 { * 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; + } + } + }) + ); + } } } diff --git a/client/src/app/shared/components/os-sort-filter-bar/filter-menu/filter-menu.component.scss b/client/src/app/shared/components/os-sort-filter-bar/filter-menu/filter-menu.component.scss deleted file mode 100644 index da255b2be..000000000 --- a/client/src/app/shared/components/os-sort-filter-bar/filter-menu/filter-menu.component.scss +++ /dev/null @@ -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; -} diff --git a/client/src/app/shared/components/os-sort-filter-bar/os-sort-bottom-sheet/os-sort-bottom-sheet.component.html b/client/src/app/shared/components/os-sort-filter-bar/os-sort-bottom-sheet/os-sort-bottom-sheet.component.html deleted file mode 100644 index d20fb0e9f..000000000 --- a/client/src/app/shared/components/os-sort-filter-bar/os-sort-bottom-sheet/os-sort-bottom-sheet.component.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/client/src/app/shared/components/os-sort-filter-bar/filter-menu/filter-menu.component.html b/client/src/app/shared/components/sort-filter-bar/filter-menu/filter-menu.component.html similarity index 75% rename from client/src/app/shared/components/os-sort-filter-bar/filter-menu/filter-menu.component.html rename to client/src/app/shared/components/sort-filter-bar/filter-menu/filter-menu.component.html index 0f6386836..ef60454bd 100644 --- a/client/src/app/shared/components/os-sort-filter-bar/filter-menu/filter-menu.component.html +++ b/client/src/app/shared/components/sort-filter-bar/filter-menu/filter-menu.component.html @@ -1,9 +1,9 @@ - + - {{ filter.count ? 'checked' : ''}} + {{ filter.count ? 'checked' : '' }} {{ service.getFilterName(filter) | translate }} @@ -12,7 +12,11 @@
- + {{ option.label | translate }}
diff --git a/client/src/app/shared/components/sort-filter-bar/filter-menu/filter-menu.component.scss b/client/src/app/shared/components/sort-filter-bar/filter-menu/filter-menu.component.scss new file mode 100644 index 000000000..66415d2e1 --- /dev/null +++ b/client/src/app/shared/components/sort-filter-bar/filter-menu/filter-menu.component.scss @@ -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; +} diff --git a/client/src/app/shared/components/os-sort-filter-bar/filter-menu/filter-menu.component.spec.ts b/client/src/app/shared/components/sort-filter-bar/filter-menu/filter-menu.component.spec.ts similarity index 100% rename from client/src/app/shared/components/os-sort-filter-bar/filter-menu/filter-menu.component.spec.ts rename to client/src/app/shared/components/sort-filter-bar/filter-menu/filter-menu.component.spec.ts diff --git a/client/src/app/shared/components/os-sort-filter-bar/filter-menu/filter-menu.component.ts b/client/src/app/shared/components/sort-filter-bar/filter-menu/filter-menu.component.ts similarity index 99% rename from client/src/app/shared/components/os-sort-filter-bar/filter-menu/filter-menu.component.ts rename to client/src/app/shared/components/sort-filter-bar/filter-menu/filter-menu.component.ts index 49de3d602..1a15344ac 100644 --- a/client/src/app/shared/components/os-sort-filter-bar/filter-menu/filter-menu.component.ts +++ b/client/src/app/shared/components/sort-filter-bar/filter-menu/filter-menu.component.ts @@ -58,6 +58,7 @@ export class FilterMenuComponent implements OnInit { this.dismissed.next(true); } } + public isFilter(option: OsFilterOption): boolean { return typeof option === 'string' ? false : true; } diff --git a/client/src/app/shared/components/sort-filter-bar/sort-bottom-sheet/sort-bottom-sheet.component.html b/client/src/app/shared/components/sort-filter-bar/sort-bottom-sheet/sort-bottom-sheet.component.html new file mode 100644 index 000000000..fb3a4713c --- /dev/null +++ b/client/src/app/shared/components/sort-filter-bar/sort-bottom-sheet/sort-bottom-sheet.component.html @@ -0,0 +1,8 @@ + + + + + diff --git a/client/src/app/shared/components/os-sort-filter-bar/os-sort-bottom-sheet/os-sort-bottom-sheet.component.scss b/client/src/app/shared/components/sort-filter-bar/sort-bottom-sheet/sort-bottom-sheet.component.scss similarity index 100% rename from client/src/app/shared/components/os-sort-filter-bar/os-sort-bottom-sheet/os-sort-bottom-sheet.component.scss rename to client/src/app/shared/components/sort-filter-bar/sort-bottom-sheet/sort-bottom-sheet.component.scss diff --git a/client/src/app/shared/components/os-sort-filter-bar/os-sort-bottom-sheet/os-sort-bottom-sheet.component.spec.ts b/client/src/app/shared/components/sort-filter-bar/sort-bottom-sheet/sort-bottom-sheet.component.spec.ts similarity index 60% rename from client/src/app/shared/components/os-sort-filter-bar/os-sort-bottom-sheet/os-sort-bottom-sheet.component.spec.ts rename to client/src/app/shared/components/sort-filter-bar/sort-bottom-sheet/sort-bottom-sheet.component.spec.ts index c2dfc77c3..ccea1e84c 100644 --- a/client/src/app/shared/components/os-sort-filter-bar/os-sort-bottom-sheet/os-sort-bottom-sheet.component.spec.ts +++ b/client/src/app/shared/components/sort-filter-bar/sort-bottom-sheet/sort-bottom-sheet.component.spec.ts @@ -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; - let fixture: ComponentFixture>; +describe('SortBottomSheetComponent', () => { + let fixture: ComponentFixture>; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -14,7 +13,7 @@ describe('OsSortBottomSheetComponent', () => { })); beforeEach(() => { - fixture = TestBed.createComponent(OsSortBottomSheetComponent); + fixture = TestBed.createComponent(SortBottomSheetComponent); // component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/client/src/app/shared/components/os-sort-filter-bar/os-sort-bottom-sheet/os-sort-bottom-sheet.component.ts b/client/src/app/shared/components/sort-filter-bar/sort-bottom-sheet/sort-bottom-sheet.component.ts similarity index 73% rename from client/src/app/shared/components/os-sort-filter-bar/os-sort-bottom-sheet/os-sort-bottom-sheet.component.ts rename to client/src/app/shared/components/sort-filter-bar/sort-bottom-sheet/sort-bottom-sheet.component.ts index d2456d449..afcc6a4f5 100644 --- a/client/src/app/shared/components/os-sort-filter-bar/os-sort-bottom-sheet/os-sort-bottom-sheet.component.ts +++ b/client/src/app/shared/components/sort-filter-bar/sort-bottom-sheet/sort-bottom-sheet.component.ts @@ -9,17 +9,17 @@ import { BaseViewModel } from 'app/site/base/base-view-model'; * usage: * ``` * @ViewChild('sortBottomSheet') - * public sortBottomSheet: OsSortBottomSheetComponent; + * public sortBottomSheet: SortBottomSheetComponent; * ... - * 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 implements OnInit { +export class SortBottomSheetComponent implements OnInit { /** * Constructor. Gets a reference to itself (for closing after interaction) * @param data @@ -31,10 +31,10 @@ export class OsSortBottomSheetComponent 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'); } } diff --git a/client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.html b/client/src/app/shared/components/sort-filter-bar/sort-filter-bar.component.html similarity index 91% rename from client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.html rename to client/src/app/shared/components/sort-filter-bar/sort-filter-bar.component.html index 6d9a26226..d03372036 100644 --- a/client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.html +++ b/client/src/app/shared/components/sort-filter-bar/sort-filter-bar.component.html @@ -1,7 +1,7 @@
{{ displayedCount }} of -  {{ filterService.totalCount }} +  {{ totalCount }}  ยท {{ extraItemInfo }}
@@ -61,12 +61,9 @@
- + diff --git a/client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.scss b/client/src/app/shared/components/sort-filter-bar/sort-filter-bar.component.scss similarity index 100% rename from client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.scss rename to client/src/app/shared/components/sort-filter-bar/sort-filter-bar.component.scss diff --git a/client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.spec.ts b/client/src/app/shared/components/sort-filter-bar/sort-filter-bar.component.spec.ts similarity index 74% rename from client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.spec.ts rename to client/src/app/shared/components/sort-filter-bar/sort-filter-bar.component.spec.ts index 20fa3d959..ac35ab901 100644 --- a/client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.spec.ts +++ b/client/src/app/shared/components/sort-filter-bar/sort-filter-bar.component.spec.ts @@ -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; + let component: SortFilterBarComponent; let fixture: ComponentFixture; beforeEach(async(() => { @@ -14,7 +14,7 @@ describe('OsSortFilterBarComponent', () => { })); beforeEach(() => { - fixture = TestBed.createComponent(OsSortFilterBarComponent); + fixture = TestBed.createComponent(SortFilterBarComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.ts b/client/src/app/shared/components/sort-filter-bar/sort-filter-bar.component.ts similarity index 80% rename from client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.ts rename to client/src/app/shared/components/sort-filter-bar/sort-filter-bar.component.ts index 7e5c955b7..6c4773aef 100644 --- a/client/src/app/shared/components/os-sort-filter-bar/os-sort-filter-bar.component.ts +++ b/client/src/app/shared/components/sort-filter-bar/sort-filter-bar.component.ts @@ -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 { +export class SortFilterBarComponent { /** * The currently active sorting service for the list view */ @@ -58,6 +58,7 @@ export class OsSortFilterBarComponent { @Output() public searchFieldChange = new EventEmitter(); + /** * The filter side drawer */ @@ -68,7 +69,7 @@ export class OsSortFilterBarComponent { * The bottom sheet used to alter sorting in mobile view */ @ViewChild('sortBottomSheet') - public sortBottomSheet: OsSortBottomSheetComponent; + public sortBottomSheet: SortBottomSheetComponent; /** * The 'opened/active' state of the fulltext filter input field @@ -87,6 +88,21 @@ export class OsSortFilterBarComponent { } } + /** + * 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) { + this.sortService.sortProperty = option.property; + } + /** * Constructor. Also creates a filtermenu component and a bottomSheet * @param translate @@ -106,7 +122,7 @@ export class OsSortFilterBarComponent { */ 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 { /** * 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): string { - if (this.sortService.sortProperty !== option.property) { - return ''; - } - return this.sortService.ascending ? 'arrow_downward' : 'arrow_upward'; + public getSortIcon(option: OsSortingOption): string { + return this.sortService.getSortIcon(option); } /** @@ -160,7 +171,7 @@ export class OsSortFilterBarComponent { * the property is used. * @param option */ - public getSortLabel(option: OsSortingItem): string { + public getSortLabel(option: OsSortingOption): string { if (option.label) { return option.label; } diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index f13797fc9..92b8b3008 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -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 {} diff --git a/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts b/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts index 947c33eeb..a5276fc13 100644 --- a/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts +++ b/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts @@ -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 implements OnInit { +export class AgendaListComponent extends ListViewBaseComponent + implements OnInit { /** * Determine the display columns in desktop view */ @@ -106,7 +107,7 @@ export class AgendaListComponent extends ListViewBaseComponent 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 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. * diff --git a/client/src/app/site/agenda/services/agenda-filter-list.service.ts b/client/src/app/site/agenda/services/agenda-filter-list.service.ts index a19c6ecab..2b02997a8 100644 --- a/client/src/app/site/agenda/services/agenda-filter-list.service.ts +++ b/client/src/app/site/agenda/services/agenda-filter-list.service.ts @@ -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 { - 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 { ] } ]; - 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 { - 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 + })); } } diff --git a/client/src/app/site/assignments/components/assignment-list/assignment-list.component.ts b/client/src/app/site/assignments/components/assignment-list/assignment-list.component.ts index 10c32e9ca..a08bcfabb 100644 --- a/client/src/app/site/assignments/components/assignment-list/assignment-list.component.ts +++ b/client/src/app/site/assignments/components/assignment-list/assignment-list.component.ts @@ -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 implements OnInit { +export class AssignmentListComponent + extends ListViewBaseComponent + implements OnInit { /** * The different phases of an assignment. Info is fetched from server */ @@ -57,7 +59,7 @@ export class AssignmentListComponent extends ListViewBaseComponent { - 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); } } diff --git a/client/src/app/site/assignments/services/assignment-sort-list.service.ts b/client/src/app/site/assignments/services/assignment-sort-list.service.ts index b42904dd0..ec7169b96 100644 --- a/client/src/app/site/assignments/services/assignment-sort-list.service.ts +++ b/client/src/app/site/assignments/services/assignment-sort-list.service.ts @@ -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 { - public sortOptions: OsSortingDefinition = { - 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[] = [ + { 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> { + return { + sortProperty: 'assignment', + sortAscending: true + }; + } } diff --git a/client/src/app/site/base/list-view-base.ts b/client/src/app/site/base/list-view-base.ts index e7b0d6022..3a2086a10 100644 --- a/client/src/app/site/base/list-view-base.ts +++ b/client/src/app/site/base/list-view-base.ts @@ -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 extends BaseViewComponent - implements OnDestroy { +export abstract class ListViewBaseComponent< + V extends BaseViewModel, + M extends BaseModel, + R extends BaseRepository +> 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, - public sortService?: BaseSortListService + protected modelFilterListService?: BaseFilterListService, + protected modelSortService?: BaseSortListService ) { super(titleService, translate, matSnackBar); this.selectedRows = []; @@ -114,40 +122,41 @@ export abstract class ListViewBaseComponent 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 { + 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 { diff --git a/client/src/app/site/history/components/history-list/history-list.component.ts b/client/src/app/site/history/components/history-list/history-list.component.ts index b50ecd3ce..0a215abca 100644 --- a/client/src/app/site/history/components/history-list/history-list.component.ts +++ b/client/src/app/site/history/components/history-list/history-list.component.ts @@ -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 implements OnInit { +export class HistoryListComponent extends ListViewBaseComponent + implements OnInit { /** * Subject determine when the custom timestamp subject changes */ @@ -51,7 +52,7 @@ export class HistoryListComponent extends ListViewBaseComponent implements OnInit { +export class MediafileListComponent extends ListViewBaseComponent + implements OnInit { /** * Holds the actions for logos. Updated via an observable */ @@ -108,7 +109,7 @@ export class MediafileListComponent extends ListViewBaseComponent { - 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]; } } diff --git a/client/src/app/site/mediafiles/services/mediafiles-sort-list.service.ts b/client/src/app/site/mediafiles/services/mediafiles-sort-list.service.ts index 92dc3d567..60022e821 100644 --- a/client/src/app/site/mediafiles/services/mediafiles-sort-list.service.ts +++ b/client/src/app/site/mediafiles/services/mediafiles-sort-list.service.ts @@ -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 { - public sortOptions: OsSortingDefinition = { - 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[] = [ + { 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> { + return { + sortProperty: 'title', + sortAscending: true + }; + } } diff --git a/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.html b/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.html index f9e02f038..0393b3e4c 100644 --- a/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.html +++ b/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.html @@ -41,10 +41,10 @@ Follow recommendations for all motions - +
- Motion + Motion {{ motion.getTitle() }} diff --git a/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.ts b/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.ts index 19b33bd34..1dddd3a1e 100644 --- a/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.ts +++ b/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.ts @@ -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 implements OnInit { +export class MotionBlockDetailComponent extends ListViewBaseComponent + 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 { - // 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 motion.isInFinalState() || !motion.recommendation_id); + if (this.dataSource.data) { + return this.dataSource.data.every(motion => motion.isInFinalState() || !motion.recommendation_id); } else { return false; } diff --git a/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.html b/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.html index 6fdf70e34..69b7f960b 100644 --- a/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.html +++ b/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.html @@ -51,10 +51,10 @@ -
+
- Projector + @@ -62,13 +62,17 @@ - Title + + Title + {{ block.title }} - Motions + + Motions + {{ getMotionAmount(block.motionBlock) }} @@ -76,7 +80,9 @@ - Menu + + Menu +