Browse Source

Implement client side of Feature/Extend assistant by selector for holdings #3941

pull/4031/head
Amandee Ellawala 10 months ago
committed by Thomas Kaul
parent
commit
73de220576
  1. 8
      apps/api/src/app/user/update-user-setting.dto.ts
  2. 4
      apps/client/src/app/components/header/header.component.ts
  3. 14
      apps/client/src/app/services/user/user.service.ts
  4. 2
      libs/common/src/lib/interfaces/user-settings.interface.ts
  5. 86
      libs/ui/src/lib/assistant/assistant.component.ts
  6. 26
      libs/ui/src/lib/assistant/assistant.html

8
apps/api/src/app/user/update-user-setting.dto.ts

@ -68,6 +68,14 @@ export class UpdateUserSettingDto {
@IsOptional() @IsOptional()
'filters.tags'?: string[]; 'filters.tags'?: string[];
@IsArray()
@IsOptional()
'filters.dataSource'?: string[];
@IsArray()
@IsOptional()
'filters.symbol'?: string[];
@IsIn(['CHART', 'TABLE'] as HoldingsViewMode[]) @IsIn(['CHART', 'TABLE'] as HoldingsViewMode[])
@IsOptional() @IsOptional()
holdingsViewMode?: HoldingsViewMode; holdingsViewMode?: HoldingsViewMode;

4
apps/client/src/app/components/header/header.component.ts

@ -183,6 +183,10 @@ export class HeaderComponent implements OnChanges {
filtersType = 'assetClasses'; filtersType = 'assetClasses';
} else if (filter.type === 'TAG') { } else if (filter.type === 'TAG') {
filtersType = 'tags'; filtersType = 'tags';
} else if (filter.type === 'DATA_SOURCE') {
filtersType = 'dataSource';
} else if (filter.type === 'SYMBOL') {
filtersType = 'symbol';
} }
userSetting[`filters.${filtersType}`] = filter.id ? [filter.id] : null; userSetting[`filters.${filtersType}`] = filter.id ? [filter.id] : null;

14
apps/client/src/app/services/user/user.service.ts

@ -72,6 +72,20 @@ export class UserService extends ObservableStore<UserStoreState> {
}); });
} }
if (user?.settings['filters.dataSource']) {
filters.push({
id: user.settings['filters.dataSource'][0],
type: 'DATA_SOURCE'
});
}
if (user?.settings['filters.symbol']) {
filters.push({
id: user.settings['filters.symbol'][0],
type: 'SYMBOL'
});
}
return filters; return filters;
} }

2
libs/common/src/lib/interfaces/user-settings.interface.ts

@ -15,6 +15,8 @@ export interface UserSettings {
emergencyFund?: number; emergencyFund?: number;
'filters.accounts'?: string[]; 'filters.accounts'?: string[];
'filters.tags'?: string[]; 'filters.tags'?: string[];
'filters.dataSource'?: string[];
'filters.symbol'?: string[];
holdingsViewMode?: HoldingsViewMode; holdingsViewMode?: HoldingsViewMode;
isExperimentalFeatures?: boolean; isExperimentalFeatures?: boolean;
isRestrictedView?: boolean; isRestrictedView?: boolean;

86
libs/ui/src/lib/assistant/assistant.component.ts

@ -1,7 +1,8 @@
import { GfAssetProfileIconComponent } from '@ghostfolio/client/components/asset-profile-icon/asset-profile-icon.component'; import { GfAssetProfileIconComponent } from '@ghostfolio/client/components/asset-profile-icon/asset-profile-icon.component';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { AdminService } from '@ghostfolio/client/services/admin.service'; import { AdminService } from '@ghostfolio/client/services/admin.service';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { Filter, User } from '@ghostfolio/common/interfaces'; import { Filter, PortfolioPosition, 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';
@ -36,6 +37,7 @@ import { MatMenuTrigger } from '@angular/material/menu';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { Account, AssetClass } from '@prisma/client'; import { Account, AssetClass } from '@prisma/client';
import { sortBy } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs'; import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs';
import { import {
@ -66,7 +68,8 @@ import {
MatSelectModule, MatSelectModule,
NgxSkeletonLoaderModule, NgxSkeletonLoaderModule,
ReactiveFormsModule, ReactiveFormsModule,
RouterModule RouterModule,
GfSymbolModule
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-assistant', selector: 'gf-assistant',
@ -132,7 +135,8 @@ export class GfAssistantComponent 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), assetClass: new FormControl<string>(undefined),
tag: new FormControl<string>(undefined) tag: new FormControl<string>(undefined),
holdings: new FormControl<PortfolioPosition>(undefined)
}); });
public isLoading = false; public isLoading = false;
public isOpen = false; public isOpen = false;
@ -143,8 +147,15 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
holdings: [] holdings: []
}; };
public tags: Filter[] = []; public tags: Filter[] = [];
public allPortfolioHoldings: PortfolioPosition[] = [];
private filterTypes: Filter['type'][] = ['ACCOUNT', 'ASSET_CLASS', 'TAG'];
private filterTypes: Filter['type'][] = [
'ACCOUNT',
'ASSET_CLASS',
'TAG',
'DATA_SOURCE',
'SYMBOL'
];
private keyManager: FocusKeyManager<GfAssistantListItemComponent>; private keyManager: FocusKeyManager<GfAssistantListItemComponent>;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
@ -156,6 +167,8 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
) {} ) {}
public ngOnInit() { public ngOnInit() {
this.loadPortfolioHoldings();
this.assetClasses = Object.keys(AssetClass).map((assetClass) => { this.assetClasses = Object.keys(AssetClass).map((assetClass) => {
return { return {
id: assetClass, id: assetClass,
@ -263,16 +276,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
this.filterForm.enable({ emitEvent: false }); this.filterForm.enable({ emitEvent: false });
} }
this.filterForm.setValue( this.loadPortfolioHoldings();
{
account: this.user?.settings?.['filters.accounts']?.[0] ?? null,
assetClass: this.user?.settings?.['filters.assetClasses']?.[0] ?? null,
tag: this.user?.settings?.['filters.tags']?.[0] ?? null
},
{
emitEvent: false
}
);
this.tags = this.tags =
this.user?.tags this.user?.tags
@ -334,6 +338,14 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
{ {
id: this.filterForm.get('tag').value, id: this.filterForm.get('tag').value,
type: 'TAG' type: 'TAG'
},
{
id: this.filterForm.get('holdings').value.dataSource,
type: 'DATA_SOURCE'
},
{
id: this.filterForm.get('holdings').value.symbol,
type: 'SYMBOL'
} }
]); ]);
@ -473,4 +485,48 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
takeUntil(this.unsubscribeSubject) takeUntil(this.unsubscribeSubject)
); );
} }
public holdingComparisonFunction(option, value): boolean {
return (
option.dataSource === value.dataSource && option.symbol === value.symbol
);
}
private loadPortfolioHoldings() {
this.dataService
.fetchPortfolioHoldings({
range: 'max'
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ holdings }) => {
this.allPortfolioHoldings = sortBy(holdings, ({ name }) => {
return name.toLowerCase();
});
this.setFormValues();
//this.changeDetectorRef.markForCheck();
});
}
private setFormValues() {
const dataSource = this.user?.settings?.['filters.dataSource']?.[0] ?? null;
const symbol = this.user?.settings?.['filters.symbol']?.[0] ?? null;
const selectedHolding = this.allPortfolioHoldings.filter(
(h) => h.dataSource === dataSource && h.symbol === symbol
);
const holding = selectedHolding[0];
this.filterForm.setValue(
{
account: this.user?.settings?.['filters.accounts']?.[0] ?? null,
assetClass: this.user?.settings?.['filters.assetClasses']?.[0] ?? null,
tag: this.user?.settings?.['filters.tags']?.[0] ?? null,
holdings: holding
},
{
emitEvent: false
}
);
}
} }

26
libs/ui/src/lib/assistant/assistant.html

@ -122,6 +122,32 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Holding</mat-label>
<mat-select
formControlName="holdings"
[compareWith]="holdingComparisonFunction"
>
<mat-select-trigger>{{
filterForm.get('holdings')?.value?.name
}}</mat-select-trigger>
<mat-option [value]="null" />
@for (holding of allPortfolioHoldings; track holding.name) {
<mat-option class="line-height-1" [value]="holding">
<span
><b>{{ holding.name }}</b></span
>
<br />
<small class="text-muted"
>{{ holding.symbol | gfSymbol }} ·
{{ holding.currency }}</small
>
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="mb-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>Tags</mat-label> <mat-label i18n>Tags</mat-label>

Loading…
Cancel
Save