From 1ec5dfbb887f09ae74d7c27b24301826078ab7de Mon Sep 17 00:00:00 2001 From: GabrielMeyer Date: Wed, 4 Sep 2019 17:42:03 +0200 Subject: [PATCH] Implements the possibility to sort trees by a given property - Separates the base sorting function to a lower service, called `base-sort.service`. - Adds the `tree-sort.service`. --- .../ui-services/base-sort-list.service.ts | 99 +------------- .../ui-services/base-sort.service.spec.ts | 19 +++ .../app/core/ui-services/base-sort.service.ts | 124 ++++++++++++++++++ .../ui-services/tree-sort.service.spec.ts | 19 +++ .../app/core/ui-services/tree-sort.service.ts | 53 ++++++++ .../sort-filter-bar.component.ts | 2 +- .../sorting-tree/sorting-tree.component.ts | 39 +++++- .../services/assignment-sort-list.service.ts | 3 +- .../src/app/site/base/sort-tree.component.ts | 6 + .../services/mediafiles-sort-list.service.ts | 3 +- .../call-list/call-list.component.html | 8 ++ .../modules/call-list/call-list.component.ts | 13 ++ .../services/amendment-sort-list.service.ts | 2 +- .../services/motion-block-sort.service.ts | 3 +- .../services/motion-sort-list.service.ts | 3 +- .../users/services/user-sort-list.service.ts | 3 +- 16 files changed, 299 insertions(+), 100 deletions(-) create mode 100644 client/src/app/core/ui-services/base-sort.service.spec.ts create mode 100644 client/src/app/core/ui-services/base-sort.service.ts create mode 100644 client/src/app/core/ui-services/tree-sort.service.spec.ts create mode 100644 client/src/app/core/ui-services/tree-sort.service.ts 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 6f798a62f..2a4b6ecae 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,32 +1,15 @@ import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { BaseSortService, OsSortingDefinition, OsSortingOption } from './base-sort.service'; import { BaseViewModel } from '../../site/base/base-view-model'; import { OpenSlidesStatusService } from '../core-services/openslides-status.service'; 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; -} - -/** - * 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 OsSortingOption { - property: keyof V; - label?: string; - sortFn?: (itemA: V, itemB: V, ascending: boolean, intl?: Intl.Collator) => number; -} - /** * Base class for generic sorting purposes */ -export abstract class BaseSortListService { +export abstract class BaseSortListService extends BaseSortService { /** * The data to be sorted. See also the setter for {@link data} */ @@ -60,11 +43,6 @@ export abstract class BaseSortListService { */ protected abstract readonly storageKey: string; - /** - * The sorting function according to current settings. - */ - public sortFn?: (a: V, b: V, ascending: boolean, intl?: Intl.Collator) => number; - /** * Set the current sorting order * @@ -131,7 +109,9 @@ export abstract class BaseSortListService { protected translate: TranslateService, private store: StorageService, private OSStatus: OpenSlidesStatusService - ) {} + ) { + super(translate); + } /** * Enforce children to implement a function that returns their sorting options @@ -229,79 +209,14 @@ export abstract class BaseSortListService { } } - /** - * Helper function to determine false-like values (if they are not boolean) - * @param property - */ - private isFalsy(property: any): boolean { - return property === null || property === undefined || property === 0 || property === ''; - } - /** * Recreates the sorting function. Is supposed to be called on init and * every time the sorting (property, ascending/descending) or the language changes */ protected updateSortedData(): void { - if (this.inputData && this.sortDefinition) { - const property = this.sortProperty as string; - - const intl = new Intl.Collator(this.translate.currentLang, { - numeric: true, - ignorePunctuation: true, - sensitivity: 'base' - }); - + if (this.inputData) { this.inputData.sort((itemA, itemB) => { - // always sort falsy values to the bottom - if (this.isFalsy(itemA[property]) && this.isFalsy(itemB[property])) { - return 0; - } else if (this.isFalsy(itemA[property])) { - return 1; - } else if (this.isFalsy(itemB[property])) { - return -1; - } - - const firstProperty = this.ascending ? itemA[property] : itemB[property]; - const secondProperty = this.ascending ? itemB[property] : itemA[property]; - - if (this.sortFn) { - return this.sortFn(itemA, itemB, this.ascending, intl); - } else { - switch (typeof firstProperty) { - case 'boolean': - if (!firstProperty && secondProperty) { - return -1; - } else { - return 1; - } - case 'number': - return firstProperty > secondProperty ? 1 : -1; - case 'string': - if (!!firstProperty && !secondProperty) { - return -1; - } else if (!firstProperty && !!secondProperty) { - return 1; - } else if ((!secondProperty && !secondProperty) || firstProperty === secondProperty) { - return 0; - } else { - 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; - } - } + return this.sortItems(itemA, itemB, this.sortProperty, this.ascending); }); this.outputSubject.next(this.inputData); } diff --git a/client/src/app/core/ui-services/base-sort.service.spec.ts b/client/src/app/core/ui-services/base-sort.service.spec.ts new file mode 100644 index 000000000..2c508dc11 --- /dev/null +++ b/client/src/app/core/ui-services/base-sort.service.spec.ts @@ -0,0 +1,19 @@ +import { TestBed } from '@angular/core/testing'; + +import { BaseSortService } from './base-sort.service'; +import { E2EImportsModule } from '../../../e2e-imports.module'; + +describe('BaseSortService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [E2EImportsModule], + providers: [BaseSortService] + }); + }); + + // TODO testing (does not work without injecting a BaseViewComponent) + // it('should be created', () => { + // const service: BaseSortService = TestBed.get(BaseSortService); + // expect(service).toBeTruthy(); + // }); +}); diff --git a/client/src/app/core/ui-services/base-sort.service.ts b/client/src/app/core/ui-services/base-sort.service.ts new file mode 100644 index 000000000..547885f5e --- /dev/null +++ b/client/src/app/core/ui-services/base-sort.service.ts @@ -0,0 +1,124 @@ +import { Injectable } from '@angular/core'; + +import { TranslateService } from '@ngx-translate/core'; + +import { Identifiable } from 'app/shared/models/base/identifiable'; +import { Displayable } from 'app/site/base/displayable'; + +export type SortDefinition = keyof T | OsSortingDefinition; + +/** + * Describes the sorting columns of an associated ListView, and their state. + */ +export interface OsSortingDefinition { + sortProperty: keyof T; + 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 OsSortingOption { + property: keyof T; + label?: string; + sortFn?: (itemA: T, itemB: T, ascending: boolean, intl?: Intl.Collator) => number; +} + +/** + * Base sorting service with main functionality for sorting. + * + * Extends sorting services to sort with a consistent function. + */ +@Injectable({ + providedIn: 'root' +}) +export abstract class BaseSortService { + /** + * The sorting function according to current settings. + */ + public sortFn?: (a: T, b: T, ascending: boolean, intl?: Intl.Collator) => number; + + /** + * The international localisation. + */ + protected intl: Intl.Collator; + + /** + * Constructor. + * Pass the `TranslatorService`. + */ + public constructor(protected translate: TranslateService) { + this.intl = new Intl.Collator(translate.currentLang, { + numeric: true, + ignorePunctuation: true, + sensitivity: 'base' + }); + } + + /** + * Helper function to determine false-like values (if they are not boolean) + * @param property + */ + private isFalsy(property: any): boolean { + return property === null || property === undefined || property === 0 || property === ''; + } + + /** + * Recreates the sorting function. Is supposed to be called on init and + * every time the sorting (property, ascending/descending) or the language changes + */ + protected sortItems(itemA: T, itemB: T, sortProperty: keyof T, ascending: boolean = true): number { + // always sort falsy values to the bottom + const property = sortProperty as string; + if (this.isFalsy(itemA[property]) && this.isFalsy(itemB[property])) { + return 0; + } else if (this.isFalsy(itemA[property])) { + return 1; + } else if (this.isFalsy(itemB[property])) { + return -1; + } + + const firstProperty = ascending ? itemA[property] : itemB[property]; + const secondProperty = ascending ? itemB[property] : itemA[property]; + + if (this.sortFn) { + return this.sortFn(itemA, itemB, ascending, this.intl); + } else { + switch (typeof firstProperty) { + case 'boolean': + if (!firstProperty && secondProperty) { + return -1; + } else { + return 1; + } + case 'number': + return firstProperty > secondProperty ? 1 : -1; + case 'string': + if (!!firstProperty && !secondProperty) { + return -1; + } else if (!firstProperty && !!secondProperty) { + return 1; + } else if ((!secondProperty && !secondProperty) || firstProperty === secondProperty) { + return 0; + } else { + return this.intl.compare(firstProperty, secondProperty); + } + case 'function': + const a = firstProperty(); + const b = secondProperty(); + return this.intl.compare(a, b); + case 'object': + if (firstProperty instanceof Date) { + return firstProperty > secondProperty ? 1 : -1; + } else { + return this.intl.compare(firstProperty.toString(), secondProperty.toString()); + } + case 'undefined': + return 1; + default: + return -1; + } + } + } +} diff --git a/client/src/app/core/ui-services/tree-sort.service.spec.ts b/client/src/app/core/ui-services/tree-sort.service.spec.ts new file mode 100644 index 000000000..f2830f515 --- /dev/null +++ b/client/src/app/core/ui-services/tree-sort.service.spec.ts @@ -0,0 +1,19 @@ +import { TestBed } from '@angular/core/testing'; + +import { E2EImportsModule } from '../../../e2e-imports.module'; +import { TreeSortService } from './tree-sort.service'; + +describe('TreeSortService', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [E2EImportsModule], + providers: [TreeSortService] + }) + ); + + // TODO testing (does not work without injecting a BaseViewComponent) + // it('should be created', () => { + // const service: TreeSortService = TestBed.get(TreeSortService); + // expect(service).toBeTruthy(); + // }); +}); diff --git a/client/src/app/core/ui-services/tree-sort.service.ts b/client/src/app/core/ui-services/tree-sort.service.ts new file mode 100644 index 000000000..1de0a88c6 --- /dev/null +++ b/client/src/app/core/ui-services/tree-sort.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@angular/core'; + +import { TranslateService } from '@ngx-translate/core'; + +import { Identifiable } from 'app/shared/models/base/identifiable'; +import { Displayable } from 'app/site/base/displayable'; +import { BaseSortService } from './base-sort.service'; +import { FlatNode } from './tree.service'; + +/** + * Sorting service for trees. + * + * Contains base functions to sort a tree by different properties. + */ +@Injectable({ + providedIn: 'root' +}) +export class TreeSortService extends BaseSortService { + /** + * Constructor. + * Calls the `super()`-method. + * + * @param translate The reference to the `TranslateService` + */ + public constructor(protected translate: TranslateService) { + super(translate); + } + + /** + * Function to sort the passed source of a tree + * and resets some properties like `level`, `expandable`, `position`. + * + * @param sourceData The source array of `FlatNode`s. + * @param property The property, the array will be sorted by. + * @param ascending Boolean, if the array should be sorted in ascending order. + * + * @returns {FlatNode[]} The sorted array. + */ + public sortTree(sourceData: FlatNode[], property: keyof T, ascending: boolean = true): FlatNode[] { + return sourceData + .sort((nodeA, nodeB) => { + const itemA = nodeA.item; + const itemB = nodeB.item; + return this.sortItems(itemA, itemB, property, ascending); + }) + .map((node, index) => { + node.level = 0; + node.position = index; + node.expandable = false; + return node; + }); + } +} diff --git a/client/src/app/shared/components/sort-filter-bar/sort-filter-bar.component.ts b/client/src/app/shared/components/sort-filter-bar/sort-filter-bar.component.ts index c11eb5229..828cd568f 100644 --- a/client/src/app/shared/components/sort-filter-bar/sort-filter-bar.component.ts +++ b/client/src/app/shared/components/sort-filter-bar/sort-filter-bar.component.ts @@ -4,8 +4,8 @@ import { MatBottomSheet } from '@angular/material/bottom-sheet'; import { TranslateService } from '@ngx-translate/core'; import { BaseFilterListService, OsFilterIndicator } from 'app/core/ui-services/base-filter-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 { OsSortingOption } from 'app/core/ui-services/base-sort.service'; import { ViewportService } from 'app/core/ui-services/viewport.service'; import { BaseViewModel } from 'app/site/base/base-view-model'; import { FilterMenuComponent } from './filter-menu/filter-menu.component'; diff --git a/client/src/app/shared/components/sorting-tree/sorting-tree.component.ts b/client/src/app/shared/components/sorting-tree/sorting-tree.component.ts index f9d12b32c..1113b064f 100644 --- a/client/src/app/shared/components/sorting-tree/sorting-tree.component.ts +++ b/client/src/app/shared/components/sorting-tree/sorting-tree.component.ts @@ -6,6 +6,8 @@ import { Component, ContentChild, EventEmitter, Input, OnDestroy, OnInit, Output import { Observable, Subscription } from 'rxjs'; import { auditTime } from 'rxjs/operators'; +import { SortDefinition } from 'app/core/ui-services/base-sort.service'; +import { TreeSortService } from 'app/core/ui-services/tree-sort.service'; import { FlatNode, TreeIdNode, TreeService } from 'app/core/ui-services/tree.service'; import { Identifiable } from 'app/shared/models/base/identifiable'; import { Displayable } from 'app/site/base/displayable'; @@ -175,6 +177,19 @@ export class SortingTreeComponent implemen }); } + /** + * Setter for sorting functions. + * + * @param predicate An `EventEmitter` to push to sort the tree by the passed property. + * It should pass a `SortDefinition`. + */ + @Input() + public set sortingDefinition(predicate: EventEmitter>) { + predicate.subscribe((event: SortDefinition) => { + this.resolveSortingPredicate(event); + }); + } + /** * EventEmitter to send info if changes has been made. */ @@ -197,8 +212,9 @@ export class SortingTreeComponent implemen * Constructor * * @param treeService Service to get data from store and build the tree nodes. + * @param sortService Service to sort tree nodes by their given items. */ - public constructor(private treeService: TreeService) {} + public constructor(private treeService: TreeService, private sortService: TreeSortService) {} /** * On init method @@ -947,6 +963,27 @@ export class SortingTreeComponent implemen return willFiltered; } + /** + * Function to sort the given source-array by the passed property of the underlying items ``. + * + * @param event `SortDefinition` - can be only a `keyof T` or + * an object defining the property and whether ascending order or not: + * `{ property: keyof T, ascending: boolean }`. + */ + private resolveSortingPredicate(event: SortDefinition): void { + this.removeSubscription(); + const sortProperty = typeof event === 'object' ? event.sortProperty : event; + const sortAscending = typeof event === 'object' ? event.sortAscending : true; + + this.osTreeData = this.sortService.sortTree(this.osTreeData, sortProperty, sortAscending); + this.checkActiveFilters(); + + this.dataSource = null; + this.dataSource = new ArrayDataSource(this.osTreeData); + + this.madeChanges(true); + } + /** * Function to check if a node has children. */ 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 95c38d34d..0dabff215 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 @@ -4,7 +4,8 @@ import { TranslateService } from '@ngx-translate/core'; import { OpenSlidesStatusService } from 'app/core/core-services/openslides-status.service'; import { StorageService } from 'app/core/core-services/storage.service'; -import { BaseSortListService, OsSortingDefinition, OsSortingOption } from 'app/core/ui-services/base-sort-list.service'; +import { BaseSortListService } from 'app/core/ui-services/base-sort-list.service'; +import { OsSortingDefinition, OsSortingOption } from 'app/core/ui-services/base-sort.service'; import { ViewAssignment } from '../models/view-assignment'; /** diff --git a/client/src/app/site/base/sort-tree.component.ts b/client/src/app/site/base/sort-tree.component.ts index 8d5f7c076..e440ab5af 100644 --- a/client/src/app/site/base/sort-tree.component.ts +++ b/client/src/app/site/base/sort-tree.component.ts @@ -4,6 +4,7 @@ import { Title } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; +import { SortDefinition } from 'app/core/ui-services/base-sort.service'; import { PromptService } from 'app/core/ui-services/prompt.service'; import { SortingTreeComponent } from 'app/shared/components/sorting-tree/sorting-tree.component'; import { Identifiable } from 'app/shared/models/base/identifiable'; @@ -40,6 +41,11 @@ export abstract class SortTreeViewComponent extends Bas */ public readonly changeFilter: EventEmitter<(item: V) => boolean> = new EventEmitter<(item: V) => boolean>(); + /** + * Emitter to notice the `tree-sorting.service` for sorting the data-source. + */ + public readonly forceSort = new EventEmitter>(); + /** * Boolean to check if changes has been made. */ 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 3426dd02f..c116352df 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 @@ -4,7 +4,8 @@ import { TranslateService } from '@ngx-translate/core'; import { OpenSlidesStatusService } from 'app/core/core-services/openslides-status.service'; import { StorageService } from 'app/core/core-services/storage.service'; -import { BaseSortListService, OsSortingDefinition, OsSortingOption } from 'app/core/ui-services/base-sort-list.service'; +import { BaseSortListService } from 'app/core/ui-services/base-sort-list.service'; +import { OsSortingDefinition, OsSortingOption } from 'app/core/ui-services/base-sort.service'; import { ViewMediafile } from '../models/view-mediafile'; /** diff --git a/client/src/app/site/motions/modules/call-list/call-list.component.html b/client/src/app/site/motions/modules/call-list/call-list.component.html index 17e965eda..bfa130237 100644 --- a/client/src/app/site/motions/modules/call-list/call-list.component.html +++ b/client/src/app/site/motions/modules/call-list/call-list.component.html @@ -82,6 +82,7 @@ (hasChanged)="receiveChanges($event)" [model]="motionsObservable" [filterChange]="changeFilter" + [sortingDefinition]="forceSort" >
@@ -100,6 +101,13 @@ + + + +