Handle deleted entities

This commit is contained in:
Adina Țeudan 2022-01-13 00:19:52 +02:00
parent fad201fcc6
commit a1c1be7edc
11 changed files with 150 additions and 73 deletions

View File

@ -14,7 +14,7 @@ import {
ToastComponent,
} from './misc';
import { FullPageLoadingIndicatorComponent } from './loading';
import { FullPageErrorComponent } from './error';
import { ConnectionStatusComponent, FullPageErrorComponent } from './error';
import { IqserListingModule } from './listing';
import { IqserFiltersModule } from './filtering';
import { IqserInputsModule } from './inputs';
@ -45,6 +45,7 @@ const modules = [
const components = [
StatusBarComponent,
FullPageLoadingIndicatorComponent,
ConnectionStatusComponent,
FullPageErrorComponent,
LogoComponent,
HiddenActionComponent,

View File

@ -0,0 +1,8 @@
<div
*ngIf="errorService.connectionStatus$ | async as status"
[@animateOpenClose]="status"
[ngClass]="status"
class="indicator flex-align-items-center"
>
<span [translate]="connectionStatusTranslations[status]"></span>
</div>

View File

@ -0,0 +1,32 @@
@use 'sass:math';
.indicator {
$width: 160px;
position: fixed;
top: 0;
right: 50%;
left: 50%;
height: 40px;
margin-left: -(math.div($width, 2));
width: $width;
border-radius: 8px;
z-index: 5;
span {
color: var(--iqser-grey-1);
text-align: center;
line-height: 18px;
font-weight: 600;
height: 18px;
width: 100%;
}
}
.offline {
background: var(--iqser-yellow-2);
}
.online {
background: var(--iqser-green-2);
}

View File

@ -0,0 +1,24 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { connectionStatusTranslations } from '../../translations/connection-status-translations';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { ErrorService } from '../error.service';
@Component({
selector: 'iqser-connection-status',
templateUrl: './connection-status.component.html',
styleUrls: ['./connection-status.component.scss'],
animations: [
trigger('animateOpenClose', [
state('offline', style({ top: '100px' })),
state('online', style({ top: '-100px' })),
transition('* => offline', animate('1s ease-out')),
transition('* => online', animate('3s ease-in')),
]),
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ConnectionStatusComponent {
connectionStatusTranslations = connectionStatusTranslations;
constructor(readonly errorService: ErrorService) {}
}

View File

@ -6,6 +6,24 @@ import { delay, filter, map, mapTo } from 'rxjs/operators';
import { NavigationStart, Router } from '@angular/router';
import { shareLast } from '../utils';
export class CustomError {
readonly label: string;
readonly actionLabel: string;
readonly actionIcon: string;
readonly message?: string;
readonly action?: () => void;
constructor(label: string, actionLabel: string, actionIcon: string, message?: string, action?: () => void) {
this.label = label;
this.actionLabel = actionLabel;
this.actionIcon = actionIcon;
this.message = message;
this.action = action;
}
}
export type ErrorType = HttpErrorResponse | CustomError | undefined;
const BANDWIDTH_LIMIT_EXCEEDED = 509 as const;
const OFFLINE_STATUSES = [
HttpStatusCode.BadGateway,
@ -14,15 +32,15 @@ const OFFLINE_STATUSES = [
BANDWIDTH_LIMIT_EXCEEDED,
] as const;
const isOffline = (error?: HttpErrorResponse) => !!error && OFFLINE_STATUSES.includes(error.status);
const isOffline = (error?: ErrorType) => error instanceof HttpErrorResponse && OFFLINE_STATUSES.includes(error.status);
@Injectable({ providedIn: 'root' })
export class ErrorService {
readonly error$ = new Subject<HttpErrorResponse | undefined>();
readonly offline$: Observable<Event>;
readonly online$: Observable<Event>;
readonly serverError$ = this.error$.pipe(filter(error => !error || !isOffline(error)));
readonly connectionStatus$: Observable<string | undefined>;
private readonly _error$ = new Subject<ErrorType>();
readonly error$ = this._error$.pipe(filter(error => !error || !isOffline(error)));
private readonly _online$ = new Subject();
constructor(private readonly _loadingService: LoadingService, private readonly _router: Router) {
@ -36,9 +54,9 @@ export class ErrorService {
this.connectionStatus$ = merge(this.online$, this.offline$, removeIndicator$).pipe(map(event => event?.type));
}
set(error: HttpErrorResponse): void {
set(error: ErrorType): void {
this._loadingService.stop();
this.error$.next(error);
this._error$.next(error);
}
setOnline(): void {
@ -46,11 +64,11 @@ export class ErrorService {
}
clear(): void {
this.error$.next();
this._error$.next();
}
private _offline() {
return merge(fromEvent(window, 'offline'), this.error$.pipe(filter(isOffline), mapTo(new Event('offline')), shareLast()));
return merge(fromEvent(window, 'offline'), this._error$.pipe(filter(isOffline), mapTo(new Event('offline')), shareLast()));
}
private _online() {

View File

@ -1,28 +1,19 @@
<div
*ngIf="errorService.connectionStatus$ | async as status"
[@animateOpenClose]="status"
[ngClass]="status"
class="indicator flex-align-items-center"
>
<span [translate]="translations[status]"></span>
</div>
<ng-container *ngIf="errorService.serverError$ | async as error">
<ng-container *ngIf="errorService.error$ | async as error">
<section class="full-page-section"></section>
<section class="full-page-content flex-align-items-center">
<mat-icon svgIcon="iqser:failure"></mat-icon>
<div [translate]="'error.title'" class="heading-l mt-24"></div>
<div [translate]="errorTitle(error)" class="heading-l mt-24"></div>
<div class="mt-16 error">{{ error.message }}</div>
<div *ngIf="error.message" class="mt-16 error">{{ error.message }}</div>
<iqser-icon-button
(action)="reload()"
[label]="'error.reload' | translate"
(action)="action(error)"
[icon]="actionIcon(error)"
[label]="actionLabel(error) | translate"
[type]="iconButtonTypes.primary"
class="mt-20"
icon="iqser:refresh"
></iqser-icon-button>
</section>
</ng-container>

View File

@ -1,36 +1,3 @@
@use 'sass:math';
.indicator {
$width: 160px;
position: fixed;
top: 0;
right: 50%;
left: 50%;
height: 40px;
margin-left: -(math.div($width, 2));
width: $width;
border-radius: 8px;
z-index: 5;
span {
color: var(--iqser-grey-1);
text-align: center;
line-height: 18px;
font-weight: 600;
height: 18px;
width: 100%;
}
}
.offline {
background: var(--iqser-yellow-2);
}
.online {
background: var(--iqser-green-2);
}
.full-page-section {
opacity: 0.95;
}
@ -50,7 +17,7 @@
}
}
:is(.offline-box, .full-page-content) .heading-l {
color: #9398a0;
.heading-l {
color: var(--iqser-grey-7);
font-weight: initial;
}

View File

@ -1,30 +1,36 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IconButtonTypes } from '../../buttons';
import { ErrorService } from '../error.service';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { connectionStatusTranslations } from '../../translations/connection-status-translations';
import { CustomError, ErrorService, ErrorType } from '../error.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@Component({
selector: 'iqser-full-page-error',
templateUrl: './full-page-error.component.html',
styleUrls: ['./full-page-error.component.scss'],
animations: [
trigger('animateOpenClose', [
state('offline', style({ top: '100px' })),
state('online', style({ top: '-100px' })),
transition('* => offline', animate('1s ease-out')),
transition('* => online', animate('3s ease-in')),
]),
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FullPageErrorComponent {
translations = connectionStatusTranslations;
readonly iconButtonTypes = IconButtonTypes;
constructor(readonly errorService: ErrorService) {}
reload(): void {
window.location.reload();
errorTitle(error: ErrorType): string {
return error instanceof CustomError ? error.label : _('error.title');
}
actionLabel(error: ErrorType): string {
return error instanceof CustomError ? error.actionLabel : _('error.reload');
}
actionIcon(error: ErrorType): string {
return error instanceof CustomError ? error.actionIcon : 'iqser:refresh';
}
action(error: ErrorType): void {
if (error instanceof CustomError && error.action) {
error.action();
} else {
window.location.reload();
}
}
}

View File

@ -2,3 +2,4 @@ export * from './error.service';
export * from './max-retries.token';
export * from './server-error-interceptor';
export * from './full-page-error/full-page-error.component';
export * from './connection-status/connection-status.component';

View File

@ -22,6 +22,7 @@ export class EntitiesService<E extends IListable, I = E> extends GenericService<
readonly all$: Observable<E[]>;
readonly allLength$: Observable<number>;
protected readonly _entityChanged$ = new Subject<E>();
protected readonly _entityDeleted$ = new Subject<E>();
private readonly _all$ = new BehaviorSubject<E[]>([]);
constructor(
@ -68,8 +69,13 @@ export class EntitiesService<E extends IListable, I = E> extends GenericService<
);
}
getEntityDeleted$(entityId: string): Observable<E | undefined> {
return this._entityDeleted$.pipe(filter(entity => entity.id === entityId));
}
setEntities(entities: E[]): void {
const changedEntities: E[] = [];
const deletedEntities = this.all.filter(oldEntity => !entities.find(newEntity => newEntity.id === oldEntity.id));
// Keep old object references for unchanged entities
const newEntities = entities.map(entity => {
@ -90,6 +96,10 @@ export class EntitiesService<E extends IListable, I = E> extends GenericService<
for (const entity of changedEntities) {
this._entityChanged$.next(entity);
}
for (const entity of deletedEntities) {
this._entityDeleted$.next(entity);
}
}
find(id: string): E | undefined {

View File

@ -1,12 +1,14 @@
import { Directive, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { OnDetach } from './custom-route-reuse.strategy';
/**
* Inherit this class when you need to subscribe to observables in your components
*/
@Directive()
export abstract class AutoUnsubscribe implements OnDestroy {
export abstract class AutoUnsubscribe implements OnDestroy, OnDetach {
private readonly _subscriptions = new Subscription();
private readonly _activeScreenSubscriptions = new Subscription();
/**
* Call this method when you want to subscribe to an observable
@ -17,12 +19,29 @@ export abstract class AutoUnsubscribe implements OnDestroy {
this._subscriptions.add(subscription);
}
/**
* Call this method when you want to subscribe to an observable while the screen is active
* @param subscription - the new subscription to add to subscriptions array
*/
set addActiveScreenSubscription(subscription: Subscription) {
this._activeScreenSubscriptions.closed = false;
this._activeScreenSubscriptions.add(subscription);
}
/**
* This method unsubscribes active subscriptions
* If you implement OnDestroy in a component that inherits AutoUnsubscribeComponent,
* then you must explicitly call super.ngOnDestroy()
*/
ngOnDestroy(): void {
this.ngOnDetach();
this._subscriptions.unsubscribe();
}
/**
* This method unsubscribes active subscriptions for active screens
*/
ngOnDetach(): void {
this._activeScreenSubscriptions.unsubscribe();
}
}