Feature/set up notification service (#3663)

* Set up notification service

* Update changelog
pull/3667/head
Thomas Kaul 4 months ago committed by GitHub
parent 9246a73f41
commit 2893d71377
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Set up a notification service for alert and confirmation dialogs
### Changed ### Changed
- Refactored the dark theme CSS selector - Refactored the dark theme CSS selector

@ -270,6 +270,7 @@ export class AppComponent implements OnDestroy, OnInit {
locale: this.user?.settings?.locale locale: this.user?.settings?.locale
}, },
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
maxWidth: this.deviceType === 'mobile' ? '95vw' : '50rem',
width: this.deviceType === 'mobile' ? '100vw' : '50rem' width: this.deviceType === 'mobile' ? '100vw' : '50rem'
}); });

@ -33,6 +33,7 @@ import { GfSubscriptionInterstitialDialogModule } from './components/subscriptio
import { authInterceptorProviders } from './core/auth.interceptor'; import { authInterceptorProviders } from './core/auth.interceptor';
import { httpResponseInterceptorProviders } from './core/http-response.interceptor'; import { httpResponseInterceptorProviders } from './core/http-response.interceptor';
import { LanguageService } from './core/language.service'; import { LanguageService } from './core/language.service';
import { GfNotificationModule } from './core/notification/notification.module';
export function NgxStripeFactory(): string { export function NgxStripeFactory(): string {
return environment.stripePublicKey; return environment.stripePublicKey;
@ -47,6 +48,7 @@ export function NgxStripeFactory(): string {
BrowserModule, BrowserModule,
GfHeaderModule, GfHeaderModule,
GfLogoComponent, GfLogoComponent,
GfNotificationModule,
GfSubscriptionInterstitialDialogModule, GfSubscriptionInterstitialDialogModule,
MarkdownModule.forRoot(), MarkdownModule.forRoot(),
MatAutocompleteModule, MatAutocompleteModule,

@ -1,3 +1,5 @@
import { ConfirmationDialogType } from '@ghostfolio/client/core/notification/confirmation-dialog/confirmation-dialog.type';
import { NotificationService } from '@ghostfolio/client/core/notification/notification.service';
import { getLocale } from '@ghostfolio/common/helper'; import { getLocale } from '@ghostfolio/common/helper';
import { import {
@ -54,7 +56,10 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor(private router: Router) {} public constructor(
private notificationService: NotificationService,
private router: Router
) {}
public ngOnInit() {} public ngOnInit() {}
@ -97,13 +102,13 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
} }
public onDeleteAccount(aId: string) { public onDeleteAccount(aId: string) {
const confirmation = confirm( this.notificationService.confirm({
$localize`Do you really want to delete this account?` confirmFn: () => {
);
if (confirmation) {
this.accountDeleted.emit(aId); this.accountDeleted.emit(aId);
} },
confirmType: ConfirmationDialogType.Warn,
title: $localize`Do you really want to delete this account?`
});
} }
public onOpenAccountDetailDialog(accountId: string) { public onOpenAccountDetailDialog(accountId: string) {

@ -1,3 +1,4 @@
import { NotificationService } from '@ghostfolio/client/core/notification/notification.service';
import { import {
getLocale, getLocale,
getNumberFormatDecimal, getNumberFormatDecimal,
@ -39,7 +40,7 @@ export class PortfolioPerformanceComponent implements OnChanges {
@ViewChild('value') value: ElementRef; @ViewChild('value') value: ElementRef;
public constructor() {} public constructor(private notificationService: NotificationService) {}
public ngOnChanges() { public ngOnChanges() {
this.precision = this.precision >= 0 ? this.precision : 2; this.precision = this.precision >= 0 ? this.precision : 2;
@ -74,12 +75,15 @@ export class PortfolioPerformanceComponent implements OnChanges {
} }
public onShowErrors() { public onShowErrors() {
const errorMessageParts = [$localize`Market data is delayed for`]; const errorMessageParts = [];
for (const error of this.errors) { for (const error of this.errors) {
errorMessageParts.push(`${error.symbol} (${error.dataSource})`); errorMessageParts.push(`${error.symbol} (${error.dataSource})`);
} }
alert(errorMessageParts.join('\n')); this.notificationService.alert({
message: errorMessageParts.join('<br />'),
title: $localize`Market data is delayed for`
});
} }
} }

@ -1,16 +1,39 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { NotificationService } from './notification/notification.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class LayoutService { export class LayoutService {
public static readonly DEFAULT_NOTIFICATION_MAX_WIDTH = '50rem';
public static readonly DEFAULT_NOTIFICATION_WIDTH = '75vw';
public shouldReloadContent$: Observable<void>; public shouldReloadContent$: Observable<void>;
private shouldReloadSubject = new Subject<void>(); private shouldReloadSubject = new Subject<void>();
public constructor() { public constructor(
private deviceService: DeviceDetectorService,
private notificationService: NotificationService
) {
this.shouldReloadContent$ = this.shouldReloadSubject.asObservable(); this.shouldReloadContent$ = this.shouldReloadSubject.asObservable();
const deviceType = this.deviceService.getDeviceInfo().deviceType;
this.notificationService.setDialogWidth(
deviceType === 'mobile'
? '95vw'
: LayoutService.DEFAULT_NOTIFICATION_WIDTH
);
this.notificationService.setDialogMaxWidth(
deviceType === 'mobile'
? '95vw'
: LayoutService.DEFAULT_NOTIFICATION_MAX_WIDTH
);
} }
public getShouldReloadSubject() { public getShouldReloadSubject() {

@ -0,0 +1,27 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { IAlertDialogParams } from './interfaces/interfaces';
@Component({
imports: [CommonModule, MatButtonModule, MatDialogModule],
selector: 'gf-alert-dialog',
standalone: true,
styleUrls: ['./alert-dialog.scss'],
templateUrl: './alert-dialog.html'
})
export class GfAlertDialogComponent {
public discardLabel: string;
public message: string;
public title: string;
public constructor(public dialogRef: MatDialogRef<GfAlertDialogComponent>) {}
public initialize(aParams: IAlertDialogParams) {
this.discardLabel = aParams.discardLabel;
this.message = aParams.message;
this.title = aParams.title;
}
}

@ -0,0 +1,11 @@
@if (title) {
<div mat-dialog-title [innerHTML]="title"></div>
}
@if (message) {
<div mat-dialog-content [innerHTML]="message"></div>
}
<div align="end" mat-dialog-actions>
<button mat-button (click)="dialogRef.close()">{{ discardLabel }}</button>
</div>

@ -0,0 +1,6 @@
export interface IAlertDialogParams {
confirmLabel?: string;
discardLabel?: string;
message?: string;
title: string;
}

@ -0,0 +1,41 @@
import { CommonModule } from '@angular/common';
import { Component, HostListener } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { ConfirmationDialogType } from './confirmation-dialog.type';
import { IConfirmDialogParams } from './interfaces/interfaces';
@Component({
imports: [CommonModule, MatButtonModule, MatDialogModule],
selector: 'gf-confirmation-dialog',
standalone: true,
styleUrls: ['./confirmation-dialog.scss'],
templateUrl: './confirmation-dialog.html'
})
export class GfConfirmationDialogComponent {
public confirmLabel: string;
public confirmType: ConfirmationDialogType;
public discardLabel: string;
public message: string;
public title: string;
public constructor(
public dialogRef: MatDialogRef<GfConfirmationDialogComponent>
) {}
@HostListener('window:keyup', ['$event'])
public keyEvent(event: KeyboardEvent) {
if (event.key === 'Enter') {
this.dialogRef.close('confirm');
}
}
public initialize(aParams: IConfirmDialogParams) {
this.confirmLabel = aParams.confirmLabel;
this.confirmType = aParams.confirmType;
this.discardLabel = aParams.discardLabel;
this.message = aParams.message;
this.title = aParams.title;
}
}

@ -0,0 +1,20 @@
@if (title) {
<div mat-dialog-title [innerHTML]="title"></div>
}
@if (message) {
<div mat-dialog-content [innerHTML]="message"></div>
}
<div align="end" mat-dialog-actions>
<button mat-button (click)="dialogRef.close('discard')">
{{ discardLabel }}
</button>
<button
mat-flat-button
[color]="confirmType"
(click)="dialogRef.close('confirm')"
>
{{ confirmLabel }}
</button>
</div>

@ -0,0 +1,5 @@
export enum ConfirmationDialogType {
Accent = 'accent',
Primary = 'primary',
Warn = 'warn'
}

@ -0,0 +1,9 @@
import { ConfirmationDialogType } from '../confirmation-dialog.type';
export interface IConfirmDialogParams {
confirmLabel?: string;
confirmType: ConfirmationDialogType;
discardLabel?: string;
message?: string;
title: string;
}

@ -0,0 +1,19 @@
import { ConfirmationDialogType } from '../confirmation-dialog/confirmation-dialog.type';
export interface IAlertParams {
discardFn?: () => void;
discardLabel?: string;
message?: string;
title: string;
}
export interface IConfirmParams {
confirmFn: () => void;
confirmLabel?: string;
confirmType?: ConfirmationDialogType;
disableClose?: boolean;
discardFn?: () => void;
discardLabel?: string;
message?: string;
title: string;
}

@ -0,0 +1,18 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatDialogModule } from '@angular/material/dialog';
import { GfAlertDialogComponent } from './alert-dialog/alert-dialog.component';
import { GfConfirmationDialogComponent } from './confirmation-dialog/confirmation-dialog.component';
import { NotificationService } from './notification.service';
@NgModule({
imports: [
CommonModule,
GfAlertDialogComponent,
GfConfirmationDialogComponent,
MatDialogModule
],
providers: [NotificationService]
})
export class GfNotificationModule {}

@ -0,0 +1,83 @@
import { translate } from '@ghostfolio/ui/i18n';
import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { isFunction } from 'lodash';
import { GfAlertDialogComponent } from './alert-dialog/alert-dialog.component';
import { GfConfirmationDialogComponent } from './confirmation-dialog/confirmation-dialog.component';
import { ConfirmationDialogType } from './confirmation-dialog/confirmation-dialog.type';
import { IAlertParams, IConfirmParams } from './interfaces/interfaces';
@Injectable()
export class NotificationService {
private dialogMaxWidth: string;
private dialogWidth: string;
public constructor(private matDialog: MatDialog) {}
public alert(aParams: IAlertParams) {
if (!aParams.discardLabel) {
aParams.discardLabel = translate('CLOSE');
}
const dialog = this.matDialog.open(GfAlertDialogComponent, {
autoFocus: false,
maxWidth: this.dialogMaxWidth,
width: this.dialogWidth
});
dialog.componentInstance.initialize({
discardLabel: aParams.discardLabel,
message: aParams.message,
title: aParams.title
});
return dialog.afterClosed().subscribe((result) => {
if (isFunction(aParams.discardFn)) {
aParams.discardFn();
}
});
}
public confirm(aParams: IConfirmParams) {
if (!aParams.confirmLabel) {
aParams.confirmLabel = translate('YES');
}
if (!aParams.discardLabel) {
aParams.discardLabel = translate('CANCEL');
}
const dialog = this.matDialog.open(GfConfirmationDialogComponent, {
autoFocus: false,
disableClose: aParams.disableClose || false,
maxWidth: this.dialogMaxWidth,
width: this.dialogWidth
});
dialog.componentInstance.initialize({
confirmLabel: aParams.confirmLabel,
confirmType: aParams.confirmType || ConfirmationDialogType.Primary,
discardLabel: aParams.discardLabel,
message: aParams.message,
title: aParams.title
});
return dialog.afterClosed().subscribe((result) => {
if (result === 'confirm' && isFunction(aParams.confirmFn)) {
aParams.confirmFn();
} else if (result === 'discard' && isFunction(aParams.discardFn)) {
aParams.discardFn();
}
});
}
public setDialogMaxWidth(aDialogMaxWidth: string) {
this.dialogMaxWidth = aDialogMaxWidth;
}
public setDialogWidth(aDialogWidth: string) {
this.dialogWidth = aDialogWidth;
}
}

@ -359,10 +359,6 @@ ngx-skeleton-loader {
.cdk-global-overlay-wrapper { .cdk-global-overlay-wrapper {
justify-content: center !important; justify-content: center !important;
} }
.cdk-overlay-pane {
max-width: 95vw !important;
}
} }
.cursor-default { .cursor-default {

@ -6,7 +6,9 @@ const locales = {
ASSET_CLASS: $localize`Asset Class`, ASSET_CLASS: $localize`Asset Class`,
ASSET_SUB_CLASS: $localize`Asset Sub Class`, ASSET_SUB_CLASS: $localize`Asset Sub Class`,
BUY_AND_SELL_ACTIVITIES_TOOLTIP: $localize`Buy and sell`, BUY_AND_SELL_ACTIVITIES_TOOLTIP: $localize`Buy and sell`,
CANCEL: $localize`Cancel`,
CORE: $localize`Core`, CORE: $localize`Core`,
CLOSE: $localize`Close`,
DATA_IMPORT_AND_EXPORT_TOOLTIP_BASIC: $localize`Switch to Ghostfolio Premium or Ghostfolio Open Source easily`, DATA_IMPORT_AND_EXPORT_TOOLTIP_BASIC: $localize`Switch to Ghostfolio Premium or Ghostfolio Open Source easily`,
DATA_IMPORT_AND_EXPORT_TOOLTIP_OSS: $localize`Switch to Ghostfolio Premium easily`, DATA_IMPORT_AND_EXPORT_TOOLTIP_OSS: $localize`Switch to Ghostfolio Premium easily`,
DATA_IMPORT_AND_EXPORT_TOOLTIP_PREMIUM: $localize`Switch to Ghostfolio Open Source or Ghostfolio Basic easily`, DATA_IMPORT_AND_EXPORT_TOOLTIP_PREMIUM: $localize`Switch to Ghostfolio Open Source or Ghostfolio Basic easily`,
@ -26,6 +28,7 @@ const locales = {
TAG: $localize`Tag`, TAG: $localize`Tag`,
YEAR: $localize`Year`, YEAR: $localize`Year`,
YEARS: $localize`Years`, YEARS: $localize`Years`,
YES: $localize`Yes`,
// Activity types // Activity types
BUY: $localize`Buy`, BUY: $localize`Buy`,

Loading…
Cancel
Save