RED-3687 - Remove AutoUnsubscribe directive

This commit is contained in:
Valentin Mihai 2022-07-21 13:48:51 +03:00
parent 6d9c169b3e
commit 63616184b5
11 changed files with 206 additions and 149 deletions

View File

@ -1,10 +1,10 @@
import { Directive, HostListener, inject, OnInit } from '@angular/core';
import { Directive, HostListener, inject, OnDestroy, OnInit } from '@angular/core';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { AutoUnsubscribe, hasFormChanged, IqserEventTarget } from '../utils';
import { hasFormChanged, IqserEventTarget } from '../utils';
import { ConfirmOptions } from '.';
import { ConfirmationDialogService } from './confirmation-dialog.service';
import { firstValueFrom } from 'rxjs';
import { firstValueFrom, Subscription } from 'rxjs';
import { LoadingService } from '../loading';
import { Toaster } from '../services';
@ -26,7 +26,7 @@ export interface SaveOptions {
* Make sure to remove the (submit)="save()" property from the form and to set type="button" on the save button
* (otherwise the save request will be triggered twice).
* */
export abstract class BaseDialogComponent extends AutoUnsubscribe implements OnInit {
export abstract class BaseDialogComponent implements OnDestroy {
form!: UntypedFormGroup;
initialFormValue!: Record<string, string>;
protected readonly _formBuilder = inject(UntypedFormBuilder);
@ -34,10 +34,11 @@ export abstract class BaseDialogComponent extends AutoUnsubscribe implements OnI
protected readonly _toaster = inject(Toaster);
readonly #confirmationDialogService = inject(ConfirmationDialogService);
readonly #dialog = inject(MatDialog);
#backdropClickSubscription = this._dialogRef.backdropClick().subscribe(() => {
this.close();
});
protected constructor(protected readonly _dialogRef: MatDialogRef<BaseDialogComponent>, private readonly _isInEditMode?: boolean) {
super();
}
protected constructor(protected readonly _dialogRef: MatDialogRef<BaseDialogComponent>, private readonly _isInEditMode?: boolean) {}
get valid(): boolean {
return this.form.valid;
@ -53,10 +54,8 @@ export abstract class BaseDialogComponent extends AutoUnsubscribe implements OnI
abstract save(options?: SaveOptions): void;
ngOnInit(): void {
this.addSubscription = this._dialogRef.backdropClick().subscribe(() => {
this.close();
});
ngOnDestroy(): void {
this.#backdropClickSubscription.unsubscribe();
}
close(): void {

View File

@ -1,9 +1,9 @@
import { Directive } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms';
import { AutoUnsubscribe, hasFormChanged } from '../utils';
import { hasFormChanged } from '../utils';
@Directive()
export abstract class BaseFormComponent extends AutoUnsubscribe {
export abstract class BaseFormComponent {
form!: UntypedFormGroup;
initialFormValue!: Record<string, string>;

View File

@ -1,20 +1,20 @@
<div class="display-contents">
<div class="display-contents" *ngIf="componentContext$ | async as ctx">
<cdk-virtual-scroll-viewport
[class.no-data]="listingComponent.noContent$ | async"
[class.no-data]="ctx.noContent"
[itemSize]="itemSize"
[maxBufferPx]="3000"
[minBufferPx]="1000"
id="virtual-scroll"
iqserHasScrollbar
>
<ng-container *cdkVirtualFor="let entity of listingComponent.sortedDisplayedEntities$ | async; trackBy: trackBy">
<ng-container *cdkVirtualFor="let entity of ctx.sortedDisplayedEntities; trackBy: trackBy">
<!-- mouseenter and mouseleave triggers change detection event if itemMouse functions are undefined -->
<!-- this little hack below ensures that change detection won't be triggered if functions are undefined -->
<div
(mouseenter)="itemMouseEnterFn && itemMouseEnterFn(entity)"
(mouseleave)="itemMouseLeaveFn && itemMouseLeaveFn(entity)"
*ngIf="itemMouseEnterFn || itemMouseLeaveFn; else withoutMouseEvents"
[class.help-mode]="helpModeService.isHelpModeActive$ | async"
[class.help-mode]="ctx.isHelpModeActive"
[id]="'item-' + entity.id"
[ngClass]="getTableItemClasses(entity)"
[routerLink]="entity.routerLink"
@ -28,7 +28,7 @@
<ng-template #withoutMouseEvents>
<div
[class.help-mode]="helpModeService.isHelpModeActive$ | async"
[class.help-mode]="ctx.isHelpModeActive"
[id]="'item-' + entity.id"
[ngClass]="getTableItemClasses(entity)"
[routerLink]="entity.routerLink"

View File

@ -1,20 +1,29 @@
/* eslint-disable @angular-eslint/prefer-on-push-component-change-detection */
import { AfterViewInit, Component, forwardRef, HostListener, Inject, Input, OnDestroy, ViewChild } from '@angular/core';
import { AfterViewInit, Component, forwardRef, HostListener, Inject, Input, ViewChild } from '@angular/core';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { delay, tap } from 'rxjs/operators';
import { AutoUnsubscribe, trackByFactory } from '../../utils';
import { trackByFactory, ContextComponent } from '../../utils';
import { IListable } from '../models';
import { ListingComponent, ListingService } from '../index';
import { HasScrollbarDirective } from '../../scrollbar';
import { BehaviorSubject } from 'rxjs';
import { HelpModeService } from '../../help-mode';
interface TableContentTemplate {
checkViewportSize: () => void,
lastScrolledIndex: number,
hasScrollbarDirective: () => void,
noContent: boolean,
sortedDisplayedEntities: any[],
isHelpModeActive: boolean,
}
@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<T extends IListable> extends ContextComponent<TableContentTemplate> implements AfterViewInit {
@Input() itemSize!: number;
@Input() itemMouseEnterFn?: (entity: T) => void;
@Input() itemMouseLeaveFn?: (entity: T) => void;
@ -28,17 +37,18 @@ export class TableContentComponent<T extends IListable> extends AutoUnsubscribe
private _lastScrolledIndex = 0;
private _multiSelectActive$ = new BehaviorSubject(false);
readonly #checkViewportSize$ = this.listingComponent.noContent$.pipe(tap(() => {
setTimeout(() => {
this.scrollViewport?.checkViewportSize();
}, 0);
}));
constructor(
@Inject(forwardRef(() => ListingComponent)) readonly listingComponent: ListingComponent<T>,
readonly listingService: ListingService<T>,
readonly helpModeService: HelpModeService,
) {
super();
this.addSubscription = this.listingComponent.noContent$.subscribe(() => {
setTimeout(() => {
this.scrollViewport?.checkViewportSize();
}, 0);
});
}
multiSelect(entity: T, $event: MouseEvent): void {
@ -49,13 +59,21 @@ export class TableContentComponent<T extends IListable> extends AutoUnsubscribe
}
ngAfterViewInit(): void {
this.addSubscription = this.scrollViewport.scrolledIndexChange.pipe(tap(index => (this._lastScrolledIndex = index))).subscribe();
this.addSubscription = this.listingService.displayedLength$
const lastScrolledIndex$ = this.scrollViewport.scrolledIndexChange.pipe(tap(index => (this._lastScrolledIndex = index)))
const hasScrollbarDirective$ = this.listingService.displayedLength$
.pipe(
delay(100),
tap(() => this.hasScrollbarDirective.process()),
)
.subscribe();
super._initContext({
checkViewportSize: this.#checkViewportSize$,
lastScrolledIndex: lastScrolledIndex$,
hasScrollbarDirective: hasScrollbarDirective$,
noContent: this.listingComponent.noContent$,
sortedDisplayedEntities: this.listingComponent.sortedDisplayedEntities$,
isHelpModeActive: this.helpModeService.isHelpModeActive$,
})
}
scrollToLastIndex(): void {

View File

@ -13,7 +13,6 @@ import {
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { AutoUnsubscribe } from '../../utils';
import { IListable, ListingModes, TableColumnConfig } from '../models';
import { ListingComponent } from '../listing-component.directive';
import { EntitiesService } from '../services';
@ -26,7 +25,7 @@ const SCROLLBAR_WIDTH = 11;
templateUrl: './table.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TableComponent<T extends IListable> extends AutoUnsubscribe implements OnChanges {
export class TableComponent<T extends IListable> implements OnChanges {
readonly listingModes = ListingModes;
@Input() tableColumnConfigs!: readonly TableColumnConfig<T>[];
@ -56,9 +55,7 @@ export class TableComponent<T extends IListable> extends AutoUnsubscribe impleme
private readonly _hostRef: ViewContainerRef,
private readonly _changeRef: ChangeDetectorRef,
readonly entitiesService: EntitiesService<T>,
) {
super();
}
) {}
get tableHeaderLabel(): string | undefined {
return this.listingComponent.tableHeaderLabel;

View File

@ -1,41 +1,43 @@
<ng-container *ngIf="column.entities | async as entities">
<div class="heading">
<span>{{ column.label | translate }} ({{ entities.length || 0 }})</span>
<span
(click)="enableSelection()"
*ngIf="!activeSelection && !selectionColumn && entities.length > 1"
class="all-caps-label primary pointer"
translate="workflow.selection.select"
></span>
<div *ngIf="activeSelection" class="d-flex">
<span (click)="selectAll()" class="all-caps-label primary pointer mr-10" translate="workflow.selection.all"></span>
<span (click)="selectNone()" class="all-caps-label primary pointer" translate="workflow.selection.none"></span>
</div>
</div>
<div *ngIf="activeSelection" class="multi-select mb-8">
<div class="selected-wrapper">
<iqser-round-checkbox
(click)="toggleSelectAll()"
[active]="allSelected$ | async"
[indeterminate]="indeterminate$ | async"
type="with-bg"
></iqser-round-checkbox>
<ng-container *ngIf="componentContext$ | async as ctx">
<ng-container *ngIf="ctx.entities as entities">
<div class="heading">
<span>{{ column.label | translate }} ({{ entities.length || 0 }})</span>
<span
[translateParams]="{ count: listingService.selectedLength$ | async }"
[translate]="'workflow.selection.count'"
class="all-caps-label"
(click)="enableSelection()"
*ngIf="!activeSelection && !selectionColumn && entities.length > 1"
class="all-caps-label primary pointer"
translate="workflow.selection.select"
></span>
<div *ngIf="activeSelection" class="d-flex">
<span (click)="selectAll()" class="all-caps-label primary pointer mr-10" translate="workflow.selection.all"></span>
<span (click)="selectNone()" class="all-caps-label primary pointer" translate="workflow.selection.none"></span>
</div>
</div>
<div *ngIf="activeSelection" class="multi-select mb-8">
<div class="selected-wrapper">
<iqser-round-checkbox
(click)="toggleSelectAll()"
[active]="ctx.allSelected"
[indeterminate]="ctx.indeterminate"
type="with-bg"
></iqser-round-checkbox>
<div #bulkActionsContainer class="flex-1 overflow-hidden">
<ng-container
*ngIf="bulkActionsContainerWidth"
[ngTemplateOutletContext]="{ maxWidth: bulkActionsContainerWidth }"
[ngTemplateOutlet]="bulkActions"
></ng-container>
<span
[translateParams]="{ count: listingService.selectedLength$ | async }"
[translate]="'workflow.selection.count'"
class="all-caps-label"
></span>
</div>
<div #bulkActionsContainer class="flex-1 overflow-hidden">
<ng-container
*ngIf="bulkActionsContainerWidth"
[ngTemplateOutletContext]="{ maxWidth: bulkActionsContainerWidth }"
[ngTemplateOutlet]="bulkActions"
></ng-container>
</div>
<iqser-circle-button (action)="disableSelection()" [type]="circleButtonTypes.primary" icon="iqser:close"></iqser-circle-button>
</div>
<iqser-circle-button (action)="disableSelection()" [type]="circleButtonTypes.primary" icon="iqser:close"></iqser-circle-button>
</div>
</ng-container>
</ng-container>

View File

@ -14,17 +14,25 @@ import { combineLatest, Observable } from 'rxjs';
import { filter, map, tap } from 'rxjs/operators';
import { CircleButtonTypes } from '../../../buttons';
import { IListable } from '../../models';
import { AutoUnsubscribe, Debounce } from '../../../utils';
import { Debounce, ContextComponent } from '../../../utils';
import { ListingService } from '../../services';
import { WorkflowColumn } from '../models/workflow-column.model';
interface ColumnHeaderTemplate {
entities: IListable[],
allSelected: boolean,
indeterminate: boolean,
selectedLength: number,
disableSelection: () => void,
}
@Component({
selector: 'iqser-column-header [column] [selectionColumn]',
templateUrl: './column-header.component.html',
styleUrls: ['./column-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ColumnHeaderComponent<T extends IListable, K extends string> extends AutoUnsubscribe implements OnInit {
export class ColumnHeaderComponent<T extends IListable, K extends string> extends ContextComponent<ColumnHeaderTemplate> implements OnInit {
readonly circleButtonTypes = CircleButtonTypes;
@Input() column!: WorkflowColumn<T, K>;
@ -32,9 +40,6 @@ export class ColumnHeaderComponent<T extends IListable, K extends string> extend
@Input() bulkActions?: TemplateRef<unknown>;
@Output() readonly selectionColumnChange = new EventEmitter<WorkflowColumn<T, K> | undefined>();
allSelected$!: Observable<boolean>;
indeterminate$!: Observable<boolean>;
bulkActionsContainerWidth?: number;
@ViewChild('bulkActionsContainer') bulkActionsContainer?: ElementRef;
@ -48,18 +53,25 @@ export class ColumnHeaderComponent<T extends IListable, K extends string> extend
}
ngOnInit(): void {
this.allSelected$ = combineLatest([this.listingService.selectedLength$, this.column.entities]).pipe(
const allSelected$ = combineLatest([this.listingService.selectedLength$, this.column.entities]).pipe(
map(([length, columnEntities]) => length > 0 && length === columnEntities.length),
);
this.indeterminate$ = combineLatest([this.listingService.selectedLength$, this.column.entities]).pipe(
const indeterminate$ = combineLatest([this.listingService.selectedLength$, this.column.entities]).pipe(
map(([length, columnEntities]) => length > 0 && length !== columnEntities.length),
);
this.addSubscription = this.column.entities
const disableSelection$ = this.column.entities
.pipe(
filter(entities => entities.length <= 1),
tap(() => this.disableSelection()),
)
.subscribe();
);
super._initContext({
entities: this.column.entities,
allSelected: allSelected$,
indeterminate: indeterminate$,
selectedLength: this.listingService.selectedLength$,
disableSelection: disableSelection$,
})
}
toggleSelectAll(): void {

View File

@ -1,77 +1,79 @@
<iqser-table-header [tableHeaderLabel]="listingComponent.tableHeaderLabel" listingMode="workflow">
<ng-container *ngTemplateOutlet="headerTemplate"></ng-container>
</iqser-table-header>
<ng-container *ngIf="componentContext$ | async">
<iqser-table-header [tableHeaderLabel]="listingComponent.tableHeaderLabel" listingMode="workflow">
<ng-container *ngTemplateOutlet="headerTemplate"></ng-container>
</iqser-table-header>
<iqser-empty-state
(action)="noDataAction.emit()"
*ngIf="entitiesService.noData$ | async"
[buttonIcon]="noDataButtonIcon"
[buttonLabel]="noDataButtonLabel"
[icon]="noDataIcon"
[showButton]="showNoDataButton"
[text]="noDataText"
></iqser-empty-state>
<iqser-empty-state
(action)="noDataAction.emit()"
*ngIf="entitiesService.noData$ | async"
[buttonIcon]="noDataButtonIcon"
[buttonLabel]="noDataButtonLabel"
[icon]="noDataIcon"
[showButton]="showNoDataButton"
[text]="noDataText"
></iqser-empty-state>
<div
*ngIf="(entitiesService.noData$ | async) === false && (draggingEntities$ | async) as draggingEntities"
cdkDropListGroup
class="columns-wrapper"
>
<div
*ngFor="let column of config.columns"
[class.dragging]="dragging"
[class.list-can-receive]="isReceiving(column)"
[class.list-dragging]="isDragging(column)"
[class.list-source]="isSource(column)"
[style.--color]="column.color"
class="column"
*ngIf="(entitiesService.noData$ | async) === false && (draggingEntities$ | async) as draggingEntities"
cdkDropListGroup
class="columns-wrapper"
>
<iqser-column-header [(selectionColumn)]="selectionColumn" [bulkActions]="bulkActions" [column]="column"></iqser-column-header>
<div
(cdkDropListDropped)="move($event)"
*ngIf="column.entities | async as entities"
[cdkDropListData]="entities"
[cdkDropListEnterPredicate]="canMoveTo(column)"
[class.multi-select-active]="selectionColumn === column"
[id]="column.key"
cdkDropList
cdkDropListSortingDisabled
*ngFor="let column of config.columns"
[class.dragging]="dragging"
[class.list-can-receive]="isReceiving(column)"
[class.list-dragging]="isDragging(column)"
[class.list-source]="isSource(column)"
[style.--color]="column.color"
class="column"
>
<iqser-column-header [(selectionColumn)]="selectionColumn" [bulkActions]="bulkActions" [column]="column"></iqser-column-header>
<div
(cdkDragEnded)="stopDragging()"
(cdkDragStarted)="startDragging(column, $event)"
(click)="selectionColumn === column && listingService.select(entity)"
*ngFor="let entity of entities; trackBy: trackBy"
[cdkDragData]="entity"
[class.no-border]="dragging && draggingEntities.includes(entity)"
[class.selected]="all[entity.id].isSelected$ | async"
[ngClass]="all[entity.id].classes$ | async"
cdkDrag
(cdkDropListDropped)="move($event)"
*ngIf="column.entities | async as entities"
[cdkDropListData]="entities"
[cdkDropListEnterPredicate]="canMoveTo(column)"
[class.multi-select-active]="selectionColumn === column"
[id]="column.key"
cdkDropList
cdkDropListSortingDisabled
>
<ng-container *ngIf="!draggingEntities.includes(entity)">
<ng-container *ngTemplateOutlet="itemTemplate; context: { entity: entity }"></ng-container>
</ng-container>
<ng-template cdkDragPlaceholder>
<div *ngFor="let e of draggingEntities" [style.min-height]="itemHeight + 'px'" class="placeholder"></div>
</ng-template>
<ng-template cdkDragPreview>
<ng-container *ngFor="let e of draggingEntities">
<div
[class.selected]="all[e.id].isSelected$ | async"
[ngClass]="all[e.id].classes$ | async"
[style.width]="itemWidth + 'px'"
>
<ng-container *ngTemplateOutlet="itemTemplate; context: { entity: e }"></ng-container>
</div>
<div
(cdkDragEnded)="stopDragging()"
(cdkDragStarted)="startDragging(column, $event)"
(click)="selectionColumn === column && listingService.select(entity)"
*ngFor="let entity of entities; trackBy: trackBy"
[cdkDragData]="entity"
[class.no-border]="dragging && draggingEntities.includes(entity)"
[class.selected]="all[entity.id].isSelected$ | async"
[ngClass]="all[entity.id].classes$ | async"
cdkDrag
>
<ng-container *ngIf="!draggingEntities.includes(entity)">
<ng-container *ngTemplateOutlet="itemTemplate; context: { entity: entity }"></ng-container>
</ng-container>
</ng-template>
</div>
<div (click)="addElement.emit()" *ngIf="column.key === addElementColumn" class="add-btn">
<mat-icon [svgIcon]="addElementIcon"></mat-icon>
<ng-template cdkDragPlaceholder>
<div *ngFor="let e of draggingEntities" [style.min-height]="itemHeight + 'px'" class="placeholder"></div>
</ng-template>
<ng-template cdkDragPreview>
<ng-container *ngFor="let e of draggingEntities">
<div
[class.selected]="all[e.id].isSelected$ | async"
[ngClass]="all[e.id].classes$ | async"
[style.width]="itemWidth + 'px'"
>
<ng-container *ngTemplateOutlet="itemTemplate; context: { entity: e }"></ng-container>
</div>
</ng-container>
</ng-template>
</div>
<div (click)="addElement.emit()" *ngIf="column.key === addElementColumn" class="add-btn">
<mat-icon [svgIcon]="addElementIcon"></mat-icon>
</div>
</div>
</div>
</div>
</div>
</ng-container>

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 { AutoUnsubscribe, Debounce, trackByFactory } from '../../utils';
import { Debounce, trackByFactory, ContextComponent } from '../../utils';
import { IListable } from '../models';
import { EntitiesService, ListingService } from '../services';
import { BehaviorSubject } from 'rxjs';
@ -24,13 +24,18 @@ import { WorkflowConfig } from './models/workflow-config.model';
import { WorkflowColumn } from './models/workflow-column.model';
import { EntityWrapper } from './models/entity-wrapper.model';
interface WorkflowTemplate {
updateConfigItems: unknown;
setupResizeObserver: unknown;
}
@Component({
selector: 'iqser-workflow [itemTemplate] [config] [addElementIcon]',
templateUrl: './workflow.component.html',
styleUrls: ['./workflow.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WorkflowComponent<T extends IListable, K extends string> extends AutoUnsubscribe implements OnInit {
export class WorkflowComponent<T extends IListable, K extends string> extends ContextComponent<WorkflowTemplate> implements OnInit {
@Input() headerTemplate?: TemplateRef<unknown>;
@Input() itemTemplate!: TemplateRef<T>;
@Input() config!: WorkflowConfig<T, K>;
@ -81,14 +86,18 @@ export class WorkflowComponent<T extends IListable, K extends string> extends Au
for (const column of this.config.columns) {
column.entities = new BehaviorSubject<T[]>([]);
}
this.addSubscription = this.listingService.displayed$.pipe(tap(entities => this._updateConfigItems(entities))).subscribe();
this.addSubscription = this.entitiesService.noData$
const updateConfigItems$ = this.listingService.displayed$.pipe(tap(entities => this._updateConfigItems(entities)))
const setupResizeObserver$ = this.entitiesService.noData$
.pipe(
filter(noData => noData),
tap(() => this._setupResizeObserver()),
)
.subscribe();
);
this._setupResizeObserver();
super._initContext({
updateConfigItems: updateConfigItems$,
setupResizeObserver: setupResizeObserver$
})
}
canMoveTo(column: WorkflowColumn<T, K>): () => boolean {

View File

@ -0,0 +1,17 @@
import { combineLatest, Observable, of } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { ValuesOf } from './types/utility-types';
export class ContextComponent<T> {
componentContext$: Observable<T> | null = of({} as T);
protected _initContext(context: Record<string, Observable<ValuesOf<T>>>): void {
const observables = Object.values(context).map(obs => obs.pipe(startWith(null)));
const keys = Object.keys(context);
this.componentContext$ = combineLatest(observables).pipe(map(values => this._mapKeysToObs(keys, values)));
}
protected _mapKeysToObs(keys: string[], observables: (ValuesOf<T> | null) []): T {
return keys.reduce((acc, key, index) => ({ ...acc, [key]: observables[index] }), {} as T);
}
}

View File

@ -15,3 +15,4 @@ export * from './types/iqser-types';
export * from './pruning-translation-loader';
export * from './custom-route-reuse.strategy';
export * from './headers-configuration';
export * from './context.component';