Add support for string or number ids

This commit is contained in:
Dan Percic 2022-07-26 15:30:35 +03:00
parent 9c2c429df3
commit 2e74e2649e
17 changed files with 136 additions and 106 deletions

View File

@ -4,6 +4,7 @@ import { IFilterGroup } from './models/filter-group.model';
import { IFilter } from './models/filter.model';
import { NestedFilter } from './models/nested-filter';
import { IListable } from '../listing';
import { Id } from '../listing/models/trackable';
function copySettings(oldFilters: INestedFilter[], newFilters: INestedFilter[]) {
if (!oldFilters || !newFilters) {
@ -78,7 +79,10 @@ export const keyChecker =
(entity: Record<string, string>, filter: INestedFilter): boolean =>
entity[key] === filter.id;
export function getFilteredEntities<T extends IListable>(entities: T[], filters: IFilterGroup[]): T[] {
export function getFilteredEntities<T extends IListable<PrimaryKey>, PrimaryKey extends Id = T['id']>(
entities: T[],
filters: IFilterGroup[],
): T[] {
const filteredEntities: T[] = [];
entities.forEach(entity => {
let add = true;

View File

@ -7,31 +7,35 @@ import { AutoUnsubscribe, shareDistinctLast } from '../utils';
import { SearchService } from '../search';
import { EntitiesService, ListingService } from './services';
import { IListable, TableColumnConfig } from './models';
import { Id } from './models/trackable';
@Directive()
export abstract class ListingComponent<T extends IListable> extends AutoUnsubscribe implements OnDestroy {
export abstract class ListingComponent<Class extends IListable<PrimaryKey>, PrimaryKey extends Id = Class['id']>
extends AutoUnsubscribe
implements OnDestroy
{
readonly filterService = inject(FilterService);
readonly searchService = inject<SearchService<T>>(SearchService);
readonly sortingService = inject<SortingService<T>>(SortingService);
readonly entitiesService = inject<EntitiesService<T>>(EntitiesService);
readonly listingService = inject<ListingService<T>>(ListingService);
readonly searchService = inject<SearchService<Class>>(SearchService);
readonly sortingService = inject<SortingService<Class>>(SortingService);
readonly entitiesService = inject<EntitiesService<Class, Class>>(EntitiesService);
readonly listingService = inject<ListingService<Class>>(ListingService);
readonly noMatch$ = this.#noMatch$;
readonly noContent$ = this.#noContent$;
readonly sortedDisplayedEntities$ = this.#sortedDisplayedEntities$;
abstract readonly tableColumnConfigs: readonly TableColumnConfig<T>[];
abstract readonly tableColumnConfigs: readonly TableColumnConfig<Class>[];
abstract readonly tableHeaderLabel: string;
@ViewChild('tableItemTemplate') readonly tableItemTemplate?: TemplateRef<unknown>;
@ViewChild('workflowItemTemplate') readonly workflowItemTemplate?: TemplateRef<unknown>;
get allEntities(): T[] {
get allEntities(): Class[] {
return this.entitiesService.all;
}
get #sortedDisplayedEntities$(): Observable<T[]> {
const sort = (entities: T[]) => this.sortingService.defaultSort(entities);
get #sortedDisplayedEntities$(): Observable<Class[]> {
const sort = (entities: Class[]) => this.sortingService.defaultSort(entities);
const sortedEntities$ = this.listingService.displayed$.pipe(map(sort));
return this.sortingService.sortingOption$.pipe(
switchMap(() => sortedEntities$),
@ -53,12 +57,12 @@ export abstract class ListingComponent<T extends IListable> extends AutoUnsubscr
);
}
toggleEntitySelected(event: MouseEvent, entity: T): void {
toggleEntitySelected(event: MouseEvent, entity: Class): void {
event.stopPropagation();
this.listingService.select(entity);
}
cast(entity: unknown): T {
return entity as T;
cast(entity: unknown): Class {
return entity as Class;
}
}

View File

@ -1,18 +1,18 @@
import { IListable } from './listable';
import { Id } from './trackable';
export abstract class Entity<I> implements IListable {
abstract readonly id: Id;
export abstract class Entity<Interface, PrimaryKey extends Id = string> implements IListable<PrimaryKey> {
abstract readonly id: PrimaryKey;
abstract readonly routerLink?: string;
abstract readonly searchKey: string;
protected constructor(private readonly _interface: I) {}
protected constructor(private readonly _interface: Interface) {}
get model(): I {
get model(): Interface {
return this._interface;
}
isEqual(entity: Entity<I>): boolean {
isEqual(entity: Entity<Interface, PrimaryKey>): boolean {
return JSON.stringify(this._interface) === JSON.stringify(entity?._interface);
}
}

View File

@ -1,6 +1,6 @@
import { ITrackable } from './trackable';
import { Id, ITrackable } from './trackable';
export interface IListable extends ITrackable {
export interface IListable<PrimaryKey extends Id = string> extends ITrackable<PrimaryKey> {
readonly searchKey: string;
readonly routerLink?: string;
}

View File

@ -1,5 +1,5 @@
export type Id = string | number;
export interface ITrackable {
readonly id: Id;
export interface ITrackable<PrimaryKey extends Id = string> {
readonly id: PrimaryKey;
}

View File

@ -8,19 +8,21 @@ import { Id } from '../models/trackable';
@Injectable()
/**
* E for Entity,
* I for Interface.
* By default, if no interface is provided, I = E
* By default, if no implementation (class) is provided, Class = Interface & IListable
*/
export class EntitiesService<I, E extends I & IListable = I & IListable> extends GenericService<I> {
export class EntitiesService<
Interface,
Class extends Interface & IListable<PrimaryKey>,
PrimaryKey extends Id = Class['id'],
> extends GenericService<Interface> {
readonly noData$: Observable<boolean>;
readonly all$: Observable<E[]>;
readonly all$: Observable<Class[]>;
readonly allLength$: Observable<number>;
protected readonly _defaultModelPath: string = '';
protected readonly _entityClass?: new (entityInterface: I, ...args: unknown[]) => E;
protected readonly _entityChanged$ = new Subject<E>();
protected readonly _entityDeleted$ = new Subject<E>();
readonly #all$ = new BehaviorSubject<E[]>([]);
protected readonly _entityClass?: new (entityInterface: Interface, ...args: unknown[]) => Class;
protected readonly _entityChanged$ = new Subject<Class>();
protected readonly _entityDeleted$ = new Subject<Class>();
readonly #all$ = new BehaviorSubject<Class[]>([]);
constructor() {
super();
@ -29,7 +31,7 @@ export class EntitiesService<I, E extends I & IListable = I & IListable> extends
this.noData$ = this.#noData$;
}
get all(): E[] {
get all(): Class[] {
return Object.values(this.#all$.getValue());
}
@ -40,15 +42,15 @@ export class EntitiesService<I, E extends I & IListable = I & IListable> extends
);
}
loadAll(...args: unknown[]): Observable<E[]>;
loadAll(modelPath = this._defaultModelPath, queryParams?: List<QueryParam>): Observable<E[]> {
loadAll(...args: unknown[]): Observable<Class[]>;
loadAll(modelPath = this._defaultModelPath, queryParams?: List<QueryParam>): Observable<Class[]> {
return this.getAll(modelPath, queryParams).pipe(
mapEach(entity => (this._entityClass ? new this._entityClass(entity) : (entity as E))),
tap((entities: E[]) => this.setEntities(entities)),
mapEach(entity => (this._entityClass ? new this._entityClass(entity) : (entity as Class))),
tap((entities: Class[]) => this.setEntities(entities)),
);
}
getEntityChanged$(entityId: Id): Observable<E | undefined> {
getEntityChanged$(entityId: Id): Observable<Class | undefined> {
return this._entityChanged$.pipe(
filter(entity => entity.id === entityId),
startWith(this.find(entityId)),
@ -56,12 +58,12 @@ export class EntitiesService<I, E extends I & IListable = I & IListable> extends
);
}
getEntityDeleted$(entityId: Id): Observable<E | undefined> {
getEntityDeleted$(entityId: Id): Observable<Class | undefined> {
return this._entityDeleted$.pipe(filter(entity => entity.id === entityId));
}
setEntities(entities: E[]): void {
const changedEntities: E[] = [];
setEntities(entities: Class[]): void {
const changedEntities: Class[] = [];
const deletedEntities = this.all.filter(oldEntity => !entities.find(newEntity => newEntity.id === oldEntity.id));
// Keep old object references for unchanged entities
@ -97,7 +99,7 @@ export class EntitiesService<I, E extends I & IListable = I & IListable> extends
}
}
find(id: Id): E | undefined {
find(id: Id): Class | undefined {
return this.all.find(entity => entity.id === id);
}
@ -105,7 +107,7 @@ export class EntitiesService<I, E extends I & IListable = I & IListable> extends
return this.all.some(entity => entity.id === id);
}
replace(entity: E): void {
replace(entity: Class): void {
const all = this.all.filter(item => item.id !== entity.id);
this.setEntities([...all, entity]);
this._entityChanged$.next(entity);

View File

@ -6,24 +6,25 @@ import { SearchService } from '../../search';
import { IListable } from '../models';
import { EntitiesService } from './entities.service';
import { any, getLength, shareDistinctLast, shareLast } from '../../utils';
import { Id } from '../models/trackable';
@Injectable()
export class ListingService<E extends IListable> {
readonly displayed$: Observable<E[]>;
export class ListingService<Class extends IListable<PrimaryKey>, PrimaryKey extends Id = Class['id']> {
readonly displayed$: Observable<Class[]>;
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 selectedEntities$: Observable<Class[]>;
readonly selectedLength$: Observable<number>;
private _displayed: E[] = [];
private _displayed: Class[] = [];
private readonly _selected$ = new BehaviorSubject<(string | number)[]>([]);
constructor(
protected readonly _filterService: FilterService,
protected readonly _searchService: SearchService<E>,
protected readonly _entitiesService: EntitiesService<E>,
protected readonly _searchService: SearchService<Class>,
protected readonly _entitiesService: EntitiesService<Class, Class>,
) {
this.displayed$ = this._getDisplayed$;
this.displayedLength$ = this.displayed$.pipe(getLength, shareDistinctLast());
@ -40,7 +41,7 @@ export class ListingService<E extends IListable> {
this.notAllSelected$ = this._notAllSelected$;
}
get selected(): E[] {
get selected(): Class[] {
const selectedIds = this.selectedIds;
return this._entitiesService.all.filter(a => selectedIds.includes(a.id));
}
@ -49,7 +50,7 @@ export class ListingService<E extends IListable> {
return this._selected$.getValue();
}
private get _getDisplayed$(): Observable<E[]> {
private get _getDisplayed$(): Observable<Class[]> {
const { filterGroups$ } = this._filterService;
const { valueChanges$ } = this._searchService;
@ -89,16 +90,16 @@ export class ListingService<E extends IListable> {
return this._displayed.length !== 0 && this._displayed.length === this.selected.length;
}
setSelected(newEntities: E[]): void {
setSelected(newEntities: Class[]): void {
const selectedIds = newEntities.map(e => e.id);
this._selected$.next(selectedIds);
}
isSelected(entity: E): boolean {
isSelected(entity: Class): boolean {
return this.selectedIds.indexOf(entity.id) !== -1;
}
isSelected$(entity: E): Observable<boolean> {
isSelected$(entity: Class): Observable<boolean> {
return this._selected$.pipe(
any(selectedId => selectedId === entity.id),
shareLast(),
@ -112,7 +113,7 @@ export class ListingService<E extends IListable> {
this.setSelected(this._displayed);
}
select(entity: E): void {
select(entity: Class): void {
const currentEntityIdx = this.selected.indexOf(entity);
if (currentEntityIdx === -1) {
return this.setSelected([...this.selected, entity]);
@ -120,7 +121,7 @@ export class ListingService<E extends IListable> {
this.setSelected(this.selected.filter((_el, idx) => idx !== currentEntityIdx));
}
deselect(entities: E | E[]) {
deselect(entities: Class | Class[]) {
const _entities = Array.isArray(entities) ? entities : [entities];
const entitiesIds = _entities.map(e => e.id);
this.setSelected(this.selected.filter(el => !entitiesIds.includes(el.id)));

View File

@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, Input, Optional } from '@angular/co
import { SortingOrders, SortingService } from '../../sorting';
import { KeysOf, Required } from '../../utils';
import { IListable } from '../models';
import { Id } from '../models/trackable';
const ifHasRightIcon = <T extends IListable>(thisArg: TableColumnNameComponent<T>) => !!thisArg.rightIcon;
@ -11,7 +12,7 @@ const ifHasRightIcon = <T extends IListable>(thisArg: TableColumnNameComponent<T
styleUrls: ['./table-column-name.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TableColumnNameComponent<T extends IListable> {
export class TableColumnNameComponent<T extends IListable<PrimaryKey>, PrimaryKey extends Id = T['id']> {
readonly sortingOrders = SortingOrders;
@Input() @Required() label!: string;

View File

@ -8,19 +8,23 @@ import { ListingComponent, ListingService } from '../index';
import { HasScrollbarDirective } from '../../scrollbar';
import { BehaviorSubject } from 'rxjs';
import { HelpModeService } from '../../help-mode';
import { Id } from '../models/trackable';
@Component({
selector: 'iqser-table-content',
templateUrl: './table-content.component.html',
styleUrls: ['./table-content.component.scss'],
})
export class TableContentComponent<T extends IListable> extends AutoUnsubscribe implements OnDestroy, AfterViewInit {
export class TableContentComponent<Class extends IListable<PrimaryKey>, PrimaryKey extends Id = Class['id']>
extends AutoUnsubscribe
implements OnDestroy, AfterViewInit
{
@Input() itemSize!: number;
@Input() itemMouseEnterFn?: (entity: T) => void;
@Input() itemMouseLeaveFn?: (entity: T) => void;
@Input() tableItemClasses?: Record<string, (e: T) => boolean>;
@Input() itemMouseEnterFn?: (entity: Class) => void;
@Input() itemMouseLeaveFn?: (entity: Class) => void;
@Input() tableItemClasses?: Record<string, (e: Class) => boolean>;
@Input() selectionEnabled!: boolean;
readonly trackBy = trackByFactory<T>();
readonly trackBy = trackByFactory<Class>();
@ViewChild(CdkVirtualScrollViewport, { static: true }) readonly scrollViewport!: CdkVirtualScrollViewport;
@ViewChild(HasScrollbarDirective, { static: true }) readonly hasScrollbarDirective!: HasScrollbarDirective;
@ -29,8 +33,8 @@ export class TableContentComponent<T extends IListable> extends AutoUnsubscribe
private _multiSelectActive$ = new BehaviorSubject(false);
constructor(
@Inject(forwardRef(() => ListingComponent)) readonly listingComponent: ListingComponent<T>,
readonly listingService: ListingService<T>,
@Inject(forwardRef(() => ListingComponent)) readonly listingComponent: ListingComponent<Class>,
readonly listingService: ListingService<Class>,
readonly helpModeService: HelpModeService,
) {
super();
@ -41,7 +45,7 @@ export class TableContentComponent<T extends IListable> extends AutoUnsubscribe
});
}
multiSelect(entity: T, $event: MouseEvent): void {
multiSelect(entity: Class, $event: MouseEvent): void {
if (this.selectionEnabled && this._multiSelectActive$.value) {
$event.stopPropagation();
this.listingService.select(entity);
@ -62,7 +66,7 @@ export class TableContentComponent<T extends IListable> extends AutoUnsubscribe
this.scrollViewport.scrollToIndex(this._lastScrolledIndex, 'smooth');
}
getTableItemClasses(entity: T): Record<string, boolean> {
getTableItemClasses(entity: Class): Record<string, boolean> {
const classes: Record<string, boolean> = {
'table-item': true,
pointer: !!entity.routerLink && entity.routerLink.length > 0,

View File

@ -3,6 +3,7 @@ import { Required } from '../../utils';
import { FilterService } from '../../filtering';
import { EntitiesService, ListingService } from '../services';
import { IListable, ListingMode, ListingModes, TableColumnConfig } from '../models';
import { Id } from '../models/trackable';
@Component({
selector: 'iqser-table-header',
@ -10,7 +11,7 @@ import { IListable, ListingMode, ListingModes, TableColumnConfig } from '../mode
styleUrls: ['./table-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TableHeaderComponent<T extends IListable> {
export class TableHeaderComponent<T extends IListable<PrimaryKey>, PrimaryKey extends Id = T['id']> {
readonly listingModes = ListingModes;
@Input() @Required() tableHeaderLabel!: string;
@ -24,7 +25,7 @@ export class TableHeaderComponent<T extends IListable> {
readonly quickFilters$ = this.filterService.getFilterModels$('quickFilters');
constructor(
readonly entitiesService: EntitiesService<T>,
readonly entitiesService: EntitiesService<T, T>,
readonly listingService: ListingService<T>,
readonly filterService: FilterService,
) {}

View File

@ -17,6 +17,7 @@ import { IListable, ListingModes, TableColumnConfig } from '../models';
import { ListingComponent } from '../listing-component.directive';
import { EntitiesService } from '../services';
import { TableContentComponent } from '../table-content/table-content.component';
import { Id } from '../models/trackable';
const SCROLLBAR_WIDTH = 11;
@ -25,10 +26,10 @@ const SCROLLBAR_WIDTH = 11;
templateUrl: './table.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TableComponent<T extends IListable> implements OnChanges {
export class TableComponent<Class extends IListable<PrimaryKey>, PrimaryKey extends Id = Class['id']> implements OnChanges {
readonly listingModes = ListingModes;
@Input() tableColumnConfigs!: readonly TableColumnConfig<T>[];
@Input() tableColumnConfigs!: readonly TableColumnConfig<Class>[];
@Input() bulkActions?: TemplateRef<unknown>;
@Input() headerTemplate?: TemplateRef<unknown>;
@Input() itemSize!: number;
@ -44,17 +45,17 @@ export class TableComponent<T extends IListable> implements OnChanges {
@Input() showNoDataButton = false;
@Input() noMatchText?: string;
@Input() helpModeKey?: string;
@Input() tableItemClasses?: Record<string, (e: T) => boolean>;
@Input() itemMouseEnterFn?: (entity: T) => void;
@Input() itemMouseLeaveFn?: (entity: T) => void;
@Input() tableItemClasses?: Record<string, (e: Class) => boolean>;
@Input() itemMouseEnterFn?: (entity: Class) => void;
@Input() itemMouseLeaveFn?: (entity: Class) => void;
@Output() readonly noDataAction = new EventEmitter<void>();
@ViewChild(TableContentComponent, { static: true }) private readonly _tableContent!: TableContentComponent<T>;
@ViewChild(TableContentComponent, { static: true }) private readonly _tableContent!: TableContentComponent<Class>;
constructor(
@Inject(forwardRef(() => ListingComponent)) readonly listingComponent: ListingComponent<T>,
@Inject(forwardRef(() => ListingComponent)) readonly listingComponent: ListingComponent<Class>,
private readonly _hostRef: ViewContainerRef,
private readonly _changeRef: ChangeDetectorRef,
readonly entitiesService: EntitiesService<T>,
readonly entitiesService: EntitiesService<Class, Class>,
) {}
get tableHeaderLabel(): string | undefined {

View File

@ -4,15 +4,19 @@ import { SortingService } from '../sorting';
import { EntitiesService, ListingService } from './services';
import { forwardRef, Provider, Type } from '@angular/core';
import { ListingComponent } from './listing-component.directive';
import { IListable } from './models';
import { Id } from './models/trackable';
export const DefaultListingServices: readonly Provider[] = [FilterService, SearchService, SortingService, ListingService] as const;
export interface IListingServiceFactoryOptions<T> {
readonly entitiesService?: Type<EntitiesService<T>>;
export interface IListingServiceFactoryOptions<T extends IListable<PrimaryKey>, PrimaryKey extends Id = T['id']> {
readonly entitiesService?: Type<EntitiesService<T, T>>;
readonly component: Type<unknown>;
}
function getEntitiesService<T>(service?: Type<EntitiesService<T>>): Provider {
function getEntitiesService<T extends IListable<PrimaryKey>, PrimaryKey extends Id = T['id']>(
service?: Type<EntitiesService<T, T>>,
): Provider {
if (service) {
return {
provide: EntitiesService,
@ -23,7 +27,9 @@ function getEntitiesService<T>(service?: Type<EntitiesService<T>>): Provider {
return EntitiesService;
}
function getOptions<T>(options?: IListingServiceFactoryOptions<T> | Type<unknown>) {
function getOptions<T extends IListable<PrimaryKey>, PrimaryKey extends Id = T['id']>(
options?: IListingServiceFactoryOptions<T> | Type<unknown>,
) {
if (typeof options === 'function') {
return {
component: options,
@ -33,7 +39,7 @@ function getOptions<T>(options?: IListingServiceFactoryOptions<T> | Type<unknown
return options;
}
function getComponent<T>(component: Type<unknown>) {
function getComponent(component: Type<unknown>) {
return {
provide: ListingComponent,
useExisting: forwardRef(() => component),
@ -54,8 +60,12 @@ export function listingProvidersFactory(component: Type<unknown>): Provider[];
* This is equivalent to
* @example <code>[{provide: EntitiesService, useExisting: entitiesService}, {provide: ListingComponent, useExisting: forwardRef(() => component)}, FilterService, SearchService, SortingService, ListingService]</code>
*/
export function listingProvidersFactory<T>(options: IListingServiceFactoryOptions<T>): Provider[];
export function listingProvidersFactory<T>(args?: IListingServiceFactoryOptions<T> | Type<unknown>): Provider[] {
export function listingProvidersFactory<T extends IListable<PrimaryKey>, PrimaryKey extends Id = T['id']>(
options: IListingServiceFactoryOptions<T>,
): Provider[];
export function listingProvidersFactory<T extends IListable<PrimaryKey>, PrimaryKey extends Id = T['id']>(
args?: IListingServiceFactoryOptions<T> | Type<unknown>,
): Provider[] {
const options = getOptions(args);
const services = [...DefaultListingServices];

View File

@ -15,7 +15,7 @@ import {
} from '@angular/core';
import { ListingComponent } from '../listing-component.directive';
import { CdkDragDrop, CdkDragStart, CdkDropList } from '@angular/cdk/drag-drop';
import { Debounce, trackByFactory, ContextComponent } from '../../utils';
import { ContextComponent, Debounce, trackByFactory } from '../../utils';
import { IListable } from '../models';
import { EntitiesService, ListingService } from '../services';
import { BehaviorSubject } from 'rxjs';
@ -69,7 +69,7 @@ export class WorkflowComponent<T extends IListable, K extends string> extends Co
private readonly _changeRef: ChangeDetectorRef,
private readonly _elementRef: ElementRef,
readonly listingService: ListingService<T>,
readonly entitiesService: EntitiesService<T>,
readonly entitiesService: EntitiesService<T, T>,
) {
super();
}

View File

@ -2,9 +2,10 @@ import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { IListable } from '../listing';
import { shareDistinctLast } from '../utils';
import { Id } from '../listing/models/trackable';
@Injectable()
export class SearchService<T extends IListable> {
export class SearchService<T extends IListable<PrimaryKey>, PrimaryKey extends Id = T['id']> {
skip = false;
private readonly _query$ = new BehaviorSubject('');
readonly valueChanges$ = this._query$.asObservable().pipe(shareDistinctLast());

View File

@ -6,12 +6,12 @@ import { RequiredParam, shareLast, Validate } from '../utils';
import { Id } from '../listing/models/trackable';
@Injectable({ providedIn: 'root' })
export abstract class EntitiesMapService<E extends Entity<I>, I> {
export abstract class EntitiesMapService<Interface, Class extends Entity<Interface, PrimaryKey>, PrimaryKey extends Id = Class['id']> {
protected abstract readonly _primaryKey: string;
protected readonly _map = new Map<Id, BehaviorSubject<E[]>>();
readonly #entityChanged$ = new Subject<E>();
readonly #entityDeleted$ = new Subject<E>();
protected readonly _map = new Map<Id, BehaviorSubject<Class[]>>();
readonly #entityChanged$ = new Subject<Class>();
readonly #entityDeleted$ = new Subject<Class>();
get empty(): boolean {
return this._map.size === 0;
@ -23,7 +23,7 @@ export abstract class EntitiesMapService<E extends Entity<I>, I> {
get$(key: Id) {
if (!this._map.has(key)) {
this._map.set(key, new BehaviorSubject<E[]>([]));
this._map.set(key, new BehaviorSubject<Class[]>([]));
}
return this._getBehaviourSubject(key).asObservable();
@ -33,9 +33,9 @@ export abstract class EntitiesMapService<E extends Entity<I>, I> {
return this._map.has(parentId);
}
get(key: Id): E[];
get(key: Id, id: Id): E | undefined;
get(key: Id, id?: Id): E | E[] | undefined {
get(key: Id): Class[];
get(key: Id, id: Id): Class | undefined;
get(key: Id, id?: Id): Class | Class[] | undefined {
const value = this._getBehaviourSubject(key)?.value;
if (!id) {
return value ?? [];
@ -43,18 +43,18 @@ export abstract class EntitiesMapService<E extends Entity<I>, I> {
return value?.find(item => item.id === id);
}
set(key: Id, entities: E[]): void {
set(key: Id, entities: Class[]): void {
if (!this._map.has(key)) {
this._map.set(key, new BehaviorSubject<E[]>(entities));
this._map.set(key, new BehaviorSubject<Class[]>(entities));
return entities.forEach(entity => this.#entityChanged$.next(entity));
}
const changedEntities: E[] = [];
const changedEntities: Class[] = [];
const deletedEntities = this.get(key).filter(oldEntity => !entities.find(newEntity => newEntity.id === oldEntity.id));
// Keep old object references for unchanged entities
const newEntities: E[] = entities.map(newEntity => {
const oldEntity: E | undefined = this.get(key, newEntity.id);
const newEntities: Class[] = entities.map(newEntity => {
const oldEntity: Class | undefined = this.get(key, newEntity.id);
if (oldEntity && newEntity.isEqual(oldEntity)) {
return oldEntity;
@ -77,7 +77,7 @@ export abstract class EntitiesMapService<E extends Entity<I>, I> {
}
}
replace(entities: E[]) {
replace(entities: Class[]) {
/** Return true if entities were replaced or false if not **/
const key = this._pluckPrimaryKey(entities[0]);
const entityIds = entities.map(entity => entity.id);
@ -97,25 +97,25 @@ export abstract class EntitiesMapService<E extends Entity<I>, I> {
}
@Validate()
watch$(@RequiredParam() key: Id, @RequiredParam() entityId: Id): Observable<E> {
watch$(@RequiredParam() key: Id, @RequiredParam() entityId: Id): Observable<Class> {
return this.#entityChanged$.pipe(
filter(entity => entity.id === entityId),
startWith(this.get(key, entityId) as E),
startWith(this.get(key, entityId) as Class),
shareLast(),
);
}
watchDeleted$(entityId: Id): Observable<E> {
watchDeleted$(entityId: Id): Observable<Class> {
return this.#entityDeleted$.pipe(filter(entity => entity.id === entityId));
}
private _pluckPrimaryKey(entity: E): Id {
private _pluckPrimaryKey(entity: Class): Id {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return entity[this._primaryKey] as Id;
}
private _getBehaviourSubject(key: Id): BehaviorSubject<E[]> {
private _getBehaviourSubject(key: Id): BehaviorSubject<Class[]> {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this._map.get(key)!;
}

View File

@ -5,9 +5,10 @@ import { SortingOrder, SortingOrders } from './models/sorting-order.type';
import { KeysOf, shareDistinctLast } from '../utils';
import { IListable } from '../listing';
import { orderBy } from 'lodash-es';
import { Id } from '../listing/models/trackable';
@Injectable()
export class SortingService<T extends IListable> {
export class SortingService<T extends IListable<PrimaryKey>, PrimaryKey extends Id = T['id']> {
private readonly _sortingOption$ = new BehaviorSubject<SortingOption<T>>({
column: 'searchKey',
order: SortingOrders.asc,

View File

@ -33,7 +33,7 @@ export function toNumber(str: string): number {
}
}
export function trackByFactory<T extends ITrackable>() {
export function trackByFactory<T extends ITrackable<PrimaryKey>, PrimaryKey extends Id = T['id']>() {
return (_index: number, item: T): Id => item.id;
}