diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a7879844..e600373b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Set up a notification service for alert and confirmation dialogs + ### Changed - Refactored the dark theme CSS selector diff --git a/apps/client/src/app/app.component.ts b/apps/client/src/app/app.component.ts index d432d9619..ad6e6e808 100644 --- a/apps/client/src/app/app.component.ts +++ b/apps/client/src/app/app.component.ts @@ -270,6 +270,7 @@ export class AppComponent implements OnDestroy, OnInit { locale: this.user?.settings?.locale }, height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', + maxWidth: this.deviceType === 'mobile' ? '95vw' : '50rem', width: this.deviceType === 'mobile' ? '100vw' : '50rem' }); diff --git a/apps/client/src/app/app.module.ts b/apps/client/src/app/app.module.ts index 9a311ac69..04602dd2e 100644 --- a/apps/client/src/app/app.module.ts +++ b/apps/client/src/app/app.module.ts @@ -33,6 +33,7 @@ import { GfSubscriptionInterstitialDialogModule } from './components/subscriptio import { authInterceptorProviders } from './core/auth.interceptor'; import { httpResponseInterceptorProviders } from './core/http-response.interceptor'; import { LanguageService } from './core/language.service'; +import { GfNotificationModule } from './core/notification/notification.module'; export function NgxStripeFactory(): string { return environment.stripePublicKey; @@ -47,6 +48,7 @@ export function NgxStripeFactory(): string { BrowserModule, GfHeaderModule, GfLogoComponent, + GfNotificationModule, GfSubscriptionInterstitialDialogModule, MarkdownModule.forRoot(), MatAutocompleteModule, diff --git a/apps/client/src/app/components/accounts-table/accounts-table.component.ts b/apps/client/src/app/components/accounts-table/accounts-table.component.ts index 702803aa0..d19cd748f 100644 --- a/apps/client/src/app/components/accounts-table/accounts-table.component.ts +++ b/apps/client/src/app/components/accounts-table/accounts-table.component.ts @@ -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 { @@ -54,7 +56,10 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit { private unsubscribeSubject = new Subject(); - public constructor(private router: Router) {} + public constructor( + private notificationService: NotificationService, + private router: Router + ) {} public ngOnInit() {} @@ -97,13 +102,13 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit { } public onDeleteAccount(aId: string) { - const confirmation = confirm( - $localize`Do you really want to delete this account?` - ); - - if (confirmation) { - this.accountDeleted.emit(aId); - } + this.notificationService.confirm({ + confirmFn: () => { + this.accountDeleted.emit(aId); + }, + confirmType: ConfirmationDialogType.Warn, + title: $localize`Do you really want to delete this account?` + }); } public onOpenAccountDetailDialog(accountId: string) { diff --git a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts index 3083184bb..7ca4677b0 100644 --- a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts +++ b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts @@ -1,3 +1,4 @@ +import { NotificationService } from '@ghostfolio/client/core/notification/notification.service'; import { getLocale, getNumberFormatDecimal, @@ -39,7 +40,7 @@ export class PortfolioPerformanceComponent implements OnChanges { @ViewChild('value') value: ElementRef; - public constructor() {} + public constructor(private notificationService: NotificationService) {} public ngOnChanges() { this.precision = this.precision >= 0 ? this.precision : 2; @@ -74,12 +75,15 @@ export class PortfolioPerformanceComponent implements OnChanges { } public onShowErrors() { - const errorMessageParts = [$localize`Market data is delayed for`]; + const errorMessageParts = []; for (const error of this.errors) { errorMessageParts.push(`${error.symbol} (${error.dataSource})`); } - alert(errorMessageParts.join('\n')); + this.notificationService.alert({ + message: errorMessageParts.join('
'), + title: $localize`Market data is delayed for` + }); } } diff --git a/apps/client/src/app/core/layout.service.ts b/apps/client/src/app/core/layout.service.ts index 3ba7af91e..a6fb65006 100644 --- a/apps/client/src/app/core/layout.service.ts +++ b/apps/client/src/app/core/layout.service.ts @@ -1,16 +1,39 @@ import { Injectable } from '@angular/core'; +import { DeviceDetectorService } from 'ngx-device-detector'; import { Observable, Subject } from 'rxjs'; +import { NotificationService } from './notification/notification.service'; + @Injectable({ providedIn: 'root' }) export class LayoutService { + public static readonly DEFAULT_NOTIFICATION_MAX_WIDTH = '50rem'; + public static readonly DEFAULT_NOTIFICATION_WIDTH = '75vw'; + public shouldReloadContent$: Observable; private shouldReloadSubject = new Subject(); - public constructor() { + public constructor( + private deviceService: DeviceDetectorService, + private notificationService: NotificationService + ) { 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() { diff --git a/apps/client/src/app/core/notification/alert-dialog/alert-dialog.component.ts b/apps/client/src/app/core/notification/alert-dialog/alert-dialog.component.ts new file mode 100644 index 000000000..65439ec42 --- /dev/null +++ b/apps/client/src/app/core/notification/alert-dialog/alert-dialog.component.ts @@ -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) {} + + public initialize(aParams: IAlertDialogParams) { + this.discardLabel = aParams.discardLabel; + this.message = aParams.message; + this.title = aParams.title; + } +} diff --git a/apps/client/src/app/core/notification/alert-dialog/alert-dialog.html b/apps/client/src/app/core/notification/alert-dialog/alert-dialog.html new file mode 100644 index 000000000..6602078d3 --- /dev/null +++ b/apps/client/src/app/core/notification/alert-dialog/alert-dialog.html @@ -0,0 +1,11 @@ +@if (title) { +
+} + +@if (message) { +
+} + +
+ +
diff --git a/apps/client/src/app/core/notification/alert-dialog/alert-dialog.scss b/apps/client/src/app/core/notification/alert-dialog/alert-dialog.scss new file mode 100644 index 000000000..dc9093b45 --- /dev/null +++ b/apps/client/src/app/core/notification/alert-dialog/alert-dialog.scss @@ -0,0 +1,2 @@ +:host { +} diff --git a/apps/client/src/app/core/notification/alert-dialog/interfaces/interfaces.ts b/apps/client/src/app/core/notification/alert-dialog/interfaces/interfaces.ts new file mode 100644 index 000000000..7cff077a7 --- /dev/null +++ b/apps/client/src/app/core/notification/alert-dialog/interfaces/interfaces.ts @@ -0,0 +1,6 @@ +export interface IAlertDialogParams { + confirmLabel?: string; + discardLabel?: string; + message?: string; + title: string; +} diff --git a/apps/client/src/app/core/notification/confirmation-dialog/confirmation-dialog.component.ts b/apps/client/src/app/core/notification/confirmation-dialog/confirmation-dialog.component.ts new file mode 100644 index 000000000..3545d39b7 --- /dev/null +++ b/apps/client/src/app/core/notification/confirmation-dialog/confirmation-dialog.component.ts @@ -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 + ) {} + + @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; + } +} diff --git a/apps/client/src/app/core/notification/confirmation-dialog/confirmation-dialog.html b/apps/client/src/app/core/notification/confirmation-dialog/confirmation-dialog.html new file mode 100644 index 000000000..e9e2b693c --- /dev/null +++ b/apps/client/src/app/core/notification/confirmation-dialog/confirmation-dialog.html @@ -0,0 +1,20 @@ +@if (title) { +
+} + +@if (message) { +
+} + +
+ + +
diff --git a/apps/client/src/app/core/notification/confirmation-dialog/confirmation-dialog.scss b/apps/client/src/app/core/notification/confirmation-dialog/confirmation-dialog.scss new file mode 100644 index 000000000..dc9093b45 --- /dev/null +++ b/apps/client/src/app/core/notification/confirmation-dialog/confirmation-dialog.scss @@ -0,0 +1,2 @@ +:host { +} diff --git a/apps/client/src/app/core/notification/confirmation-dialog/confirmation-dialog.type.ts b/apps/client/src/app/core/notification/confirmation-dialog/confirmation-dialog.type.ts new file mode 100644 index 000000000..1fe1fc7c9 --- /dev/null +++ b/apps/client/src/app/core/notification/confirmation-dialog/confirmation-dialog.type.ts @@ -0,0 +1,5 @@ +export enum ConfirmationDialogType { + Accent = 'accent', + Primary = 'primary', + Warn = 'warn' +} diff --git a/apps/client/src/app/core/notification/confirmation-dialog/interfaces/interfaces.ts b/apps/client/src/app/core/notification/confirmation-dialog/interfaces/interfaces.ts new file mode 100644 index 000000000..834988ceb --- /dev/null +++ b/apps/client/src/app/core/notification/confirmation-dialog/interfaces/interfaces.ts @@ -0,0 +1,9 @@ +import { ConfirmationDialogType } from '../confirmation-dialog.type'; + +export interface IConfirmDialogParams { + confirmLabel?: string; + confirmType: ConfirmationDialogType; + discardLabel?: string; + message?: string; + title: string; +} diff --git a/apps/client/src/app/core/notification/interfaces/interfaces.ts b/apps/client/src/app/core/notification/interfaces/interfaces.ts new file mode 100644 index 000000000..f5a526c92 --- /dev/null +++ b/apps/client/src/app/core/notification/interfaces/interfaces.ts @@ -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; +} diff --git a/apps/client/src/app/core/notification/notification.module.ts b/apps/client/src/app/core/notification/notification.module.ts new file mode 100644 index 000000000..542cae928 --- /dev/null +++ b/apps/client/src/app/core/notification/notification.module.ts @@ -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 {} diff --git a/apps/client/src/app/core/notification/notification.service.ts b/apps/client/src/app/core/notification/notification.service.ts new file mode 100644 index 000000000..2e7d9de6c --- /dev/null +++ b/apps/client/src/app/core/notification/notification.service.ts @@ -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; + } +} diff --git a/apps/client/src/styles.scss b/apps/client/src/styles.scss index 2b8644521..1f068da1b 100644 --- a/apps/client/src/styles.scss +++ b/apps/client/src/styles.scss @@ -359,10 +359,6 @@ ngx-skeleton-loader { .cdk-global-overlay-wrapper { justify-content: center !important; } - - .cdk-overlay-pane { - max-width: 95vw !important; - } } .cursor-default { diff --git a/libs/ui/src/lib/i18n.ts b/libs/ui/src/lib/i18n.ts index 2c1fd9273..a98cbd704 100644 --- a/libs/ui/src/lib/i18n.ts +++ b/libs/ui/src/lib/i18n.ts @@ -6,7 +6,9 @@ const locales = { ASSET_CLASS: $localize`Asset Class`, ASSET_SUB_CLASS: $localize`Asset Sub Class`, BUY_AND_SELL_ACTIVITIES_TOOLTIP: $localize`Buy and sell`, + CANCEL: $localize`Cancel`, 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_OSS: $localize`Switch to Ghostfolio Premium 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`, YEAR: $localize`Year`, YEARS: $localize`Years`, + YES: $localize`Yes`, // Activity types BUY: $localize`Buy`,