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