diff --git a/CHANGELOG.md b/CHANGELOG.md
index 02acf84e1..eb4468a9d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added a new static portfolio analysis rule: _Regional Market Cluster Risk_ (Europe)
+- Added a link to _Duck.ai_ to the _Copy AI prompt to clipboard_ action on the analysis page (experimental)
+- Extracted the tags selector to a reusable component used in the create or update activity dialog and holding detail dialog
+
+### Changed
+
+- Improved the caching of the portfolio snapshot in the portfolio calculator by expiring cache entries when a user changes tags in the holding detail dialog
+- Improved the language localization for German (`de`)
## 2.137.1 - 2025-02-01
diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts
index e80811351..a26099e9d 100644
--- a/apps/api/src/app/order/order.service.ts
+++ b/apps/api/src/app/order/order.service.ts
@@ -63,14 +63,14 @@ export class OrderService {
}
});
- return Promise.all(
+ await Promise.all(
orders.map(({ id }) =>
this.prismaService.order.update({
data: {
tags: {
// The set operation replaces all existing connections with the provided ones
- set: tags.map(({ id }) => {
- return { id };
+ set: tags.map((tag) => {
+ return { id: tag.id };
})
}
},
@@ -78,6 +78,13 @@ export class OrderService {
})
)
);
+
+ this.eventEmitter.emit(
+ PortfolioChangedEvent.getName(),
+ new PortfolioChangedEvent({
+ userId
+ })
+ );
}
public async createOrder(
diff --git a/apps/api/src/models/rules/regional-market-cluster-risk/north-america.ts b/apps/api/src/models/rules/regional-market-cluster-risk/north-america.ts
index 86e9effed..4563b7c54 100644
--- a/apps/api/src/models/rules/regional-market-cluster-risk/north-america.ts
+++ b/apps/api/src/models/rules/regional-market-cluster-risk/north-america.ts
@@ -11,7 +11,7 @@ export class RegionalMarketClusterRiskNorthAmerica extends Rule {
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
currentValueInBaseCurrency: number,
- valueInBaseCurrency
+ northAmericaValueInBaseCurrency: number
) {
super(exchangeRateDataService, {
key: RegionalMarketClusterRiskNorthAmerica.name,
@@ -19,7 +19,7 @@ export class RegionalMarketClusterRiskNorthAmerica extends Rule {
});
this.currentValueInBaseCurrency = currentValueInBaseCurrency;
- this.northAmericaValueInBaseCurrency = valueInBaseCurrency;
+ this.northAmericaValueInBaseCurrency = northAmericaValueInBaseCurrency;
}
public evaluate(ruleSettings: Settings) {
diff --git a/apps/client/src/app/components/access-table/access-table.component.ts b/apps/client/src/app/components/access-table/access-table.component.ts
index 32ae7bfef..34c5fbda2 100644
--- a/apps/client/src/app/components/access-table/access-table.component.ts
+++ b/apps/client/src/app/components/access-table/access-table.component.ts
@@ -14,6 +14,7 @@ import {
} from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableDataSource } from '@angular/material/table';
+import ms from 'ms';
@Component({
selector: 'gf-access-table',
@@ -64,7 +65,7 @@ export class AccessTableComponent implements OnChanges {
'✅ ' + $localize`Link has been copied to the clipboard`,
undefined,
{
- duration: 3000
+ duration: ms('3 seconds')
}
);
}
diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
index bb19ad96c..1467a1ba3 100644
--- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
+++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
@@ -88,8 +88,14 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
public isBenchmark = false;
public marketDataItems: MarketData[] = [];
public modeValues = [
- { value: 'lazy', viewValue: $localize`Lazy` },
- { value: 'instant', viewValue: $localize`Instant` }
+ {
+ value: 'lazy',
+ viewValue: $localize`Lazy` + ' (' + $localize`end of day` + ')'
+ },
+ {
+ value: 'instant',
+ viewValue: $localize`Instant` + ' (' + $localize`real-time` + ')'
+ }
];
public scraperConfiguationIsExpanded = signal(false);
public sectors: {
diff --git a/apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html b/apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html
index ac777ffda..d3b0985fa 100644
--- a/apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html
+++ b/apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html
@@ -17,8 +17,8 @@
data provider for self-hosters, offering
- 100’000+ tickers from over 50 exchanges,
- is coming soon!
+ 80’000+ tickers from over 50 exchanges, is
+ coming soon!
Want to stay updated? Click below to get notified as soon as it’s available.
diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
index d13158898..297a990ec 100644
--- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
+++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
@@ -18,26 +18,20 @@ import { GfDataProviderCreditsComponent } from '@ghostfolio/ui/data-provider-cre
import { translate } from '@ghostfolio/ui/i18n';
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart';
+import { GfTagsSelectorComponent } from '@ghostfolio/ui/tags-selector';
import { GfValueComponent } from '@ghostfolio/ui/value';
-import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { CommonModule } from '@angular/common';
import {
CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
- ElementRef,
Inject,
OnDestroy,
- OnInit,
- ViewChild
+ OnInit
} from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
-import {
- MatAutocompleteModule,
- MatAutocompleteSelectedEvent
-} from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips';
import {
@@ -53,8 +47,8 @@ import { Router } from '@angular/router';
import { Account, Tag } from '@prisma/client';
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
-import { Observable, of, Subject } from 'rxjs';
-import { map, startWith, takeUntil } from 'rxjs/operators';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
import { HoldingDetailDialogParams } from './interfaces/interfaces';
@@ -70,8 +64,8 @@ import { HoldingDetailDialogParams } from './interfaces/interfaces';
GfDialogHeaderModule,
GfLineChartComponent,
GfPortfolioProportionChartComponent,
+ GfTagsSelectorComponent,
GfValueComponent,
- MatAutocompleteModule,
MatButtonModule,
MatChipsModule,
MatDialogModule,
@@ -85,8 +79,6 @@ import { HoldingDetailDialogParams } from './interfaces/interfaces';
templateUrl: 'holding-detail-dialog.html'
})
export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
- @ViewChild('tagInput') tagInput: ElementRef;
-
public activityForm: FormGroup;
public accounts: Account[];
public assetClass: string;
@@ -102,7 +94,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
public dividendInBaseCurrencyPrecision = 2;
public dividendYieldPercentWithCurrencyEffect: number;
public feeInBaseCurrency: number;
- public filteredTagsObservable: Observable = of([]);
public firstBuyDate: string;
public historicalDataItems: LineChartItem[];
public investment: number;
@@ -122,7 +113,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
public sectors: {
[name: string]: { name: string; value: number };
};
- public separatorKeysCodes: number[] = [COMMA, ENTER];
public sortColumn = 'date';
public sortDirection: SortDirection = 'desc';
public SymbolProfile: EnhancedSymbolProfile;
@@ -319,17 +309,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
this.activityForm.setValue({ tags: this.tags }, { emitEvent: false });
- this.filteredTagsObservable = this.activityForm.controls[
- 'tags'
- ].valueChanges.pipe(
- startWith(this.activityForm.get('tags').value),
- map((aTags: Tag[] | null) => {
- return aTags
- ? this.filterTags(aTags)
- : this.tagsAvailable.slice();
- })
- );
-
this.transactionCount = transactionCount;
this.totalItems = transactionCount;
this.value = value;
@@ -437,17 +416,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
});
}
- public onAddTag(event: MatAutocompleteSelectedEvent) {
- this.activityForm.get('tags').setValue([
- ...(this.activityForm.get('tags').value ?? []),
- this.tagsAvailable.find(({ id }) => {
- return id === event.option.value;
- })
- ]);
-
- this.tagInput.nativeElement.value = '';
- }
-
public onCloneActivity(aActivity: Activity) {
this.router.navigate(['/portfolio', 'activities'], {
queryParams: { activityId: aActivity.id, createDialog: true }
@@ -480,12 +448,8 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
});
}
- public onRemoveTag(aTag: Tag) {
- this.activityForm.get('tags').setValue(
- this.activityForm.get('tags').value.filter(({ id }) => {
- return id !== aTag.id;
- })
- );
+ public onTagsChanged(tags: Tag[]) {
+ this.activityForm.get('tags').setValue(tags);
}
public onUpdateActivity(aActivity: Activity) {
@@ -500,14 +464,4 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
-
- private filterTags(aTags: Tag[]) {
- const tagIds = aTags.map(({ id }) => {
- return id;
- });
-
- return this.tagsAvailable.filter(({ id }) => {
- return !tagIds.includes(id);
- });
- }
}
diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
index f92ad54f8..a20c9af7a 100644
--- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
+++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
@@ -373,38 +373,11 @@
}"
>
-
- Tags
-
- @for (tag of activityForm.get('tags')?.value; track tag.id) {
-
- {{ tag.name }}
-
-
- }
-
-
-
- @for (tag of filteredTagsObservable | async; track tag.id) {
-
- {{ tag.name }}
-
- }
-
-
+
diff --git a/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts b/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
index 6139d173e..2214d91f9 100644
--- a/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
+++ b/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
@@ -12,11 +12,7 @@ import {
Component,
OnDestroy
} from '@angular/core';
-import {
- MatSnackBar,
- MatSnackBarRef,
- TextOnlySnackBar
-} from '@angular/material/snack-bar';
+import { MatSnackBar } from '@angular/material/snack-bar';
import ms, { StringValue } from 'ms';
import { StripeService } from 'ngx-stripe';
import { EMPTY, Subject } from 'rxjs';
@@ -41,7 +37,6 @@ export class UserAccountMembershipComponent implements OnDestroy {
public price: number;
public priceId: string;
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`];
- public snackBarRef: MatSnackBarRef;
public trySubscriptionMail =
'mailto:hi@ghostfol.io?Subject=Ghostfolio Premium Trial&body=Hello%0D%0DI am interested in Ghostfolio Premium. Can you please send me a coupon code to try it for some time?%0D%0DKind regards';
public user: User;
@@ -186,22 +181,22 @@ export class UserAccountMembershipComponent implements OnDestroy {
takeUntil(this.unsubscribeSubject)
)
.subscribe(() => {
- this.snackBarRef = this.snackBar.open(
+ const snackBarRef = this.snackBar.open(
'✅ ' + $localize`Coupon code has been redeemed`,
$localize`Reload`,
{
- duration: 3000
+ duration: ms('3 seconds')
}
);
- this.snackBarRef
+ snackBarRef
.afterDismissed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
window.location.reload();
});
- this.snackBarRef
+ snackBarRef
.onAction()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
diff --git a/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts b/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
index c1472515f..ced617117 100644
--- a/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
+++ b/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
@@ -25,6 +25,7 @@ import { MatSlideToggleChange } from '@angular/material/slide-toggle';
import { MatSnackBar } from '@angular/material/snack-bar';
import { format, parseISO } from 'date-fns';
import { uniq } from 'lodash';
+import ms from 'ms';
import { EMPTY, Subject, throwError } from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators';
@@ -301,7 +302,9 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit {
this.snackBar.open(
$localize`Oops! There was an error setting up biometric authentication.`,
undefined,
- { duration: 3000 }
+ {
+ duration: ms('3 seconds')
+ }
);
return throwError(() => {
diff --git a/apps/client/src/app/core/http-response.interceptor.ts b/apps/client/src/app/core/http-response.interceptor.ts
index 203d3adf5..018e441fc 100644
--- a/apps/client/src/app/core/http-response.interceptor.ts
+++ b/apps/client/src/app/core/http-response.interceptor.ts
@@ -19,6 +19,7 @@ import {
} from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { StatusCodes } from 'http-status-codes';
+import ms from 'ms';
import { Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
@@ -54,13 +55,17 @@ export class HttpResponseInterceptor implements HttpInterceptor {
' ' +
$localize`Please try again later.`,
undefined,
- { duration: 6000 }
+ {
+ duration: ms('6 seconds')
+ }
);
} else if (!error.url.includes('/auth')) {
this.snackBarRef = this.snackBar.open(
$localize`This action is not allowed.`,
undefined,
- { duration: 6000 }
+ {
+ duration: ms('6 seconds')
+ }
);
}
@@ -79,7 +84,9 @@ export class HttpResponseInterceptor implements HttpInterceptor {
' ' +
$localize`Please try again later.`,
$localize`Okay`,
- { duration: 6000 }
+ {
+ duration: ms('6 seconds')
+ }
);
this.snackBarRef.afterDismissed().subscribe(() => {
diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
index 271a5cd53..555fbc7aa 100644
--- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
+++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
@@ -3,24 +3,20 @@ import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { getDateFormatString } from '@ghostfolio/common/helper';
import { translate } from '@ghostfolio/ui/i18n';
-import { COMMA, ENTER } from '@angular/cdk/keycodes';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
- ElementRef,
Inject,
- OnDestroy,
- ViewChild
+ OnDestroy
} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
-import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AssetClass, AssetSubClass, Tag, Type } from '@prisma/client';
import { isAfter, isToday } from 'date-fns';
-import { EMPTY, Observable, Subject, lastValueFrom, of } from 'rxjs';
-import { catchError, delay, map, startWith, takeUntil } from 'rxjs/operators';
+import { EMPTY, Subject, lastValueFrom } from 'rxjs';
+import { catchError, delay, takeUntil } from 'rxjs/operators';
import { DataService } from '../../../../services/data.service';
import { validateObjectForForm } from '../../../../util/form.util';
@@ -35,9 +31,6 @@ import { CreateOrUpdateActivityDialogParams } from './interfaces/interfaces';
standalone: false
})
export class CreateOrUpdateActivityDialog implements OnDestroy {
- @ViewChild('symbolAutocomplete') symbolAutocomplete;
- @ViewChild('tagInput') tagInput: ElementRef;
-
public activityForm: FormGroup;
public assetClasses = Object.keys(AssetClass).map((assetClass) => {
return { id: assetClass, label: translate(assetClass) };
@@ -48,12 +41,10 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
public currencies: string[] = [];
public currentMarketPrice = null;
public defaultDateFormat: string;
- public filteredTagsObservable: Observable = of([]);
public isLoading = false;
public isToday = isToday;
public mode: 'create' | 'update';
public platforms: { id: string; name: string }[];
- public separatorKeysCodes: number[] = [COMMA, ENTER];
public tagsAvailable: Tag[] = [];
public total = 0;
public typesTranslationMap = new Map();
@@ -284,15 +275,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.changeDetectorRef.markForCheck();
});
- this.filteredTagsObservable = this.activityForm.controls[
- 'tags'
- ].valueChanges.pipe(
- startWith(this.activityForm.get('tags').value),
- map((aTags: Tag[] | null) => {
- return aTags ? this.filterTags(aTags) : this.tagsAvailable.slice();
- })
- );
-
this.activityForm
.get('type')
.valueChanges.pipe(takeUntil(this.unsubscribeSubject))
@@ -440,29 +422,10 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
return isAfter(aDate, new Date(0));
}
- public onAddTag(event: MatAutocompleteSelectedEvent) {
- this.activityForm.get('tags').setValue([
- ...(this.activityForm.get('tags').value ?? []),
- this.tagsAvailable.find(({ id }) => {
- return id === event.option.value;
- })
- ]);
-
- this.tagInput.nativeElement.value = '';
- }
-
public onCancel() {
this.dialogRef.close();
}
- public onRemoveTag(aTag: Tag) {
- this.activityForm.get('tags').setValue(
- this.activityForm.get('tags').value.filter(({ id }) => {
- return id !== aTag.id;
- })
- );
- }
-
public async onSubmit() {
const activity: CreateOrderDto | UpdateOrderDto = {
accountId: this.activityForm.get('accountId').value,
@@ -518,21 +481,15 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
}
}
+ public onTagsChanged(tags: Tag[]) {
+ this.activityForm.get('tags').setValue(tags);
+ }
+
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
- private filterTags(aTags: Tag[]) {
- const tagIds = aTags.map(({ id }) => {
- return id;
- });
-
- return this.tagsAvailable.filter(({ id }) => {
- return !tagIds.includes(id);
- });
- }
-
private updateSymbol() {
this.isLoading = true;
this.changeDetectorRef.markForCheck();
diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
index 7795688c0..85fcf5a94 100644
--- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
+++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
@@ -379,38 +379,11 @@
-
- Tags
-
- @for (tag of activityForm.get('tags')?.value; track tag.id) {
-
- {{ tag.name }}
-
-
- }
-
-
-
- @for (tag of filteredTagsObservable | async; track tag.id) {
-
- {{ tag.name }}
-
- }
-
-
+
diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.module.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.module.ts
index a4d28d0e0..8fb2c1bed 100644
--- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.module.ts
+++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.module.ts
@@ -1,14 +1,13 @@
import { GfAssetProfileIconComponent } from '@ghostfolio/client/components/asset-profile-icon/asset-profile-icon.component';
import { GfSymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete';
+import { GfTagsSelectorComponent } from '@ghostfolio/ui/tags-selector';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
-import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
-import { MatChipsModule } from '@angular/material/chips';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
@@ -24,11 +23,10 @@ import { CreateOrUpdateActivityDialog } from './create-or-update-activity-dialog
FormsModule,
GfAssetProfileIconComponent,
GfSymbolAutocompleteComponent,
+ GfTagsSelectorComponent,
GfValueComponent,
- MatAutocompleteModule,
MatButtonModule,
MatCheckboxModule,
- MatChipsModule,
MatDatepickerModule,
MatDialogModule,
MatFormFieldModule,
diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
index 2f5ead47a..82e78a180 100644
--- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
+++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
@@ -23,6 +23,7 @@ import { MatStepper } from '@angular/material/stepper';
import { MatTableDataSource } from '@angular/material/table';
import { AssetClass } from '@prisma/client';
import { isArray, sortBy } from 'lodash';
+import ms from 'ms';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject, takeUntil } from 'rxjs';
@@ -133,7 +134,7 @@ export class ImportActivitiesDialog implements OnDestroy {
'✅ ' + $localize`Import has been completed`,
undefined,
{
- duration: 3000
+ duration: ms('3 seconds')
}
);
} catch (error) {
@@ -142,7 +143,9 @@ export class ImportActivitiesDialog implements OnDestroy {
' ' +
$localize`Please try again later.`,
$localize`Okay`,
- { duration: 3000 }
+ {
+ duration: ms('3 seconds')
+ }
);
} finally {
this.dialogRef.close();
diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
index 7e27a05f9..a9a189d1f 100644
--- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
+++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
@@ -20,6 +20,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { SymbolProfile } from '@prisma/client';
import { isNumber, sortBy } from 'lodash';
+import ms from 'ms';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@@ -142,17 +143,27 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
}
public onCopyPromptToClipboard() {
- this.dataService.fetchPrompt().subscribe(({ prompt }) => {
- this.clipboard.copy(prompt);
-
- this.snackBar.open(
- '✅ ' + $localize`AI prompt has been copied to the clipboard`,
- undefined,
- {
- duration: 3000
- }
- );
- });
+ this.dataService
+ .fetchPrompt()
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe(({ prompt }) => {
+ this.clipboard.copy(prompt);
+
+ const snackBarRef = this.snackBar.open(
+ '✅ ' + $localize`AI prompt has been copied to the clipboard`,
+ $localize`Open Duck.ai` + ' →',
+ {
+ duration: ms('7 seconds')
+ }
+ );
+
+ snackBarRef
+ .onAction()
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe(() => {
+ window.open('https://duck.ai', '_blank');
+ });
+ });
}
public ngOnDestroy() {
diff --git a/apps/client/src/locales/messages.ca.xlf b/apps/client/src/locales/messages.ca.xlf
index fd822d831..18c8288c3 100644
--- a/apps/client/src/locales/messages.ca.xlf
+++ b/apps/client/src/locales/messages.ca.xlf
@@ -653,11 +653,11 @@
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
- 43
+ 39
apps/client/src/app/core/http-response.interceptor.ts
- 72
+ 77
apps/client/src/app/core/paths.ts
@@ -883,7 +883,7 @@
Realment vol revocar aquest accés?
apps/client/src/app/components/access-table/access-table.component.ts
- 78
+ 79
@@ -1483,7 +1483,7 @@
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
- 427
+ 400
apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html
@@ -1527,7 +1527,7 @@
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
- 434
+ 407
libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html
@@ -1755,7 +1755,7 @@
El preu de mercat actual és
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
- 393
+ 399
@@ -2219,15 +2219,11 @@
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
- 377
+ 387
- apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
- 414
-
-
- apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
- 383
+ libs/ui/src/lib/tags-selector/tags-selector.component.html
+ 2
@@ -2519,7 +2515,7 @@
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
- 159
+ 160
@@ -2643,7 +2639,7 @@
Informar d’un Problema amb les Dades
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
- 433
+ 406
@@ -3323,7 +3319,7 @@
Please enter your coupon code.
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
- 213
+ 208
@@ -3331,7 +3327,7 @@
Could not redeem coupon code
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
- 177
+ 172
@@ -3339,7 +3335,7 @@
Coupon code has been redeemed
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
- 190
+ 185
@@ -3347,7 +3343,7 @@
Reload
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
- 191
+ 186
@@ -3383,7 +3379,7 @@
Auto
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
- 39
+ 40
@@ -3391,7 +3387,7 @@
Do you really want to close your Ghostfolio account?
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
- 174
+ 175
@@ -3399,7 +3395,7 @@
Do you really want to remove this sign in method?
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
- 248
+ 249
@@ -3407,7 +3403,7 @@
Oops! There was an error setting up biometric authentication.
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
- 302
+ 303
@@ -3591,7 +3587,7 @@
This feature is currently unavailable.
apps/client/src/app/core/http-response.interceptor.ts
- 53
+ 54
@@ -3599,15 +3595,15 @@
Please try again later.
apps/client/src/app/core/http-response.interceptor.ts
- 55
+ 56
apps/client/src/app/core/http-response.interceptor.ts
- 80
+ 85
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
- 143
+ 144
@@ -3615,7 +3611,7 @@
This action is not allowed.
apps/client/src/app/core/http-response.interceptor.ts
- 61
+ 64
@@ -3623,11 +3619,11 @@
Oops! Something went wrong.
apps/client/src/app/core/http-response.interceptor.ts
- 78
+ 83
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
- 141
+ 142
@@ -3635,15 +3631,15 @@
Okay
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
- 152
+ 147
apps/client/src/app/core/http-response.interceptor.ts
- 81
+ 86
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
- 144
+ 145
@@ -3651,7 +3647,7 @@
Oops! It looks like you’re making too many requests. Please slow down a bit.
apps/client/src/app/core/http-response.interceptor.ts
- 96
+ 103
@@ -4755,7 +4751,7 @@
Import Activities
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
- 46
+ 47
@@ -4763,7 +4759,7 @@
Import Dividends
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
- 87
+ 88
@@ -4771,7 +4767,7 @@
Importing data...
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
- 125
+ 126
@@ -4779,7 +4775,7 @@
Import has been completed
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
- 133
+ 134
@@ -4787,7 +4783,7 @@
Validating data...
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
- 239
+ 242
@@ -7035,7 +7031,7 @@
Error
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
- 384
+ 390
@@ -7103,16 +7099,16 @@
- Change with currency effect Change
- Change with currency effect Change
+ Change with currency effect Change
+ Change with currency effect Change
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
50
- Performance with currency effect Performance
- Performance with currency effect Performance
+ Performance with currency effect Performance
+ Performance with currency effect Performance
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
69
@@ -7358,9 +7354,9 @@
23
-
- Get access to 100’000+ tickers from over 50 exchanges
- Get access to 100’000+ tickers from over 50 exchanges
+
+ Get access to 80’000+ tickers from over 50 exchanges
+ Get access to 80’000+ tickers from over 50 exchanges
libs/ui/src/lib/i18n.ts
24
@@ -7533,7 +7529,7 @@
Could not generate an API key
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
- 139
+ 134
@@ -7541,7 +7537,7 @@
Set this API key in your self-hosted environment:
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
- 154
+ 149
@@ -7549,7 +7545,7 @@
Ghostfolio Premium Data Provider API Key
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
- 157
+ 152
@@ -7557,7 +7553,7 @@
Do you really want to generate a new API key?
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
- 162
+ 157
@@ -7661,7 +7657,7 @@
Link has been copied to the clipboard
apps/client/src/app/components/access-table/access-table.component.ts
- 64
+ 65
@@ -7685,7 +7681,7 @@
Lazy
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
- 91
+ 93
@@ -7693,7 +7689,7 @@
Instant
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
- 92
+ 97
@@ -7728,6 +7724,22 @@
310
+
+ end of day
+ end of day
+
+ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
+ 93
+
+
+
+ real-time
+ real-time
+
+ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
+ 97
+
+