Handle deleted entities
This commit is contained in:
parent
fad201fcc6
commit
a1c1be7edc
@ -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,
|
||||
|
||||
@ -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>
|
||||
@ -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);
|
||||
}
|
||||
@ -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) {}
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user