Feature/extend assistant by asset class selector (#2957)

* Remove tabs

* Add asset class selector

* Update changelog
This commit is contained in:
Thomas Kaul 2024-02-04 15:50:58 +01:00 committed by GitHub
parent c68d113d27
commit 06ba7a4b1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 92 additions and 74 deletions

View File

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Extended the assistant by an asset class selector (experimental)
### Changed ### Changed
- Improved the usability of the account selector in the assistant (experimental) - Improved the usability of the account selector in the assistant (experimental)

View File

@ -42,6 +42,10 @@ export class UpdateUserSettingDto {
@IsOptional() @IsOptional()
'filters.accounts'?: string[]; 'filters.accounts'?: string[];
@IsArray()
@IsOptional()
'filters.assetClasses'?: string[];
@IsArray() @IsArray()
@IsOptional() @IsOptional()
'filters.tags'?: string[]; 'filters.tags'?: string[];

View File

@ -170,6 +170,8 @@ export class HeaderComponent implements OnChanges {
if (filter.type === 'ACCOUNT') { if (filter.type === 'ACCOUNT') {
filtersType = 'accounts'; filtersType = 'accounts';
} else if (filter.type === 'ASSET_CLASS') {
filtersType = 'assetClasses';
} else if (filter.type === 'TAG') { } else if (filter.type === 'TAG') {
filtersType = 'tags'; filtersType = 'tags';
} }

View File

@ -58,6 +58,13 @@ export class UserService extends ObservableStore<UserStoreState> {
}); });
} }
if (user.settings['filters.assetClasses']) {
filters.push({
id: user.settings['filters.assetClasses'][0],
type: 'ASSET_CLASS'
});
}
if (user.settings['filters.tags']) { if (user.settings['filters.tags']) {
filters.push({ filters.push({
id: user.settings['filters.tags'][0], id: user.settings['filters.tags'][0],

View File

@ -22,7 +22,7 @@ import { DataService } from '@ghostfolio/client/services/data.service';
import { Filter, User } from '@ghostfolio/common/interfaces'; import { Filter, User } from '@ghostfolio/common/interfaces';
import { DateRange } from '@ghostfolio/common/types'; import { DateRange } from '@ghostfolio/common/types';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
import { Account, Tag } from '@prisma/client'; import { Account, AssetClass, Tag } from '@prisma/client';
import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs'; import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs';
import { import {
catchError, catchError,
@ -92,6 +92,7 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
public static readonly SEARCH_RESULTS_DEFAULT_LIMIT = 5; public static readonly SEARCH_RESULTS_DEFAULT_LIMIT = 5;
public accounts: Account[] = []; public accounts: Account[] = [];
public assetClasses: Filter[] = [];
public dateRangeFormControl = new FormControl<string>(undefined); public dateRangeFormControl = new FormControl<string>(undefined);
public readonly dateRangeOptions = [ public readonly dateRangeOptions = [
{ label: $localize`Today`, value: '1d' }, { label: $localize`Today`, value: '1d' },
@ -116,6 +117,7 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
]; ];
public filterForm = this.formBuilder.group({ public filterForm = this.formBuilder.group({
account: new FormControl<string>(undefined), account: new FormControl<string>(undefined),
assetClass: new FormControl<string>(undefined),
tag: new FormControl<string>(undefined) tag: new FormControl<string>(undefined)
}); });
public isLoading = false; public isLoading = false;
@ -126,8 +128,9 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
assetProfiles: [], assetProfiles: [],
holdings: [] holdings: []
}; };
public tags: Tag[] = []; public tags: Filter[] = [];
private filterTypes: Filter['type'][] = ['ACCOUNT', 'ASSET_CLASS', 'TAG'];
private keyManager: FocusKeyManager<AssistantListItemComponent>; private keyManager: FocusKeyManager<AssistantListItemComponent>;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
@ -140,28 +143,38 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
public ngOnInit() { public ngOnInit() {
this.accounts = this.user?.accounts; this.accounts = this.user?.accounts;
this.assetClasses = Object.keys(AssetClass).map((assetClass) => {
return {
id: assetClass,
label: translate(assetClass),
type: 'ASSET_CLASS'
};
});
this.tags = this.user?.tags.map(({ id, name }) => { this.tags = this.user?.tags.map(({ id, name }) => {
return { return {
id, id,
name: translate(name) label: translate(name),
type: 'TAG'
}; };
}); });
this.filterForm.valueChanges this.filterForm.valueChanges
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ account, tag }) => { .subscribe(({ account, assetClass, tag }) => {
this.filtersChanged.emit([ this.filtersChanged.emit([
{ {
id: account, id: account,
type: 'ACCOUNT' type: 'ACCOUNT'
}, },
{
id: assetClass,
type: 'ASSET_CLASS'
},
{ {
id: tag, id: tag,
type: 'TAG' type: 'TAG'
} }
]); ]);
this.onCloseAssistant();
}); });
this.searchFormControl.valueChanges this.searchFormControl.valueChanges
@ -209,6 +222,7 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
this.filterForm.setValue( this.filterForm.setValue(
{ {
account: this.user?.settings?.['filters.accounts']?.[0] ?? null, account: this.user?.settings?.['filters.accounts']?.[0] ?? null,
assetClass: this.user?.settings?.['filters.assetClasses']?.[0] ?? null,
tag: this.user?.settings?.['filters.tags']?.[0] ?? null tag: this.user?.settings?.['filters.tags']?.[0] ?? null
}, },
{ {
@ -257,16 +271,14 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
} }
public onResetFilters() { public onResetFilters() {
this.filtersChanged.emit([ this.filtersChanged.emit(
{ this.filterTypes.map((type) => {
id: null, return {
type: 'ACCOUNT' type,
}, id: null
{ };
id: null, })
type: 'TAG' );
}
]);
this.onCloseAssistant(); this.onCloseAssistant();
} }

View File

@ -89,9 +89,9 @@
<form [formGroup]="filterForm"> <form [formGroup]="filterForm">
<div <div
*ngIf="!(isLoading || searchFormControl.value) && user?.settings?.isExperimentalFeatures" *ngIf="!(isLoading || searchFormControl.value) && user?.settings?.isExperimentalFeatures"
class="filter-container" class="filter-container p-3"
> >
<div class="p-3"> <div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint"> <mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Date Range</mat-label> <mat-label i18n>Date Range</mat-label>
<mat-select <mat-select
@ -104,60 +104,51 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<mat-tab-group <div class="mb-3">
animationDuration="0" <mat-form-field appearance="outline" class="w-100 without-hint">
mat-align-tabs="start" <mat-label i18n>Accounts</mat-label>
[mat-stretch-tabs]="false" <mat-select formControlName="account">
> <mat-option [value]="null"></mat-option>
<mat-tab> @for (account of accounts; track account.id) {
<ng-template mat-tab-label <mat-option [value]="account.id">
><ion-icon name="albums-outline" /><span <div class="d-flex">
class="d-none d-sm-block ml-2" <gf-symbol-icon
i18n *ngIf="account.Platform?.url"
>Accounts</span class="mr-1"
></ng-template [tooltip]="account.Platform?.name"
> [url]="account.Platform?.url"
<div class="p-3"> /><span>{{ account.name }}</span>
<mat-form-field appearance="outline" class="w-100 without-hint"> </div>
<mat-select formControlName="account"> </mat-option>
<mat-option [value]="null"></mat-option> }
@for (account of accounts; track account.id) { </mat-select>
<mat-option [value]="account.id"> </mat-form-field>
<div class="d-flex"> </div>
<gf-symbol-icon <div class="mb-3">
*ngIf="account.Platform?.url" <mat-form-field appearance="outline" class="w-100 without-hint">
class="mr-1" <mat-label i18n>Tags</mat-label>
[tooltip]="account.Platform?.name" <mat-select formControlName="tag">
[url]="account.Platform?.url" <mat-option [value]="null"></mat-option>
/><span>{{ account.name }}</span> @for (tag of tags; track tag.id) {
</div> <mat-option [value]="tag.id">{{ tag.label }}</mat-option>
</mat-option> }
} </mat-select>
</mat-select> </mat-form-field>
</mat-form-field> </div>
</div> <div class="mb-3">
</mat-tab> <mat-form-field appearance="outline" class="w-100 without-hint">
<mat-tab> <mat-label i18n>Asset Classes</mat-label>
<ng-template mat-tab-label <mat-select formControlName="assetClass">
><ion-icon name="pricetag-outline" /><span <mat-option [value]="null"></mat-option>
class="d-none d-sm-block ml-2" @for (assetClass of assetClasses; track assetClass.id) {
i18n <mat-option [value]="assetClass.id"
>Tags</span >{{ assetClass.label }}</mat-option
></ng-template >
> }
<div class="p-3"> </mat-select>
<mat-form-field appearance="outline" class="w-100 without-hint"> </mat-form-field>
<mat-select formControlName="tag"> </div>
<mat-option [value]="null"></mat-option> <div>
@for (tag of tags; track tag.id) {
<mat-option [value]="tag.id">{{ tag.name }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</mat-tab>
</mat-tab-group>
<div class="pb-3 px-3">
<button <button
class="w-100" class="w-100"
color="primary" color="primary"

View File

@ -4,7 +4,6 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module'; import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -23,7 +22,6 @@ import { AssistantComponent } from './assistant.component';
MatButtonModule, MatButtonModule,
MatFormFieldModule, MatFormFieldModule,
MatSelectModule, MatSelectModule,
MatTabsModule,
NgxSkeletonLoaderModule, NgxSkeletonLoaderModule,
ReactiveFormsModule, ReactiveFormsModule,
RouterModule RouterModule