From 14cb0c98cec612a5627ab97dd985dde12c190320 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sun, 2 Feb 2025 14:47:34 +0100
Subject: [PATCH 1/5] Feature/update ghostfolio data provider info (#4269)
* Update info
---
.../ghostfolio-premium-api-dialog.html | 4 ++--
libs/ui/src/lib/i18n.ts | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
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/libs/ui/src/lib/i18n.ts b/libs/ui/src/lib/i18n.ts
index c0f95d457..7ea36123b 100644
--- a/libs/ui/src/lib/i18n.ts
+++ b/libs/ui/src/lib/i18n.ts
@@ -21,7 +21,7 @@ const locales = {
MONTH: $localize`Month`,
MONTHS: $localize`Months`,
OTHER: $localize`Other`,
- PROFESSIONAL_DATA_PROVIDER_TOOLTIP_PREMIUM: $localize`Get access to 100’000+ tickers from over 50 exchanges`,
+ PROFESSIONAL_DATA_PROVIDER_TOOLTIP_PREMIUM: $localize`Get access to 80’000+ tickers from over 50 exchanges`,
PRESET_ID: $localize`Preset`,
RETIREMENT_PROVISION: $localize`Retirement Provision`,
SATELLITE: $localize`Satellite`,
From 9ab21508a5c5bfea43fe0cbe3fa4688e7b35c1ba Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sun, 2 Feb 2025 14:48:07 +0100
Subject: [PATCH 2/5] Feature/refactor snack bars (#4273)
* Refactor snack bars
---
.../access-table/access-table.component.ts | 3 ++-
.../user-account-membership.component.ts | 15 +++++----------
.../user-account-settings.component.ts | 5 ++++-
.../src/app/core/http-response.interceptor.ts | 13 ++++++++++---
.../import-activities-dialog.component.ts | 7 +++++--
.../historical-market-data-editor.component.ts | 4 +++-
6 files changed, 29 insertions(+), 18 deletions(-)
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/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/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/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts
index caa0b15e6..7fbb1e621 100644
--- a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts
+++ b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts
@@ -261,7 +261,9 @@ export class GfHistoricalMarketDataEditorComponent
this.snackBar.open(
$localize`Oops! Could not parse historical data.`,
undefined,
- { duration: ms('3 seconds') }
+ {
+ duration: ms('3 seconds')
+ }
);
}
}
From 9905c428af4b0a2c0f4c93ea0b8a836a28765cdc Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sun, 2 Feb 2025 15:25:33 +0100
Subject: [PATCH 3/5] Feature/refactor regional market cluster risk rule for
North America (#4276)
* Refactoring
---
.../rules/regional-market-cluster-risk/north-america.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
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 a137f7abf..dc3f6d979 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
@@ -10,7 +10,7 @@ export class RegionalMarketClusterRiskNorthAmerica extends Rule {
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
currentValueInBaseCurrency: number,
- valueInBaseCurrency
+ northAmericaValueInBaseCurrency: number
) {
super(exchangeRateDataService, {
key: RegionalMarketClusterRiskNorthAmerica.name,
@@ -18,7 +18,7 @@ export class RegionalMarketClusterRiskNorthAmerica extends Rule {
});
this.currentValueInBaseCurrency = currentValueInBaseCurrency;
- this.northAmericaValueInBaseCurrency = valueInBaseCurrency;
+ this.northAmericaValueInBaseCurrency = northAmericaValueInBaseCurrency;
}
public evaluate(ruleSettings: Settings) {
From d711fed4f5f1738282746f1b4b467bcce51adbcb Mon Sep 17 00:00:00 2001
From: Ken Tandrian <60643640+KenTandrian@users.noreply.github.com>
Date: Sun, 2 Feb 2025 22:14:35 +0700
Subject: [PATCH 4/5] Feature/extract tags selector to reusable component
(#4256)
* feat(ui): create gf-tags-selector component
* feat(ui): implement gf-tags-selector in activity dialog
* feat(ui): implement gf-tags-selector in holding detail dialog
* Update changelog
---
CHANGELOG.md | 6 +
.../holding-detail-dialog.component.ts | 60 +--------
.../holding-detail-dialog.html | 37 +-----
...ate-or-update-activity-dialog.component.ts | 57 +-------
.../create-or-update-activity-dialog.html | 37 +-----
...create-or-update-activity-dialog.module.ts | 6 +-
libs/ui/src/lib/tags-selector/index.ts | 1 +
.../tags-selector.component.html | 32 +++++
.../tags-selector.component.scss | 3 +
.../tags-selector/tags-selector.component.ts | 123 ++++++++++++++++++
10 files changed, 191 insertions(+), 171 deletions(-)
create mode 100644 libs/ui/src/lib/tags-selector/index.ts
create mode 100644 libs/ui/src/lib/tags-selector/tags-selector.component.html
create mode 100644 libs/ui/src/lib/tags-selector/tags-selector.component.scss
create mode 100644 libs/ui/src/lib/tags-selector/tags-selector.component.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e8d6ea601..902fe4695 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## Unreleased
+
+### Added
+
+- Extracted the tags selector to a reusable component used in the create or update activity dialog and holding detail dialog
+
## 2.137.1 - 2025-02-01
### Added
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/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/libs/ui/src/lib/tags-selector/index.ts b/libs/ui/src/lib/tags-selector/index.ts
new file mode 100644
index 000000000..360bce671
--- /dev/null
+++ b/libs/ui/src/lib/tags-selector/index.ts
@@ -0,0 +1 @@
+export * from './tags-selector.component';
diff --git a/libs/ui/src/lib/tags-selector/tags-selector.component.html b/libs/ui/src/lib/tags-selector/tags-selector.component.html
new file mode 100644
index 000000000..55f8a39f2
--- /dev/null
+++ b/libs/ui/src/lib/tags-selector/tags-selector.component.html
@@ -0,0 +1,32 @@
+
+ Tags
+
+ @for (tag of tagsSelected(); track tag.id) {
+
+ {{ tag.name }}
+
+
+ }
+
+
+
+ @for (tag of filteredOptions | async; track tag.id) {
+
+ {{ tag.name }}
+
+ }
+
+
diff --git a/libs/ui/src/lib/tags-selector/tags-selector.component.scss b/libs/ui/src/lib/tags-selector/tags-selector.component.scss
new file mode 100644
index 000000000..5d4e87f30
--- /dev/null
+++ b/libs/ui/src/lib/tags-selector/tags-selector.component.scss
@@ -0,0 +1,3 @@
+:host {
+ display: block;
+}
diff --git a/libs/ui/src/lib/tags-selector/tags-selector.component.ts b/libs/ui/src/lib/tags-selector/tags-selector.component.ts
new file mode 100644
index 000000000..77c776ece
--- /dev/null
+++ b/libs/ui/src/lib/tags-selector/tags-selector.component.ts
@@ -0,0 +1,123 @@
+import { COMMA, ENTER } from '@angular/cdk/keycodes';
+import { CommonModule } from '@angular/common';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ CUSTOM_ELEMENTS_SCHEMA,
+ ElementRef,
+ EventEmitter,
+ Input,
+ OnChanges,
+ OnDestroy,
+ OnInit,
+ Output,
+ signal,
+ ViewChild
+} from '@angular/core';
+import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
+import {
+ MatAutocompleteModule,
+ MatAutocompleteSelectedEvent
+} from '@angular/material/autocomplete';
+import { MatChipsModule } from '@angular/material/chips';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { Tag } from '@prisma/client';
+import { BehaviorSubject, Subject, takeUntil } from 'rxjs';
+
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [
+ CommonModule,
+ FormsModule,
+ MatAutocompleteModule,
+ MatChipsModule,
+ MatFormFieldModule,
+ MatInputModule,
+ ReactiveFormsModule
+ ],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
+ selector: 'gf-tags-selector',
+ styleUrls: ['./tags-selector.component.scss'],
+ templateUrl: 'tags-selector.component.html'
+})
+export class GfTagsSelectorComponent implements OnInit, OnChanges, OnDestroy {
+ @Input() tags: Tag[];
+ @Input() tagsAvailable: Tag[];
+
+ @Output() tagsChanged = new EventEmitter();
+
+ @ViewChild('tagInput') tagInput: ElementRef;
+
+ public filteredOptions: Subject = new BehaviorSubject([]);
+ public readonly separatorKeysCodes: number[] = [COMMA, ENTER];
+ public readonly tagInputControl = new FormControl('');
+ public readonly tagsSelected = signal([]);
+
+ private unsubscribeSubject = new Subject();
+
+ public constructor() {
+ this.tagInputControl.valueChanges
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe((value) => {
+ this.filteredOptions.next(this.filterTags(value));
+ });
+ }
+
+ public ngOnInit() {
+ this.tagsSelected.set(this.tags);
+ this.updateFilters();
+ }
+
+ public ngOnChanges() {
+ this.tagsSelected.set(this.tags);
+ this.updateFilters();
+ }
+
+ public onAddTag(event: MatAutocompleteSelectedEvent) {
+ const tag = this.tagsAvailable.find(({ id }) => {
+ return id === event.option.value;
+ });
+
+ this.tagsSelected.update((tags) => {
+ return [...(tags ?? []), tag];
+ });
+
+ this.tagsChanged.emit(this.tagsSelected());
+ this.tagInput.nativeElement.value = '';
+ this.tagInputControl.setValue(undefined);
+ }
+
+ public onRemoveTag(tag: Tag) {
+ this.tagsSelected.update((tags) => {
+ return tags.filter(({ id }) => {
+ return id !== tag.id;
+ });
+ });
+
+ this.tagsChanged.emit(this.tagsSelected());
+ this.updateFilters();
+ }
+
+ public ngOnDestroy() {
+ this.unsubscribeSubject.next();
+ this.unsubscribeSubject.complete();
+ }
+
+ private filterTags(query: string = ''): Tag[] {
+ const tags = this.tagsSelected() ?? [];
+ const tagIds = tags.map(({ id }) => {
+ return id;
+ });
+
+ return this.tagsAvailable.filter(({ id, name }) => {
+ return (
+ !tagIds.includes(id) && name.toLowerCase().includes(query.toLowerCase())
+ );
+ });
+ }
+
+ private updateFilters() {
+ this.filteredOptions.next(this.filterTags());
+ }
+}
From e6416d5a00fc20191e1161eea3d61f7df291a56f Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sun, 2 Feb 2025 16:24:22 +0100
Subject: [PATCH 5/5] Feature/improve mode value labels in scraper
configuration (#4274)
* Improve mode value labels
---
.../asset-profile-dialog.component.ts | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
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: {