From aed0b51a6d390bf023e7afb20086e9342ea85f2f Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Wed, 6 Oct 2021 00:28:07 +0300 Subject: [PATCH] add new listing service with listing related methods --- .../listing/listing-component.directive.ts | 19 +-- src/lib/listing/services/entities.service.ts | 114 +++--------------- src/lib/listing/services/index.ts | 1 + src/lib/listing/services/listing.service.ts | 110 +++++++++++++++++ .../table-header/table-header.component.html | 8 +- .../table-header/table-header.component.ts | 10 +- src/lib/listing/table/table.component.html | 2 +- src/lib/listing/table/table.component.ts | 12 +- .../listing/workflow/workflow.component.html | 5 +- .../listing/workflow/workflow.component.ts | 16 +-- src/lib/utils/operators.ts | 7 +- 11 files changed, 168 insertions(+), 136 deletions(-) create mode 100644 src/lib/listing/services/listing.service.ts diff --git a/src/lib/listing/listing-component.directive.ts b/src/lib/listing/listing-component.directive.ts index 7f2203a..b065fd9 100644 --- a/src/lib/listing/listing-component.directive.ts +++ b/src/lib/listing/listing-component.directive.ts @@ -5,11 +5,11 @@ import { FilterService } from '../filtering'; import { SortingService } from '../sorting'; import { AutoUnsubscribe } from '../utils'; import { SearchService } from '../search'; -import { EntitiesService } from './services'; +import { EntitiesService, ListingService } from './services'; import { IListable, ListingMode, ListingModes, TableColumnConfig } from './models'; -export const DefaultListingServices = [FilterService, SearchService, EntitiesService, SortingService] as const; -export const DefaultListingServicesTmp = [FilterService, SearchService, SortingService] as const; +export const DefaultListingServicesTmp = [FilterService, SearchService, SortingService, ListingService] as const; +export const DefaultListingServices = [...DefaultListingServicesTmp, EntitiesService] as const; @Directive() export abstract class ListingComponent extends AutoUnsubscribe implements OnDestroy { @@ -17,6 +17,7 @@ export abstract class ListingComponent extends AutoUnsubscr readonly searchService = this._injector.get>(SearchService); readonly sortingService = this._injector.get>(SortingService); readonly entitiesService = this._injector.get>(EntitiesService); + readonly listingService = this._injector.get>(ListingService); readonly noMatch$ = this._noMatch$; readonly noContent$ = this._noContent$; @@ -46,30 +47,30 @@ export abstract class ListingComponent extends AutoUnsubscr private get _sortedDisplayedEntities$(): Observable { const sort = (entities: T[]) => this.sortingService.defaultSort(entities); - const sortedEntities = () => this.entitiesService.displayed$.pipe(map(sort)); + const sortedEntities = () => this.listingService.displayed$.pipe(map(sort)); return this.sortingService.sortingOption$.pipe(switchMap(sortedEntities)); } private get _noMatch$(): Observable { - return combineLatest([this.entitiesService.allLength$, this.entitiesService.displayedLength$]).pipe( + return combineLatest([this.entitiesService.allLength$, this.listingService.displayedLength$]).pipe( map(([hasEntities, hasDisplayedEntities]) => !!hasEntities && !hasDisplayedEntities), - distinctUntilChanged() + distinctUntilChanged(), ); } private get _noContent$(): Observable { return combineLatest([this._noMatch$, this.entitiesService.noData$]).pipe( map(([noMatch, noData]) => noMatch || noData), - distinctUntilChanged() + distinctUntilChanged(), ); } toggleEntitySelected(event: MouseEvent, entity: T): void { event.stopPropagation(); - this.entitiesService.select(entity); + this.listingService.select(entity); } isSelected(entity: T): boolean { - return this.entitiesService.isSelected(entity); + return this.listingService.isSelected(entity); } } diff --git a/src/lib/listing/services/entities.service.ts b/src/lib/listing/services/entities.service.ts index 6bab44f..9bd0823 100644 --- a/src/lib/listing/services/entities.service.ts +++ b/src/lib/listing/services/entities.service.ts @@ -1,18 +1,15 @@ import { Inject, Injectable, InjectionToken, Injector, Optional } from '@angular/core'; -import { BehaviorSubject, combineLatest, Observable, pipe, Subject } from 'rxjs'; +import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { distinctUntilChanged, map, tap } from 'rxjs/operators'; -import { FilterService, getFilteredEntities } from '../../filtering'; -import { SearchService } from '../../search'; import { IListable } from '../models'; import { GenericService } from '../../services'; - -const toLengthValue = (entities: unknown[]) => entities?.length ?? 0; -const getLength = pipe(map(toLengthValue), distinctUntilChanged()); +import { getLength } from '../../utils'; /** * This should be removed when refactoring is done */ const ENTITY_PATH = new InjectionToken('This is here for compatibility while refactoring things.'); +const ENTITY_CLASS = new InjectionToken('This is here for compatibility while refactoring things.'); @Injectable() /** @@ -21,88 +18,27 @@ const ENTITY_PATH = new InjectionToken('This is here for compatibility w * By default, if no interface is provided, I = E */ export class EntitiesService extends GenericService { - readonly displayed$: Observable; - readonly displayedLength$: Observable; readonly noData$: Observable; - readonly areAllSelected$: Observable; - readonly areSomeSelected$: Observable; - readonly notAllSelected$: Observable; - readonly selected$: Observable<(string | number)[]>; - readonly selectedEntities$: Observable; - readonly selectedLength$: Observable; readonly all$: Observable; readonly allLength$: Observable; readonly entityChanged$ = new Subject(); - private readonly _filterService = this._injector.get(FilterService); - private readonly _searchService = this._injector.get>(SearchService); private readonly _all$ = new BehaviorSubject([]); - private _displayed: E[] = []; - private readonly _selected$ = new BehaviorSubject<(string | number)[]>([]); constructor( protected readonly _injector: Injector, + @Optional() @Inject(ENTITY_CLASS) private readonly _entityClass: new (entityInterface: I) => E, @Optional() @Inject(ENTITY_PATH) protected readonly _defaultModelPath = '', ) { super(_injector, _defaultModelPath); this.all$ = this._all$.asObservable(); this.allLength$ = this._all$.pipe(getLength); - - this.displayed$ = this._getDisplayed$; - this.displayedLength$ = this.displayed$.pipe(getLength); - - this.selected$ = this._selected$.asObservable(); - this.selectedEntities$ = this._selected$.asObservable().pipe(map(() => this.selected)); - this.selectedLength$ = this._selected$.pipe(getLength); - this.noData$ = this._noData$; - this.areAllSelected$ = this._areAllSelected$; - this.areSomeSelected$ = this._areSomeSelected$; - this.notAllSelected$ = this._notAllSelected$; } get all(): E[] { return Object.values(this._all$.getValue()); } - get selected(): E[] { - const selectedIds = Object.values(this._selected$.getValue()); - return this.all.filter(a => selectedIds.indexOf(a.id) !== -1); - } - - private get _getDisplayed$(): Observable { - const { filterGroups$ } = this._filterService; - const { valueChanges$ } = this._searchService; - - return combineLatest([this.all$, filterGroups$, valueChanges$]).pipe( - map(([entities, filterGroups]) => getFilteredEntities(entities, filterGroups)), - map(entities => this._searchService.searchIn(entities)), - tap(displayed => { - this._displayed = displayed; - }), - ); - } - - private get _areAllSelected$(): Observable { - return combineLatest([this.displayedLength$, this.selectedLength$]).pipe( - map(([displayedLength, selectedLength]) => !!displayedLength && displayedLength === selectedLength), - distinctUntilChanged(), - ); - } - - private get _areSomeSelected$(): Observable { - return this.selectedLength$.pipe( - map(length => !!length), - distinctUntilChanged(), - ); - } - - private get _notAllSelected$(): Observable { - return combineLatest([this.areAllSelected$, this.areSomeSelected$]).pipe( - map(([allAreSelected, someAreSelected]) => !allAreSelected && someAreSelected), - distinctUntilChanged(), - ); - } - private get _noData$(): Observable { return this.allLength$.pipe( map(length => length === 0), @@ -110,38 +46,23 @@ export class EntitiesService extends GenericService< ); } - private get _allSelected() { - return this._displayed.length !== 0 && this._displayed.length === this.selected.length; + loadAll(): Observable { + return this.getAll().pipe( + map((entities: I[]) => entities.map(entity => new this._entityClass(entity))), + tap((entities: E[]) => this.setEntities(entities)), + ); + } + + loadAllIfNecessary(): Promise | void { + if (!this.all.length) { + return this.loadAll().toPromise(); + } } setEntities(newEntities: E[]): void { this._all$.next(newEntities); } - setSelected(newEntities: E[]): void { - const selectedIds = newEntities.map(e => e.id); - this._selected$.next(selectedIds); - } - - isSelected(entity: E): boolean { - return this.selected.indexOf(entity) !== -1; - } - - selectAll(): void { - if (this._allSelected) { - return this.setSelected([]); - } - this.setSelected(this._displayed); - } - - select(entity: E): void { - const currentEntityIdx = this.selected.indexOf(entity); - if (currentEntityIdx === -1) { - return this.setSelected([...this.selected, entity]); - } - this.setSelected(this.selected.filter((el, idx) => idx !== currentEntityIdx)); - } - find(id: string): E | undefined { return this.all.find(entity => entity.id === id); } @@ -150,11 +71,6 @@ export class EntitiesService extends GenericService< return this.all.some(entity => entity.id === id); } - updateSelection(): void { - const items = this._displayed.filter(item => this.selected.includes(item)); - this.setSelected(items); - } - replace(newEntity: E): void { const all = this.all.filter(item => item.id !== newEntity.id); all.push(newEntity); diff --git a/src/lib/listing/services/index.ts b/src/lib/listing/services/index.ts index 8d6e94c..2bd0d5c 100644 --- a/src/lib/listing/services/index.ts +++ b/src/lib/listing/services/index.ts @@ -1 +1,2 @@ export * from './entities.service'; +export * from './listing.service'; diff --git a/src/lib/listing/services/listing.service.ts b/src/lib/listing/services/listing.service.ts new file mode 100644 index 0000000..37abf8b --- /dev/null +++ b/src/lib/listing/services/listing.service.ts @@ -0,0 +1,110 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; +import { distinctUntilChanged, map, tap } from 'rxjs/operators'; +import { FilterService, getFilteredEntities } from '../../filtering'; +import { SearchService } from '../../search'; +import { IListable } from '../models'; +import { EntitiesService, getLength } from '@iqser/common-ui'; + +@Injectable() +export class ListingService { + readonly displayed$: Observable; + readonly displayedLength$: Observable; + readonly areAllSelected$: Observable; + readonly areSomeSelected$: Observable; + readonly notAllSelected$: Observable; + readonly selected$: Observable<(string | number)[]>; + readonly selectedEntities$: Observable; + readonly selectedLength$: Observable; + private _displayed: E[] = []; + private readonly _selected$ = new BehaviorSubject<(string | number)[]>([]); + + constructor( + private readonly _filterService: FilterService, + private readonly _searchService: SearchService, + private readonly _entitiesService: EntitiesService, + ) { + this.displayed$ = this._getDisplayed$; + this.displayedLength$ = this.displayed$.pipe(getLength); + + this.selected$ = this._selected$.asObservable(); + this.selectedEntities$ = this._selected$.asObservable().pipe(map(() => this.selected)); + this.selectedLength$ = this._selected$.pipe(getLength); + + this.areAllSelected$ = this._areAllSelected$; + this.areSomeSelected$ = this._areSomeSelected$; + this.notAllSelected$ = this._notAllSelected$; + } + + get selected(): E[] { + const selectedIds = Object.values(this._selected$.getValue()); + return this._entitiesService.all.filter(a => selectedIds.indexOf(a.id) !== -1); + } + + private get _getDisplayed$(): Observable { + const { filterGroups$ } = this._filterService; + const { valueChanges$ } = this._searchService; + + return combineLatest([this._entitiesService.all$, filterGroups$, valueChanges$]).pipe( + map(([entities, filterGroups]) => getFilteredEntities(entities, filterGroups)), + map(entities => this._searchService.searchIn(entities)), + tap(displayed => { + this._displayed = displayed; + }), + ); + } + + private get _areAllSelected$(): Observable { + return combineLatest([this.displayedLength$, this.selectedLength$]).pipe( + map(([displayedLength, selectedLength]) => !!displayedLength && displayedLength === selectedLength), + distinctUntilChanged(), + ); + } + + private get _areSomeSelected$(): Observable { + return this.selectedLength$.pipe( + map(length => !!length), + distinctUntilChanged(), + ); + } + + private get _notAllSelected$(): Observable { + return combineLatest([this.areAllSelected$, this.areSomeSelected$]).pipe( + map(([allAreSelected, someAreSelected]) => !allAreSelected && someAreSelected), + distinctUntilChanged(), + ); + } + + private get _allSelected() { + return this._displayed.length !== 0 && this._displayed.length === this.selected.length; + } + + setSelected(newEntities: E[]): void { + const selectedIds = newEntities.map(e => e.id); + this._selected$.next(selectedIds); + } + + isSelected(entity: E): boolean { + return this.selected.indexOf(entity) !== -1; + } + + selectAll(): void { + if (this._allSelected) { + return this.setSelected([]); + } + this.setSelected(this._displayed); + } + + select(entity: E): void { + const currentEntityIdx = this.selected.indexOf(entity); + if (currentEntityIdx === -1) { + return this.setSelected([...this.selected, entity]); + } + this.setSelected(this.selected.filter((el, idx) => idx !== currentEntityIdx)); + } + + updateSelection(): void { + const items = this._displayed.filter(item => this.selected.includes(item)); + this.setSelected(items); + } +} diff --git a/src/lib/listing/table-header/table-header.component.html b/src/lib/listing/table-header/table-header.component.html index 8d699f2..31cf36a 100644 --- a/src/lib/listing/table-header/table-header.component.html +++ b/src/lib/listing/table-header/table-header.component.html @@ -1,13 +1,13 @@
- {{ tableHeaderLabel | translate: {length: totalSize || (entitiesService.displayedLength$ | async)} }} + {{ tableHeaderLabel | translate: { length: totalSize || (listingService.displayedLength$ | async) } }} diff --git a/src/lib/listing/table-header/table-header.component.ts b/src/lib/listing/table-header/table-header.component.ts index 241ace1..ffe2d44 100644 --- a/src/lib/listing/table-header/table-header.component.ts +++ b/src/lib/listing/table-header/table-header.component.ts @@ -1,14 +1,14 @@ import { ChangeDetectionStrategy, Component, Input, TemplateRef } from '@angular/core'; import { Required } from '../../utils'; import { FilterService } from '../../filtering'; -import { EntitiesService } from '../services'; +import { EntitiesService, ListingService } from '../services'; import { IListable, ListingMode, ListingModes, TableColumnConfig } from '../models'; @Component({ selector: 'iqser-table-header', templateUrl: './table-header.component.html', styleUrls: ['./table-header.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class TableHeaderComponent { readonly listingModes = ListingModes; @@ -21,5 +21,9 @@ export class TableHeaderComponent { @Input() totalSize?: number; @Input() bulkActions?: TemplateRef; - constructor(readonly entitiesService: EntitiesService, readonly filterService: FilterService) {} + constructor( + readonly entitiesService: EntitiesService, + readonly listingService: ListingService, + readonly filterService: FilterService, + ) {} } diff --git a/src/lib/listing/table/table.component.html b/src/lib/listing/table/table.component.html index 76341b7..013506d 100644 --- a/src/lib/listing/table/table.component.html +++ b/src/lib/listing/table/table.component.html @@ -12,7 +12,7 @@ implements OnInit { readonly listingModes = ListingModes; @@ -49,14 +50,11 @@ export class TableComponent implements OnInit { @ViewChild(CdkVirtualScrollViewport, { static: true }) readonly scrollViewport!: CdkVirtualScrollViewport; constructor( - @Inject(forwardRef(() => ListingComponent)) private _parent: ListingComponent, - private readonly _hostRef: ViewContainerRef + @Inject(forwardRef(() => ListingComponent)) readonly listingComponent: ListingComponent, + private readonly _hostRef: ViewContainerRef, + readonly entitiesService: EntitiesService, ) {} - get listingComponent(): ListingComponent { - return this._parent; - } - get tableColumnConfigs(): readonly TableColumnConfig[] { return this.listingComponent.tableColumnConfigs; } diff --git a/src/lib/listing/workflow/workflow.component.html b/src/lib/listing/workflow/workflow.component.html index b506131..768a865 100644 --- a/src/lib/listing/workflow/workflow.component.html +++ b/src/lib/listing/workflow/workflow.component.html @@ -7,7 +7,7 @@ -
+
- diff --git a/src/lib/listing/workflow/workflow.component.ts b/src/lib/listing/workflow/workflow.component.ts index 14ea829..0fa2113 100644 --- a/src/lib/listing/workflow/workflow.component.ts +++ b/src/lib/listing/workflow/workflow.component.ts @@ -15,7 +15,8 @@ import { ListingComponent } from '../listing-component.directive'; import { CdkDrag, CdkDragDrop, CdkDropList } from '@angular/cdk/drag-drop'; import { Required } from '../../utils'; import { LoadingService } from '../../loading'; -import {IListable} from "../models"; +import { IListable } from '../models'; +import { EntitiesService, ListingService } from '../services'; interface WorkflowColumn { key: K; @@ -60,12 +61,11 @@ export class WorkflowComponent implements private _existingEntities: { [key: string]: T } = {}; constructor( - @Inject(forwardRef(() => ListingComponent)) private _parent: ListingComponent, - private readonly _loadingService: LoadingService - ) {} - - get listingComponent(): ListingComponent { - return this._parent; + @Inject(forwardRef(() => ListingComponent)) readonly listingComponent: ListingComponent, + private readonly _loadingService: LoadingService, + readonly listingService: ListingService, + readonly entitiesService: EntitiesService, + ) { } get tableHeaderLabel(): string | undefined { @@ -90,7 +90,7 @@ export class WorkflowComponent implements } ngOnInit(): void { - this.listingComponent.entitiesService.displayed$.subscribe(entities => this._updateConfigItems(entities)); + this.listingService.displayed$.subscribe(entities => this._updateConfigItems(entities)); } canMoveTo(column: WorkflowColumn): (item: CdkDrag) => boolean { diff --git a/src/lib/utils/operators.ts b/src/lib/utils/operators.ts index 3e6c9d0..b6abbc4 100644 --- a/src/lib/utils/operators.ts +++ b/src/lib/utils/operators.ts @@ -1,5 +1,5 @@ -import { map } from 'rxjs/operators'; -import { OperatorFunction } from 'rxjs'; +import { distinctUntilChanged, map } from 'rxjs/operators'; +import { OperatorFunction, pipe } from 'rxjs'; export function get(predicate: (value: T, index: number) => boolean): OperatorFunction { return map(entities => entities.find(predicate)); @@ -8,3 +8,6 @@ export function get(predicate: (value: T, index: number) => boolean): Operato export function any(predicate: (value: T, index: number) => boolean): OperatorFunction { return map(entities => entities.some(predicate)); } + +export const toLengthValue = (entities: unknown[]): number => entities?.length ?? 0; +export const getLength = pipe(map(toLengthValue), distinctUntilChanged());