Add support for string or number ids
This commit is contained in:
parent
9c2c429df3
commit
2e74e2649e
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)));
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
) {}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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];
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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)!;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user