diff --git a/src/lib/filtering/filter-utils.ts b/src/lib/filtering/filter-utils.ts index 2184158..daf1362 100644 --- a/src/lib/filtering/filter-utils.ts +++ b/src/lib/filtering/filter-utils.ts @@ -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, filter: INestedFilter): boolean => entity[key] === filter.id; -export function getFilteredEntities(entities: T[], filters: IFilterGroup[]): T[] { +export function getFilteredEntities, PrimaryKey extends Id = T['id']>( + entities: T[], + filters: IFilterGroup[], +): T[] { const filteredEntities: T[] = []; entities.forEach(entity => { let add = true; diff --git a/src/lib/listing/listing-component.directive.ts b/src/lib/listing/listing-component.directive.ts index c7c700a..3352b07 100644 --- a/src/lib/listing/listing-component.directive.ts +++ b/src/lib/listing/listing-component.directive.ts @@ -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 extends AutoUnsubscribe implements OnDestroy { +export abstract class ListingComponent, PrimaryKey extends Id = Class['id']> + extends AutoUnsubscribe + implements OnDestroy +{ readonly filterService = inject(FilterService); - readonly searchService = inject>(SearchService); - readonly sortingService = inject>(SortingService); - readonly entitiesService = inject>(EntitiesService); - readonly listingService = inject>(ListingService); + readonly searchService = inject>(SearchService); + readonly sortingService = inject>(SortingService); + readonly entitiesService = inject>(EntitiesService); + readonly listingService = inject>(ListingService); readonly noMatch$ = this.#noMatch$; readonly noContent$ = this.#noContent$; readonly sortedDisplayedEntities$ = this.#sortedDisplayedEntities$; - abstract readonly tableColumnConfigs: readonly TableColumnConfig[]; + abstract readonly tableColumnConfigs: readonly TableColumnConfig[]; abstract readonly tableHeaderLabel: string; @ViewChild('tableItemTemplate') readonly tableItemTemplate?: TemplateRef; @ViewChild('workflowItemTemplate') readonly workflowItemTemplate?: TemplateRef; - get allEntities(): T[] { + get allEntities(): Class[] { return this.entitiesService.all; } - get #sortedDisplayedEntities$(): Observable { - const sort = (entities: T[]) => this.sortingService.defaultSort(entities); + get #sortedDisplayedEntities$(): Observable { + 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 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; } } diff --git a/src/lib/listing/models/entity.model.ts b/src/lib/listing/models/entity.model.ts index fb4d63a..ba92c81 100644 --- a/src/lib/listing/models/entity.model.ts +++ b/src/lib/listing/models/entity.model.ts @@ -1,18 +1,18 @@ import { IListable } from './listable'; import { Id } from './trackable'; -export abstract class Entity implements IListable { - abstract readonly id: Id; +export abstract class Entity implements IListable { + 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): boolean { + isEqual(entity: Entity): boolean { return JSON.stringify(this._interface) === JSON.stringify(entity?._interface); } } diff --git a/src/lib/listing/models/listable.ts b/src/lib/listing/models/listable.ts index ead3157..dd35f30 100644 --- a/src/lib/listing/models/listable.ts +++ b/src/lib/listing/models/listable.ts @@ -1,6 +1,6 @@ -import { ITrackable } from './trackable'; +import { Id, ITrackable } from './trackable'; -export interface IListable extends ITrackable { +export interface IListable extends ITrackable { readonly searchKey: string; readonly routerLink?: string; } diff --git a/src/lib/listing/models/trackable.ts b/src/lib/listing/models/trackable.ts index 66c3fb4..56de54f 100644 --- a/src/lib/listing/models/trackable.ts +++ b/src/lib/listing/models/trackable.ts @@ -1,5 +1,5 @@ export type Id = string | number; -export interface ITrackable { - readonly id: Id; +export interface ITrackable { + readonly id: PrimaryKey; } diff --git a/src/lib/listing/services/entities.service.ts b/src/lib/listing/services/entities.service.ts index ab843a6..294e2b0 100644 --- a/src/lib/listing/services/entities.service.ts +++ b/src/lib/listing/services/entities.service.ts @@ -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 extends GenericService { +export class EntitiesService< + Interface, + Class extends Interface & IListable, + PrimaryKey extends Id = Class['id'], +> extends GenericService { readonly noData$: Observable; - readonly all$: Observable; + readonly all$: Observable; readonly allLength$: Observable; protected readonly _defaultModelPath: string = ''; - protected readonly _entityClass?: new (entityInterface: I, ...args: unknown[]) => E; - protected readonly _entityChanged$ = new Subject(); - protected readonly _entityDeleted$ = new Subject(); - readonly #all$ = new BehaviorSubject([]); + protected readonly _entityClass?: new (entityInterface: Interface, ...args: unknown[]) => Class; + protected readonly _entityChanged$ = new Subject(); + protected readonly _entityDeleted$ = new Subject(); + readonly #all$ = new BehaviorSubject([]); constructor() { super(); @@ -29,7 +31,7 @@ export class EntitiesService extends this.noData$ = this.#noData$; } - get all(): E[] { + get all(): Class[] { return Object.values(this.#all$.getValue()); } @@ -40,15 +42,15 @@ export class EntitiesService extends ); } - loadAll(...args: unknown[]): Observable; - loadAll(modelPath = this._defaultModelPath, queryParams?: List): Observable { + loadAll(...args: unknown[]): Observable; + loadAll(modelPath = this._defaultModelPath, queryParams?: List): Observable { 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 { + getEntityChanged$(entityId: Id): Observable { return this._entityChanged$.pipe( filter(entity => entity.id === entityId), startWith(this.find(entityId)), @@ -56,12 +58,12 @@ export class EntitiesService extends ); } - getEntityDeleted$(entityId: Id): Observable { + getEntityDeleted$(entityId: Id): Observable { 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 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 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); diff --git a/src/lib/listing/services/listing.service.ts b/src/lib/listing/services/listing.service.ts index dd1bedc..66d6316 100644 --- a/src/lib/listing/services/listing.service.ts +++ b/src/lib/listing/services/listing.service.ts @@ -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 { - readonly displayed$: Observable; +export class ListingService, PrimaryKey extends Id = Class['id']> { + readonly displayed$: Observable; readonly displayedLength$: Observable; readonly areAllSelected$: Observable; readonly areSomeSelected$: Observable; readonly notAllSelected$: Observable; readonly selected$: Observable<(string | number)[]>; - readonly selectedEntities$: Observable; + readonly selectedEntities$: Observable; readonly selectedLength$: Observable; - private _displayed: E[] = []; + private _displayed: Class[] = []; private readonly _selected$ = new BehaviorSubject<(string | number)[]>([]); constructor( protected readonly _filterService: FilterService, - protected readonly _searchService: SearchService, - protected readonly _entitiesService: EntitiesService, + protected readonly _searchService: SearchService, + protected readonly _entitiesService: EntitiesService, ) { this.displayed$ = this._getDisplayed$; this.displayedLength$ = this.displayed$.pipe(getLength, shareDistinctLast()); @@ -40,7 +41,7 @@ export class ListingService { 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 { return this._selected$.getValue(); } - private get _getDisplayed$(): Observable { + private get _getDisplayed$(): Observable { const { filterGroups$ } = this._filterService; const { valueChanges$ } = this._searchService; @@ -89,16 +90,16 @@ export class ListingService { 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 { + isSelected$(entity: Class): Observable { return this._selected$.pipe( any(selectedId => selectedId === entity.id), shareLast(), @@ -112,7 +113,7 @@ export class ListingService { 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 { 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))); diff --git a/src/lib/listing/table-column-name/table-column-name.component.ts b/src/lib/listing/table-column-name/table-column-name.component.ts index 4b2b81a..24494d5 100644 --- a/src/lib/listing/table-column-name/table-column-name.component.ts +++ b/src/lib/listing/table-column-name/table-column-name.component.ts @@ -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 = (thisArg: TableColumnNameComponent) => !!thisArg.rightIcon; @@ -11,7 +12,7 @@ const ifHasRightIcon = (thisArg: TableColumnNameComponent { +export class TableColumnNameComponent, PrimaryKey extends Id = T['id']> { readonly sortingOrders = SortingOrders; @Input() @Required() label!: string; diff --git a/src/lib/listing/table-content/table-content.component.ts b/src/lib/listing/table-content/table-content.component.ts index d36bb9b..0479910 100644 --- a/src/lib/listing/table-content/table-content.component.ts +++ b/src/lib/listing/table-content/table-content.component.ts @@ -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 extends AutoUnsubscribe implements OnDestroy, AfterViewInit { +export class TableContentComponent, 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 boolean>; + @Input() itemMouseEnterFn?: (entity: Class) => void; + @Input() itemMouseLeaveFn?: (entity: Class) => void; + @Input() tableItemClasses?: Record boolean>; @Input() selectionEnabled!: boolean; - readonly trackBy = trackByFactory(); + readonly trackBy = trackByFactory(); @ViewChild(CdkVirtualScrollViewport, { static: true }) readonly scrollViewport!: CdkVirtualScrollViewport; @ViewChild(HasScrollbarDirective, { static: true }) readonly hasScrollbarDirective!: HasScrollbarDirective; @@ -29,8 +33,8 @@ export class TableContentComponent extends AutoUnsubscribe private _multiSelectActive$ = new BehaviorSubject(false); constructor( - @Inject(forwardRef(() => ListingComponent)) readonly listingComponent: ListingComponent, - readonly listingService: ListingService, + @Inject(forwardRef(() => ListingComponent)) readonly listingComponent: ListingComponent, + readonly listingService: ListingService, readonly helpModeService: HelpModeService, ) { super(); @@ -41,7 +45,7 @@ export class TableContentComponent 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 extends AutoUnsubscribe this.scrollViewport.scrollToIndex(this._lastScrolledIndex, 'smooth'); } - getTableItemClasses(entity: T): Record { + getTableItemClasses(entity: Class): Record { const classes: Record = { 'table-item': true, pointer: !!entity.routerLink && entity.routerLink.length > 0, diff --git a/src/lib/listing/table-header/table-header.component.ts b/src/lib/listing/table-header/table-header.component.ts index 8632905..7b1c6df 100644 --- a/src/lib/listing/table-header/table-header.component.ts +++ b/src/lib/listing/table-header/table-header.component.ts @@ -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 { +export class TableHeaderComponent, PrimaryKey extends Id = T['id']> { readonly listingModes = ListingModes; @Input() @Required() tableHeaderLabel!: string; @@ -24,7 +25,7 @@ export class TableHeaderComponent { readonly quickFilters$ = this.filterService.getFilterModels$('quickFilters'); constructor( - readonly entitiesService: EntitiesService, + readonly entitiesService: EntitiesService, readonly listingService: ListingService, readonly filterService: FilterService, ) {} diff --git a/src/lib/listing/table/table.component.ts b/src/lib/listing/table/table.component.ts index c8e49b1..fb52934 100644 --- a/src/lib/listing/table/table.component.ts +++ b/src/lib/listing/table/table.component.ts @@ -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 implements OnChanges { +export class TableComponent, PrimaryKey extends Id = Class['id']> implements OnChanges { readonly listingModes = ListingModes; - @Input() tableColumnConfigs!: readonly TableColumnConfig[]; + @Input() tableColumnConfigs!: readonly TableColumnConfig[]; @Input() bulkActions?: TemplateRef; @Input() headerTemplate?: TemplateRef; @Input() itemSize!: number; @@ -44,17 +45,17 @@ export class TableComponent implements OnChanges { @Input() showNoDataButton = false; @Input() noMatchText?: string; @Input() helpModeKey?: string; - @Input() tableItemClasses?: Record boolean>; - @Input() itemMouseEnterFn?: (entity: T) => void; - @Input() itemMouseLeaveFn?: (entity: T) => void; + @Input() tableItemClasses?: Record boolean>; + @Input() itemMouseEnterFn?: (entity: Class) => void; + @Input() itemMouseLeaveFn?: (entity: Class) => void; @Output() readonly noDataAction = new EventEmitter(); - @ViewChild(TableContentComponent, { static: true }) private readonly _tableContent!: TableContentComponent; + @ViewChild(TableContentComponent, { static: true }) private readonly _tableContent!: TableContentComponent; constructor( - @Inject(forwardRef(() => ListingComponent)) readonly listingComponent: ListingComponent, + @Inject(forwardRef(() => ListingComponent)) readonly listingComponent: ListingComponent, private readonly _hostRef: ViewContainerRef, private readonly _changeRef: ChangeDetectorRef, - readonly entitiesService: EntitiesService, + readonly entitiesService: EntitiesService, ) {} get tableHeaderLabel(): string | undefined { diff --git a/src/lib/listing/utils.ts b/src/lib/listing/utils.ts index 40f7010..f997f36 100644 --- a/src/lib/listing/utils.ts +++ b/src/lib/listing/utils.ts @@ -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 { - readonly entitiesService?: Type>; +export interface IListingServiceFactoryOptions, PrimaryKey extends Id = T['id']> { + readonly entitiesService?: Type>; readonly component: Type; } -function getEntitiesService(service?: Type>): Provider { +function getEntitiesService, PrimaryKey extends Id = T['id']>( + service?: Type>, +): Provider { if (service) { return { provide: EntitiesService, @@ -23,7 +27,9 @@ function getEntitiesService(service?: Type>): Provider { return EntitiesService; } -function getOptions(options?: IListingServiceFactoryOptions | Type) { +function getOptions, PrimaryKey extends Id = T['id']>( + options?: IListingServiceFactoryOptions | Type, +) { if (typeof options === 'function') { return { component: options, @@ -33,7 +39,7 @@ function getOptions(options?: IListingServiceFactoryOptions | Type(component: Type) { +function getComponent(component: Type) { return { provide: ListingComponent, useExisting: forwardRef(() => component), @@ -54,8 +60,12 @@ export function listingProvidersFactory(component: Type): Provider[]; * This is equivalent to * @example [{provide: EntitiesService, useExisting: entitiesService}, {provide: ListingComponent, useExisting: forwardRef(() => component)}, FilterService, SearchService, SortingService, ListingService] */ -export function listingProvidersFactory(options: IListingServiceFactoryOptions): Provider[]; -export function listingProvidersFactory(args?: IListingServiceFactoryOptions | Type): Provider[] { +export function listingProvidersFactory, PrimaryKey extends Id = T['id']>( + options: IListingServiceFactoryOptions, +): Provider[]; +export function listingProvidersFactory, PrimaryKey extends Id = T['id']>( + args?: IListingServiceFactoryOptions | Type, +): Provider[] { const options = getOptions(args); const services = [...DefaultListingServices]; diff --git a/src/lib/listing/workflow/workflow.component.ts b/src/lib/listing/workflow/workflow.component.ts index 0e86f4d..98c75e8 100644 --- a/src/lib/listing/workflow/workflow.component.ts +++ b/src/lib/listing/workflow/workflow.component.ts @@ -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 extends Co private readonly _changeRef: ChangeDetectorRef, private readonly _elementRef: ElementRef, readonly listingService: ListingService, - readonly entitiesService: EntitiesService, + readonly entitiesService: EntitiesService, ) { super(); } diff --git a/src/lib/search/search.service.ts b/src/lib/search/search.service.ts index c232c0e..f454349 100644 --- a/src/lib/search/search.service.ts +++ b/src/lib/search/search.service.ts @@ -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 { +export class SearchService, PrimaryKey extends Id = T['id']> { skip = false; private readonly _query$ = new BehaviorSubject(''); readonly valueChanges$ = this._query$.asObservable().pipe(shareDistinctLast()); diff --git a/src/lib/services/entities-map.service.ts b/src/lib/services/entities-map.service.ts index 4642c15..27790da 100644 --- a/src/lib/services/entities-map.service.ts +++ b/src/lib/services/entities-map.service.ts @@ -6,12 +6,12 @@ import { RequiredParam, shareLast, Validate } from '../utils'; import { Id } from '../listing/models/trackable'; @Injectable({ providedIn: 'root' }) -export abstract class EntitiesMapService, I> { +export abstract class EntitiesMapService, PrimaryKey extends Id = Class['id']> { protected abstract readonly _primaryKey: string; - protected readonly _map = new Map>(); - readonly #entityChanged$ = new Subject(); - readonly #entityDeleted$ = new Subject(); + protected readonly _map = new Map>(); + readonly #entityChanged$ = new Subject(); + readonly #entityDeleted$ = new Subject(); get empty(): boolean { return this._map.size === 0; @@ -23,7 +23,7 @@ export abstract class EntitiesMapService, I> { get$(key: Id) { if (!this._map.has(key)) { - this._map.set(key, new BehaviorSubject([])); + this._map.set(key, new BehaviorSubject([])); } return this._getBehaviourSubject(key).asObservable(); @@ -33,9 +33,9 @@ export abstract class EntitiesMapService, 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, 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(entities)); + this._map.set(key, new BehaviorSubject(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, 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, I> { } @Validate() - watch$(@RequiredParam() key: Id, @RequiredParam() entityId: Id): Observable { + watch$(@RequiredParam() key: Id, @RequiredParam() entityId: Id): Observable { 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 { + watchDeleted$(entityId: Id): Observable { 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 { + private _getBehaviourSubject(key: Id): BehaviorSubject { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this._map.get(key)!; } diff --git a/src/lib/sorting/sorting.service.ts b/src/lib/sorting/sorting.service.ts index 1cb00ba..83214a2 100644 --- a/src/lib/sorting/sorting.service.ts +++ b/src/lib/sorting/sorting.service.ts @@ -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 { +export class SortingService, PrimaryKey extends Id = T['id']> { private readonly _sortingOption$ = new BehaviorSubject>({ column: 'searchKey', order: SortingOrders.asc, diff --git a/src/lib/utils/functions.ts b/src/lib/utils/functions.ts index 510f3ad..e28239f 100644 --- a/src/lib/utils/functions.ts +++ b/src/lib/utils/functions.ts @@ -33,7 +33,7 @@ export function toNumber(str: string): number { } } -export function trackByFactory() { +export function trackByFactory, PrimaryKey extends Id = T['id']>() { return (_index: number, item: T): Id => item.id; }