add new listing service with listing related methods

This commit is contained in:
Dan Percic 2021-10-06 00:28:07 +03:00
parent 916bf9bda3
commit aed0b51a6d
11 changed files with 168 additions and 136 deletions

View File

@ -5,11 +5,11 @@ import { FilterService } from '../filtering';
import { SortingService } from '../sorting'; import { SortingService } from '../sorting';
import { AutoUnsubscribe } from '../utils'; import { AutoUnsubscribe } from '../utils';
import { SearchService } from '../search'; import { SearchService } from '../search';
import { EntitiesService } from './services'; import { EntitiesService, ListingService } from './services';
import { IListable, ListingMode, ListingModes, TableColumnConfig } from './models'; import { IListable, ListingMode, ListingModes, TableColumnConfig } from './models';
export const DefaultListingServices = [FilterService, SearchService, EntitiesService, SortingService] as const; export const DefaultListingServicesTmp = [FilterService, SearchService, SortingService, ListingService] as const;
export const DefaultListingServicesTmp = [FilterService, SearchService, SortingService] as const; export const DefaultListingServices = [...DefaultListingServicesTmp, EntitiesService] as const;
@Directive() @Directive()
export abstract class ListingComponent<T extends IListable> extends AutoUnsubscribe implements OnDestroy { export abstract class ListingComponent<T extends IListable> extends AutoUnsubscribe implements OnDestroy {
@ -17,6 +17,7 @@ export abstract class ListingComponent<T extends IListable> extends AutoUnsubscr
readonly searchService = this._injector.get<SearchService<T>>(SearchService); readonly searchService = this._injector.get<SearchService<T>>(SearchService);
readonly sortingService = this._injector.get<SortingService<T>>(SortingService); readonly sortingService = this._injector.get<SortingService<T>>(SortingService);
readonly entitiesService = this._injector.get<EntitiesService<T>>(EntitiesService); readonly entitiesService = this._injector.get<EntitiesService<T>>(EntitiesService);
readonly listingService = this._injector.get<ListingService<T>>(ListingService);
readonly noMatch$ = this._noMatch$; readonly noMatch$ = this._noMatch$;
readonly noContent$ = this._noContent$; readonly noContent$ = this._noContent$;
@ -46,30 +47,30 @@ export abstract class ListingComponent<T extends IListable> extends AutoUnsubscr
private get _sortedDisplayedEntities$(): Observable<readonly T[]> { private get _sortedDisplayedEntities$(): Observable<readonly T[]> {
const sort = (entities: T[]) => this.sortingService.defaultSort(entities); 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)); return this.sortingService.sortingOption$.pipe(switchMap(sortedEntities));
} }
private get _noMatch$(): Observable<boolean> { private get _noMatch$(): Observable<boolean> {
return combineLatest([this.entitiesService.allLength$, this.entitiesService.displayedLength$]).pipe( return combineLatest([this.entitiesService.allLength$, this.listingService.displayedLength$]).pipe(
map(([hasEntities, hasDisplayedEntities]) => !!hasEntities && !hasDisplayedEntities), map(([hasEntities, hasDisplayedEntities]) => !!hasEntities && !hasDisplayedEntities),
distinctUntilChanged() distinctUntilChanged(),
); );
} }
private get _noContent$(): Observable<boolean> { private get _noContent$(): Observable<boolean> {
return combineLatest([this._noMatch$, this.entitiesService.noData$]).pipe( return combineLatest([this._noMatch$, this.entitiesService.noData$]).pipe(
map(([noMatch, noData]) => noMatch || noData), map(([noMatch, noData]) => noMatch || noData),
distinctUntilChanged() distinctUntilChanged(),
); );
} }
toggleEntitySelected(event: MouseEvent, entity: T): void { toggleEntitySelected(event: MouseEvent, entity: T): void {
event.stopPropagation(); event.stopPropagation();
this.entitiesService.select(entity); this.listingService.select(entity);
} }
isSelected(entity: T): boolean { isSelected(entity: T): boolean {
return this.entitiesService.isSelected(entity); return this.listingService.isSelected(entity);
} }
} }

View File

@ -1,18 +1,15 @@
import { Inject, Injectable, InjectionToken, Injector, Optional } from '@angular/core'; 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 { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { FilterService, getFilteredEntities } from '../../filtering';
import { SearchService } from '../../search';
import { IListable } from '../models'; import { IListable } from '../models';
import { GenericService } from '../../services'; import { GenericService } from '../../services';
import { getLength } from '../../utils';
const toLengthValue = (entities: unknown[]) => entities?.length ?? 0;
const getLength = pipe(map(toLengthValue), distinctUntilChanged());
/** /**
* This should be removed when refactoring is done * This should be removed when refactoring is done
*/ */
const ENTITY_PATH = new InjectionToken<string>('This is here for compatibility while refactoring things.'); const ENTITY_PATH = new InjectionToken<string>('This is here for compatibility while refactoring things.');
const ENTITY_CLASS = new InjectionToken<string>('This is here for compatibility while refactoring things.');
@Injectable() @Injectable()
/** /**
@ -21,88 +18,27 @@ const ENTITY_PATH = new InjectionToken<string>('This is here for compatibility w
* By default, if no interface is provided, I = E * By default, if no interface is provided, I = E
*/ */
export class EntitiesService<E extends IListable, I = E> extends GenericService<I> { export class EntitiesService<E extends IListable, I = E> extends GenericService<I> {
readonly displayed$: Observable<E[]>;
readonly displayedLength$: Observable<number>;
readonly noData$: Observable<boolean>; readonly noData$: Observable<boolean>;
readonly areAllSelected$: Observable<boolean>;
readonly areSomeSelected$: Observable<boolean>;
readonly notAllSelected$: Observable<boolean>;
readonly selected$: Observable<(string | number)[]>;
readonly selectedEntities$: Observable<E[]>;
readonly selectedLength$: Observable<number>;
readonly all$: Observable<E[]>; readonly all$: Observable<E[]>;
readonly allLength$: Observable<number>; readonly allLength$: Observable<number>;
readonly entityChanged$ = new Subject<E>(); readonly entityChanged$ = new Subject<E>();
private readonly _filterService = this._injector.get(FilterService);
private readonly _searchService = this._injector.get<SearchService<E>>(SearchService);
private readonly _all$ = new BehaviorSubject<E[]>([]); private readonly _all$ = new BehaviorSubject<E[]>([]);
private _displayed: E[] = [];
private readonly _selected$ = new BehaviorSubject<(string | number)[]>([]);
constructor( constructor(
protected readonly _injector: Injector, protected readonly _injector: Injector,
@Optional() @Inject(ENTITY_CLASS) private readonly _entityClass: new (entityInterface: I) => E,
@Optional() @Inject(ENTITY_PATH) protected readonly _defaultModelPath = '', @Optional() @Inject(ENTITY_PATH) protected readonly _defaultModelPath = '',
) { ) {
super(_injector, _defaultModelPath); super(_injector, _defaultModelPath);
this.all$ = this._all$.asObservable(); this.all$ = this._all$.asObservable();
this.allLength$ = this._all$.pipe(getLength); 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.noData$ = this._noData$;
this.areAllSelected$ = this._areAllSelected$;
this.areSomeSelected$ = this._areSomeSelected$;
this.notAllSelected$ = this._notAllSelected$;
} }
get all(): E[] { get all(): E[] {
return Object.values(this._all$.getValue()); 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<E[]> {
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<boolean> {
return combineLatest([this.displayedLength$, this.selectedLength$]).pipe(
map(([displayedLength, selectedLength]) => !!displayedLength && displayedLength === selectedLength),
distinctUntilChanged(),
);
}
private get _areSomeSelected$(): Observable<boolean> {
return this.selectedLength$.pipe(
map(length => !!length),
distinctUntilChanged(),
);
}
private get _notAllSelected$(): Observable<boolean> {
return combineLatest([this.areAllSelected$, this.areSomeSelected$]).pipe(
map(([allAreSelected, someAreSelected]) => !allAreSelected && someAreSelected),
distinctUntilChanged(),
);
}
private get _noData$(): Observable<boolean> { private get _noData$(): Observable<boolean> {
return this.allLength$.pipe( return this.allLength$.pipe(
map(length => length === 0), map(length => length === 0),
@ -110,38 +46,23 @@ export class EntitiesService<E extends IListable, I = E> extends GenericService<
); );
} }
private get _allSelected() { loadAll(): Observable<E[]> {
return this._displayed.length !== 0 && this._displayed.length === this.selected.length; return this.getAll().pipe(
map((entities: I[]) => entities.map(entity => new this._entityClass(entity))),
tap((entities: E[]) => this.setEntities(entities)),
);
}
loadAllIfNecessary(): Promise<unknown> | void {
if (!this.all.length) {
return this.loadAll().toPromise();
}
} }
setEntities(newEntities: E[]): void { setEntities(newEntities: E[]): void {
this._all$.next(newEntities); 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 { find(id: string): E | undefined {
return this.all.find(entity => entity.id === id); return this.all.find(entity => entity.id === id);
} }
@ -150,11 +71,6 @@ export class EntitiesService<E extends IListable, I = E> extends GenericService<
return this.all.some(entity => entity.id === id); 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 { replace(newEntity: E): void {
const all = this.all.filter(item => item.id !== newEntity.id); const all = this.all.filter(item => item.id !== newEntity.id);
all.push(newEntity); all.push(newEntity);

View File

@ -1 +1,2 @@
export * from './entities.service'; export * from './entities.service';
export * from './listing.service';

View File

@ -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<E extends IListable> {
readonly displayed$: Observable<E[]>;
readonly displayedLength$: Observable<number>;
readonly areAllSelected$: Observable<boolean>;
readonly areSomeSelected$: Observable<boolean>;
readonly notAllSelected$: Observable<boolean>;
readonly selected$: Observable<(string | number)[]>;
readonly selectedEntities$: Observable<E[]>;
readonly selectedLength$: Observable<number>;
private _displayed: E[] = [];
private readonly _selected$ = new BehaviorSubject<(string | number)[]>([]);
constructor(
private readonly _filterService: FilterService,
private readonly _searchService: SearchService<E>,
private readonly _entitiesService: EntitiesService<E>,
) {
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<E[]> {
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<boolean> {
return combineLatest([this.displayedLength$, this.selectedLength$]).pipe(
map(([displayedLength, selectedLength]) => !!displayedLength && displayedLength === selectedLength),
distinctUntilChanged(),
);
}
private get _areSomeSelected$(): Observable<boolean> {
return this.selectedLength$.pipe(
map(length => !!length),
distinctUntilChanged(),
);
}
private get _notAllSelected$(): Observable<boolean> {
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);
}
}

View File

@ -1,13 +1,13 @@
<div [class.selection-enabled]="selectionEnabled" class="header-item"> <div [class.selection-enabled]="selectionEnabled" class="header-item">
<iqser-round-checkbox <iqser-round-checkbox
(click)="entitiesService.selectAll()" (click)="listingService.selectAll()"
*ngIf="selectionEnabled" *ngIf="selectionEnabled"
[active]="entitiesService.areAllSelected$ | async" [active]="listingService.areAllSelected$ | async"
[indeterminate]="entitiesService.notAllSelected$ | async" [indeterminate]="listingService.notAllSelected$ | async"
></iqser-round-checkbox> ></iqser-round-checkbox>
<span class="all-caps-label"> <span class="all-caps-label">
{{ tableHeaderLabel | translate: {length: totalSize || (entitiesService.displayedLength$ | async)} }} {{ tableHeaderLabel | translate: { length: totalSize || (listingService.displayedLength$ | async) } }}
</span> </span>
<ng-container [ngTemplateOutlet]="bulkActions"></ng-container> <ng-container [ngTemplateOutlet]="bulkActions"></ng-container>

View File

@ -1,14 +1,14 @@
import { ChangeDetectionStrategy, Component, Input, TemplateRef } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, TemplateRef } from '@angular/core';
import { Required } from '../../utils'; import { Required } from '../../utils';
import { FilterService } from '../../filtering'; import { FilterService } from '../../filtering';
import { EntitiesService } from '../services'; import { EntitiesService, ListingService } from '../services';
import { IListable, ListingMode, ListingModes, TableColumnConfig } from '../models'; import { IListable, ListingMode, ListingModes, TableColumnConfig } from '../models';
@Component({ @Component({
selector: 'iqser-table-header', selector: 'iqser-table-header',
templateUrl: './table-header.component.html', templateUrl: './table-header.component.html',
styleUrls: ['./table-header.component.scss'], styleUrls: ['./table-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class TableHeaderComponent<T extends IListable> { export class TableHeaderComponent<T extends IListable> {
readonly listingModes = ListingModes; readonly listingModes = ListingModes;
@ -21,5 +21,9 @@ export class TableHeaderComponent<T extends IListable> {
@Input() totalSize?: number; @Input() totalSize?: number;
@Input() bulkActions?: TemplateRef<unknown>; @Input() bulkActions?: TemplateRef<unknown>;
constructor(readonly entitiesService: EntitiesService<T>, readonly filterService: FilterService) {} constructor(
readonly entitiesService: EntitiesService<T>,
readonly listingService: ListingService<T>,
readonly filterService: FilterService,
) {}
} }

View File

@ -12,7 +12,7 @@
<iqser-empty-state <iqser-empty-state
(action)="noDataAction.emit()" (action)="noDataAction.emit()"
*ngIf="listingComponent.entitiesService.noData$ | async" *ngIf="entitiesService.noData$ | async"
[buttonIcon]="noDataButtonIcon" [buttonIcon]="noDataButtonIcon"
[buttonLabel]="noDataButtonLabel" [buttonLabel]="noDataButtonLabel"
[icon]="noDataIcon" [icon]="noDataIcon"

View File

@ -15,6 +15,7 @@ import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { Required } from '../../utils'; import { Required } from '../../utils';
import { IListable, ListingModes, TableColumnConfig } from '../models'; import { IListable, ListingModes, TableColumnConfig } from '../models';
import { ListingComponent } from '../listing-component.directive'; import { ListingComponent } from '../listing-component.directive';
import { EntitiesService } from '../services';
const SCROLLBAR_WIDTH = 11; const SCROLLBAR_WIDTH = 11;
@ -22,7 +23,7 @@ const SCROLLBAR_WIDTH = 11;
selector: 'iqser-table', selector: 'iqser-table',
templateUrl: './table.component.html', templateUrl: './table.component.html',
styleUrls: ['./table.component.scss'], styleUrls: ['./table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class TableComponent<T extends IListable> implements OnInit { export class TableComponent<T extends IListable> implements OnInit {
readonly listingModes = ListingModes; readonly listingModes = ListingModes;
@ -49,14 +50,11 @@ export class TableComponent<T extends IListable> implements OnInit {
@ViewChild(CdkVirtualScrollViewport, { static: true }) readonly scrollViewport!: CdkVirtualScrollViewport; @ViewChild(CdkVirtualScrollViewport, { static: true }) readonly scrollViewport!: CdkVirtualScrollViewport;
constructor( constructor(
@Inject(forwardRef(() => ListingComponent)) private _parent: ListingComponent<T>, @Inject(forwardRef(() => ListingComponent)) readonly listingComponent: ListingComponent<T>,
private readonly _hostRef: ViewContainerRef private readonly _hostRef: ViewContainerRef,
readonly entitiesService: EntitiesService<T>,
) {} ) {}
get listingComponent(): ListingComponent<T> {
return this._parent;
}
get tableColumnConfigs(): readonly TableColumnConfig<T>[] { get tableColumnConfigs(): readonly TableColumnConfig<T>[] {
return this.listingComponent.tableColumnConfigs; return this.listingComponent.tableColumnConfigs;
} }

View File

@ -7,7 +7,7 @@
<iqser-empty-state <iqser-empty-state
(action)="noDataAction.emit()" (action)="noDataAction.emit()"
*ngIf="listingComponent.entitiesService.noData$ | async" *ngIf="entitiesService.noData$ | async"
[buttonIcon]="noDataButtonIcon" [buttonIcon]="noDataButtonIcon"
[buttonLabel]="noDataButtonLabel" [buttonLabel]="noDataButtonLabel"
[icon]="noDataIcon" [icon]="noDataIcon"
@ -15,7 +15,7 @@
[text]="noDataText" [text]="noDataText"
></iqser-empty-state> ></iqser-empty-state>
<div *ngIf="(listingComponent.entitiesService.noData$ | async) === false" cdkDropListGroup class="columns-wrapper"> <div *ngIf="(entitiesService.noData$ | async) === false" cdkDropListGroup class="columns-wrapper">
<div *ngFor="let column of config.columns" [class.dragging]="dragging" <div *ngFor="let column of config.columns" [class.dragging]="dragging"
[class.list-can-receive]="isReceiving(column)" [class.list-can-receive]="isReceiving(column)"
[class.list-dragging]="isDragging(column)" [class.list-dragging]="isDragging(column)"
@ -41,4 +41,3 @@
</div> </div>
</div> </div>
</div> </div>

View File

@ -15,7 +15,8 @@ import { ListingComponent } from '../listing-component.directive';
import { CdkDrag, CdkDragDrop, CdkDropList } from '@angular/cdk/drag-drop'; import { CdkDrag, CdkDragDrop, CdkDropList } from '@angular/cdk/drag-drop';
import { Required } from '../../utils'; import { Required } from '../../utils';
import { LoadingService } from '../../loading'; import { LoadingService } from '../../loading';
import {IListable} from "../models"; import { IListable } from '../models';
import { EntitiesService, ListingService } from '../services';
interface WorkflowColumn<T, K> { interface WorkflowColumn<T, K> {
key: K; key: K;
@ -60,12 +61,11 @@ export class WorkflowComponent<T extends IListable, K extends string> implements
private _existingEntities: { [key: string]: T } = {}; private _existingEntities: { [key: string]: T } = {};
constructor( constructor(
@Inject(forwardRef(() => ListingComponent)) private _parent: ListingComponent<T>, @Inject(forwardRef(() => ListingComponent)) readonly listingComponent: ListingComponent<T>,
private readonly _loadingService: LoadingService private readonly _loadingService: LoadingService,
) {} readonly listingService: ListingService<T>,
readonly entitiesService: EntitiesService<T>,
get listingComponent(): ListingComponent<T> { ) {
return this._parent;
} }
get tableHeaderLabel(): string | undefined { get tableHeaderLabel(): string | undefined {
@ -90,7 +90,7 @@ export class WorkflowComponent<T extends IListable, K extends string> implements
} }
ngOnInit(): void { ngOnInit(): void {
this.listingComponent.entitiesService.displayed$.subscribe(entities => this._updateConfigItems(entities)); this.listingService.displayed$.subscribe(entities => this._updateConfigItems(entities));
} }
canMoveTo(column: WorkflowColumn<T, K>): (item: CdkDrag<T>) => boolean { canMoveTo(column: WorkflowColumn<T, K>): (item: CdkDrag<T>) => boolean {

View File

@ -1,5 +1,5 @@
import { map } from 'rxjs/operators'; import { distinctUntilChanged, map } from 'rxjs/operators';
import { OperatorFunction } from 'rxjs'; import { OperatorFunction, pipe } from 'rxjs';
export function get<T>(predicate: (value: T, index: number) => boolean): OperatorFunction<T[], T | undefined> { export function get<T>(predicate: (value: T, index: number) => boolean): OperatorFunction<T[], T | undefined> {
return map(entities => entities.find(predicate)); return map(entities => entities.find(predicate));
@ -8,3 +8,6 @@ export function get<T>(predicate: (value: T, index: number) => boolean): Operato
export function any<T>(predicate: (value: T, index: number) => boolean): OperatorFunction<T[], boolean> { export function any<T>(predicate: (value: T, index: number) => boolean): OperatorFunction<T[], boolean> {
return map(entities => entities.some(predicate)); return map(entities => entities.some(predicate));
} }
export const toLengthValue = (entities: unknown[]): number => entities?.length ?? 0;
export const getLength = pipe(map(toLengthValue), distinctUntilChanged());