Feature/extend assistant by asset class selector (#2957)
* Remove tabs * Add asset class selector * Update changelog
This commit is contained in:
parent
c68d113d27
commit
06ba7a4b1b
@ -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)
|
||||||
|
@ -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[];
|
||||||
|
@ -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';
|
||||||
}
|
}
|
||||||
|
@ -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],
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user