diff --git a/src/lib/common-ui.module.ts b/src/lib/common-ui.module.ts index 5b01813..19d0947 100644 --- a/src/lib/common-ui.module.ts +++ b/src/lib/common-ui.module.ts @@ -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, diff --git a/src/lib/error/connection-status/connection-status.component.html b/src/lib/error/connection-status/connection-status.component.html new file mode 100644 index 0000000..45b1490 --- /dev/null +++ b/src/lib/error/connection-status/connection-status.component.html @@ -0,0 +1,8 @@ +
+ +
diff --git a/src/lib/error/connection-status/connection-status.component.scss b/src/lib/error/connection-status/connection-status.component.scss new file mode 100644 index 0000000..f7f2fb1 --- /dev/null +++ b/src/lib/error/connection-status/connection-status.component.scss @@ -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); +} diff --git a/src/lib/error/connection-status/connection-status.component.ts b/src/lib/error/connection-status/connection-status.component.ts new file mode 100644 index 0000000..fab4848 --- /dev/null +++ b/src/lib/error/connection-status/connection-status.component.ts @@ -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) {} +} diff --git a/src/lib/error/error.service.ts b/src/lib/error/error.service.ts index 237ac1e..ee07896 100644 --- a/src/lib/error/error.service.ts +++ b/src/lib/error/error.service.ts @@ -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(); readonly offline$: Observable; readonly online$: Observable; - readonly serverError$ = this.error$.pipe(filter(error => !error || !isOffline(error))); readonly connectionStatus$: Observable; + private readonly _error$ = new Subject(); + 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() { diff --git a/src/lib/error/full-page-error/full-page-error.component.html b/src/lib/error/full-page-error/full-page-error.component.html index 864ed88..eb9b099 100644 --- a/src/lib/error/full-page-error/full-page-error.component.html +++ b/src/lib/error/full-page-error/full-page-error.component.html @@ -1,28 +1,19 @@ -
- -
- - +
-
+
-
{{ error.message }}
+
{{ error.message }}
diff --git a/src/lib/error/full-page-error/full-page-error.component.scss b/src/lib/error/full-page-error/full-page-error.component.scss index cc2b1f9..6bab451 100644 --- a/src/lib/error/full-page-error/full-page-error.component.scss +++ b/src/lib/error/full-page-error/full-page-error.component.scss @@ -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; } diff --git a/src/lib/error/full-page-error/full-page-error.component.ts b/src/lib/error/full-page-error/full-page-error.component.ts index 1c29a90..0a2f7a6 100644 --- a/src/lib/error/full-page-error/full-page-error.component.ts +++ b/src/lib/error/full-page-error/full-page-error.component.ts @@ -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(); + } } } diff --git a/src/lib/error/index.ts b/src/lib/error/index.ts index 42fbcd1..bf107e8 100644 --- a/src/lib/error/index.ts +++ b/src/lib/error/index.ts @@ -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'; diff --git a/src/lib/listing/services/entities.service.ts b/src/lib/listing/services/entities.service.ts index b326182..bb7f627 100644 --- a/src/lib/listing/services/entities.service.ts +++ b/src/lib/listing/services/entities.service.ts @@ -22,6 +22,7 @@ export class EntitiesService extends GenericService< readonly all$: Observable; readonly allLength$: Observable; protected readonly _entityChanged$ = new Subject(); + protected readonly _entityDeleted$ = new Subject(); private readonly _all$ = new BehaviorSubject([]); constructor( @@ -68,8 +69,13 @@ export class EntitiesService extends GenericService< ); } + getEntityDeleted$(entityId: string): Observable { + 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 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 { diff --git a/src/lib/utils/auto-unsubscribe.directive.ts b/src/lib/utils/auto-unsubscribe.directive.ts index f912ca1..1431dc3 100644 --- a/src/lib/utils/auto-unsubscribe.directive.ts +++ b/src/lib/utils/auto-unsubscribe.directive.ts @@ -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(); + } }