Feature/set up notification service (#3663)
* Set up notification service * Update changelog
This commit is contained in:
parent
716f979502
commit
706784f7a0
@ -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: () => {
|
||||||
);
|
this.accountDeleted.emit(aId);
|
||||||
|
},
|
||||||
if (confirmation) {
|
confirmType: ConfirmationDialogType.Warn,
|
||||||
this.accountDeleted.emit(aId);
|
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,2 @@
|
|||||||
|
:host {
|
||||||
|
}
|
@ -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,2 @@
|
|||||||
|
:host {
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
18
apps/client/src/app/core/notification/notification.module.ts
Normal file
18
apps/client/src/app/core/notification/notification.module.ts
Normal file
@ -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…
x
Reference in New Issue
Block a user