Feature/extend assistant with search for asset profile (#2499)
* Extend assistant with search for asset profile * Extend search results by currency, symbol and asset sub class * Update changelog
This commit is contained in:
parent
7243090c0e
commit
30e561c06f
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added the endpoint `GET api/v1/account/:id/balances` which provides historical cash balances
|
- Added the endpoint `GET api/v1/account/:id/balances` which provides historical cash balances
|
||||||
|
- Added support to search for an asset profile by `isin`, `name` and `symbol` as an administrator (experimental)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
||||||
|
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
|
||||||
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
||||||
import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
|
import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
|
||||||
import {
|
import {
|
||||||
DEFAULT_PAGE_SIZE,
|
|
||||||
GATHER_ASSET_PROFILE_PROCESS,
|
GATHER_ASSET_PROFILE_PROCESS,
|
||||||
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
@ -12,8 +12,7 @@ import {
|
|||||||
AdminData,
|
AdminData,
|
||||||
AdminMarketData,
|
AdminMarketData,
|
||||||
AdminMarketDataDetails,
|
AdminMarketDataDetails,
|
||||||
EnhancedSymbolProfile,
|
EnhancedSymbolProfile
|
||||||
Filter
|
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import type {
|
import type {
|
||||||
@ -50,6 +49,7 @@ import { UpdateMarketDataDto } from './update-market-data.dto';
|
|||||||
export class AdminController {
|
export class AdminController {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly adminService: AdminService,
|
private readonly adminService: AdminService,
|
||||||
|
private readonly apiService: ApiService,
|
||||||
private readonly dataGatheringService: DataGatheringService,
|
private readonly dataGatheringService: DataGatheringService,
|
||||||
private readonly marketDataService: MarketDataService,
|
private readonly marketDataService: MarketDataService,
|
||||||
@Inject(REQUEST) private readonly request: RequestWithUser
|
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||||
@ -255,6 +255,7 @@ export class AdminController {
|
|||||||
public async getMarketData(
|
public async getMarketData(
|
||||||
@Query('assetSubClasses') filterByAssetSubClasses?: string,
|
@Query('assetSubClasses') filterByAssetSubClasses?: string,
|
||||||
@Query('presetId') presetId?: MarketDataPreset,
|
@Query('presetId') presetId?: MarketDataPreset,
|
||||||
|
@Query('query') filterBySearchQuery?: string,
|
||||||
@Query('skip') skip?: number,
|
@Query('skip') skip?: number,
|
||||||
@Query('sortColumn') sortColumn?: string,
|
@Query('sortColumn') sortColumn?: string,
|
||||||
@Query('sortDirection') sortDirection?: Prisma.SortOrder,
|
@Query('sortDirection') sortDirection?: Prisma.SortOrder,
|
||||||
@ -272,16 +273,10 @@ export class AdminController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const assetSubClasses = filterByAssetSubClasses?.split(',') ?? [];
|
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||||
|
filterByAssetSubClasses,
|
||||||
const filters: Filter[] = [
|
filterBySearchQuery
|
||||||
...assetSubClasses.map((assetSubClass) => {
|
});
|
||||||
return <Filter>{
|
|
||||||
id: assetSubClass,
|
|
||||||
type: 'ASSET_SUB_CLASS'
|
|
||||||
};
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
return this.adminService.getMarketData({
|
return this.adminService.getMarketData({
|
||||||
filters,
|
filters,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
|
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
|
||||||
|
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
@ -15,6 +16,7 @@ import { QueueModule } from './queue/queue.module';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
ApiModule,
|
||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
DataGatheringModule,
|
DataGatheringModule,
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
|
@ -131,10 +131,14 @@ export class AdminService {
|
|||||||
filters = [{ id: 'ETF', type: 'ASSET_SUB_CLASS' }];
|
filters = [{ id: 'ETF', type: 'ASSET_SUB_CLASS' }];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const searchQuery = filters.find(({ type }) => {
|
||||||
|
return type === 'SEARCH_QUERY';
|
||||||
|
})?.id;
|
||||||
|
|
||||||
const { ASSET_SUB_CLASS: filtersByAssetSubClass } = groupBy(
|
const { ASSET_SUB_CLASS: filtersByAssetSubClass } = groupBy(
|
||||||
filters,
|
filters,
|
||||||
(filter) => {
|
({ type }) => {
|
||||||
return filter.type;
|
return type;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -147,6 +151,14 @@ export class AdminService {
|
|||||||
where.assetSubClass = AssetSubClass[filtersByAssetSubClass[0].id];
|
where.assetSubClass = AssetSubClass[filtersByAssetSubClass[0].id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (searchQuery) {
|
||||||
|
where.OR = [
|
||||||
|
{ isin: { mode: 'insensitive', startsWith: searchQuery } },
|
||||||
|
{ name: { mode: 'insensitive', startsWith: searchQuery } },
|
||||||
|
{ symbol: { mode: 'insensitive', startsWith: searchQuery } }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
if (sortColumn) {
|
if (sortColumn) {
|
||||||
orderBy = [{ [sortColumn]: sortDirection }];
|
orderBy = [{ [sortColumn]: sortDirection }];
|
||||||
|
|
||||||
@ -173,7 +185,9 @@ export class AdminService {
|
|||||||
assetSubClass: true,
|
assetSubClass: true,
|
||||||
comment: true,
|
comment: true,
|
||||||
countries: true,
|
countries: true,
|
||||||
|
currency: true,
|
||||||
dataSource: true,
|
dataSource: true,
|
||||||
|
name: true,
|
||||||
Order: {
|
Order: {
|
||||||
orderBy: [{ date: 'asc' }],
|
orderBy: [{ date: 'asc' }],
|
||||||
select: { date: true },
|
select: { date: true },
|
||||||
@ -194,7 +208,9 @@ export class AdminService {
|
|||||||
assetSubClass,
|
assetSubClass,
|
||||||
comment,
|
comment,
|
||||||
countries,
|
countries,
|
||||||
|
currency,
|
||||||
dataSource,
|
dataSource,
|
||||||
|
name,
|
||||||
Order,
|
Order,
|
||||||
sectors,
|
sectors,
|
||||||
symbol
|
symbol
|
||||||
@ -213,8 +229,10 @@ export class AdminService {
|
|||||||
assetClass,
|
assetClass,
|
||||||
assetSubClass,
|
assetSubClass,
|
||||||
comment,
|
comment,
|
||||||
|
currency,
|
||||||
countriesCount,
|
countriesCount,
|
||||||
dataSource,
|
dataSource,
|
||||||
|
name,
|
||||||
symbol,
|
symbol,
|
||||||
marketDataItemCount,
|
marketDataItemCount,
|
||||||
sectorsCount,
|
sectorsCount,
|
||||||
@ -341,6 +359,8 @@ export class AdminService {
|
|||||||
symbol,
|
symbol,
|
||||||
assetClass: 'CASH',
|
assetClass: 'CASH',
|
||||||
countriesCount: 0,
|
countriesCount: 0,
|
||||||
|
currency: symbol.replace(DEFAULT_CURRENCY, ''),
|
||||||
|
name: symbol,
|
||||||
sectorsCount: 0
|
sectorsCount: 0
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -1088,6 +1088,7 @@ export class PortfolioService {
|
|||||||
return {
|
return {
|
||||||
...position,
|
...position,
|
||||||
assetClass: symbolProfileMap[position.symbol].assetClass,
|
assetClass: symbolProfileMap[position.symbol].assetClass,
|
||||||
|
assetSubClass: symbolProfileMap[position.symbol].assetSubClass,
|
||||||
averagePrice: new Big(position.averagePrice).toNumber(),
|
averagePrice: new Big(position.averagePrice).toNumber(),
|
||||||
grossPerformance: position.grossPerformance?.toNumber() ?? null,
|
grossPerformance: position.grossPerformance?.toNumber() ?? null,
|
||||||
grossPerformancePercentage:
|
grossPerformancePercentage:
|
||||||
|
@ -8,16 +8,19 @@ export class ApiService {
|
|||||||
public buildFiltersFromQueryParams({
|
public buildFiltersFromQueryParams({
|
||||||
filterByAccounts,
|
filterByAccounts,
|
||||||
filterByAssetClasses,
|
filterByAssetClasses,
|
||||||
|
filterByAssetSubClasses,
|
||||||
filterBySearchQuery,
|
filterBySearchQuery,
|
||||||
filterByTags
|
filterByTags
|
||||||
}: {
|
}: {
|
||||||
filterByAccounts?: string;
|
filterByAccounts?: string;
|
||||||
filterByAssetClasses?: string;
|
filterByAssetClasses?: string;
|
||||||
|
filterByAssetSubClasses?: string;
|
||||||
filterBySearchQuery?: string;
|
filterBySearchQuery?: string;
|
||||||
filterByTags?: string;
|
filterByTags?: string;
|
||||||
}): Filter[] {
|
}): Filter[] {
|
||||||
const accountIds = filterByAccounts?.split(',') ?? [];
|
const accountIds = filterByAccounts?.split(',') ?? [];
|
||||||
const assetClasses = filterByAssetClasses?.split(',') ?? [];
|
const assetClasses = filterByAssetClasses?.split(',') ?? [];
|
||||||
|
const assetSubClasses = filterByAssetSubClasses?.split(',') ?? [];
|
||||||
const searchQuery = filterBySearchQuery?.toLowerCase();
|
const searchQuery = filterBySearchQuery?.toLowerCase();
|
||||||
const tagIds = filterByTags?.split(',') ?? [];
|
const tagIds = filterByTags?.split(',') ?? [];
|
||||||
|
|
||||||
@ -34,6 +37,12 @@ export class ApiService {
|
|||||||
type: 'ASSET_CLASS'
|
type: 'ASSET_CLASS'
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
...assetSubClasses.map((assetClass) => {
|
||||||
|
return <Filter>{
|
||||||
|
id: assetClass,
|
||||||
|
type: 'ASSET_SUB_CLASS'
|
||||||
|
};
|
||||||
|
}),
|
||||||
{
|
{
|
||||||
id: searchQuery,
|
id: searchQuery,
|
||||||
type: 'SEARCH_QUERY'
|
type: 'SEARCH_QUERY'
|
||||||
|
@ -131,6 +131,9 @@
|
|||||||
<gf-assistant
|
<gf-assistant
|
||||||
#assistant
|
#assistant
|
||||||
[deviceType]="deviceType"
|
[deviceType]="deviceType"
|
||||||
|
[hasPermissionToAccessAdminControl]="
|
||||||
|
hasPermissionToAccessAdminControl
|
||||||
|
"
|
||||||
(closed)="closeAssistant()"
|
(closed)="closeAssistant()"
|
||||||
/>
|
/>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
@ -9,9 +9,11 @@ export interface AdminMarketDataItem {
|
|||||||
assetClass?: AssetClass;
|
assetClass?: AssetClass;
|
||||||
assetSubClass?: AssetSubClass;
|
assetSubClass?: AssetSubClass;
|
||||||
countriesCount: number;
|
countriesCount: number;
|
||||||
|
currency: string;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
date?: Date;
|
date?: Date;
|
||||||
marketDataItemCount: number;
|
marketDataItemCount: number;
|
||||||
|
name: string;
|
||||||
sectorsCount: number;
|
sectorsCount: number;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { AssetClass, DataSource } from '@prisma/client';
|
import { MarketState } from '@ghostfolio/common/types';
|
||||||
|
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
|
||||||
import { MarketState } from '../types';
|
|
||||||
|
|
||||||
export interface Position {
|
export interface Position {
|
||||||
assetClass: AssetClass;
|
assetClass: AssetClass;
|
||||||
|
assetSubClass: AssetSubClass;
|
||||||
averagePrice: number;
|
averagePrice: number;
|
||||||
currency: string;
|
currency: string;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
|
@ -7,10 +7,12 @@ import {
|
|||||||
EventEmitter,
|
EventEmitter,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
Input,
|
Input,
|
||||||
|
OnChanges,
|
||||||
Output,
|
Output,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { Position } from '@ghostfolio/common/interfaces';
|
import { Params } from '@angular/router';
|
||||||
|
import { ISearchResultItem } from '@ghostfolio/ui/assistant/interfaces/interfaces';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@ -18,22 +20,46 @@ import { Position } from '@ghostfolio/common/interfaces';
|
|||||||
templateUrl: './assistant-list-item.html',
|
templateUrl: './assistant-list-item.html',
|
||||||
styleUrls: ['./assistant-list-item.scss']
|
styleUrls: ['./assistant-list-item.scss']
|
||||||
})
|
})
|
||||||
export class AssistantListItemComponent implements FocusableOption {
|
export class AssistantListItemComponent implements FocusableOption, OnChanges {
|
||||||
@HostBinding('attr.tabindex') tabindex = -1;
|
@HostBinding('attr.tabindex') tabindex = -1;
|
||||||
@HostBinding('class.has-focus') get getHasFocus() {
|
@HostBinding('class.has-focus') get getHasFocus() {
|
||||||
return this.hasFocus;
|
return this.hasFocus;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Input() holding: Position;
|
@Input() item: ISearchResultItem;
|
||||||
|
@Input() mode: 'assetProfile' | 'holding';
|
||||||
|
|
||||||
@Output() clicked = new EventEmitter<void>();
|
@Output() clicked = new EventEmitter<void>();
|
||||||
|
|
||||||
@ViewChild('link') public linkElement: ElementRef;
|
@ViewChild('link') public linkElement: ElementRef;
|
||||||
|
|
||||||
public hasFocus = false;
|
public hasFocus = false;
|
||||||
|
public queryParams: Params;
|
||||||
|
public routerLink: string[];
|
||||||
|
|
||||||
public constructor(private changeDetectorRef: ChangeDetectorRef) {}
|
public constructor(private changeDetectorRef: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
public ngOnChanges() {
|
||||||
|
const dataSource = this.item?.dataSource;
|
||||||
|
const symbol = this.item?.symbol;
|
||||||
|
|
||||||
|
if (this.mode === 'assetProfile') {
|
||||||
|
this.queryParams = {
|
||||||
|
dataSource,
|
||||||
|
symbol,
|
||||||
|
assetProfileDialog: true
|
||||||
|
};
|
||||||
|
this.routerLink = ['/admin', 'market-data'];
|
||||||
|
} else if (this.mode === 'holding') {
|
||||||
|
this.queryParams = {
|
||||||
|
dataSource,
|
||||||
|
symbol,
|
||||||
|
positionDetailDialog: true
|
||||||
|
};
|
||||||
|
this.routerLink = ['/portfolio', 'holdings'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public focus() {
|
public focus() {
|
||||||
this.hasFocus = true;
|
this.hasFocus = true;
|
||||||
|
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
<a
|
<a
|
||||||
#link
|
#link
|
||||||
class="d-block px-2 py-1 text-truncate"
|
class="d-block line-height-1 px-2 py-1 text-truncate"
|
||||||
[queryParams]="{
|
[queryParams]="queryParams"
|
||||||
dataSource: holding?.dataSource,
|
[routerLink]="routerLink"
|
||||||
positionDetailDialog: true,
|
|
||||||
symbol: holding?.symbol
|
|
||||||
}"
|
|
||||||
[routerLink]="['/portfolio', 'holdings']"
|
|
||||||
(click)="onClick()"
|
(click)="onClick()"
|
||||||
>{{ holding?.name }}</a
|
><span><b>{{ item?.name }}</b></span>
|
||||||
|
<br />
|
||||||
|
<small class="text-muted"
|
||||||
|
>{{ item?.symbol | gfSymbol }} · {{ item?.currency }}<ng-container
|
||||||
|
*ngIf="item?.assetSubClassString"
|
||||||
|
>
|
||||||
|
· {{ item?.assetSubClassString }}</ng-container
|
||||||
|
></small
|
||||||
|
></a
|
||||||
>
|
>
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||||
|
|
||||||
import { AssistantListItemComponent } from './assistant-list-item.component';
|
import { AssistantListItemComponent } from './assistant-list-item.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [AssistantListItemComponent],
|
declarations: [AssistantListItemComponent],
|
||||||
exports: [AssistantListItemComponent],
|
exports: [AssistantListItemComponent],
|
||||||
imports: [CommonModule, RouterModule]
|
imports: [CommonModule, GfSymbolModule, RouterModule]
|
||||||
})
|
})
|
||||||
export class GfAssistantListItemModule {}
|
export class GfAssistantListItemModule {}
|
||||||
|
@ -16,9 +16,10 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormControl } from '@angular/forms';
|
import { FormControl } from '@angular/forms';
|
||||||
import { MatMenuTrigger } from '@angular/material/menu';
|
import { MatMenuTrigger } from '@angular/material/menu';
|
||||||
|
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 { Position } from '@ghostfolio/common/interfaces';
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
import { EMPTY, Subject, lastValueFrom } from 'rxjs';
|
import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
catchError,
|
catchError,
|
||||||
debounceTime,
|
debounceTime,
|
||||||
@ -29,13 +30,13 @@ import {
|
|||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
import { AssistantListItemComponent } from './assistant-list-item/assistant-list-item.component';
|
import { AssistantListItemComponent } from './assistant-list-item/assistant-list-item.component';
|
||||||
import { ISearchResults } from './interfaces/interfaces';
|
import { ISearchResultItem, ISearchResults } from './interfaces/interfaces';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
selector: 'gf-assistant',
|
selector: 'gf-assistant',
|
||||||
templateUrl: './assistant.html',
|
styleUrls: ['./assistant.scss'],
|
||||||
styleUrls: ['./assistant.scss']
|
templateUrl: './assistant.html'
|
||||||
})
|
})
|
||||||
export class AssistantComponent implements OnDestroy, OnInit {
|
export class AssistantComponent implements OnDestroy, OnInit {
|
||||||
@HostListener('document:keydown', ['$event']) onKeydown(
|
@HostListener('document:keydown', ['$event']) onKeydown(
|
||||||
@ -71,6 +72,7 @@ export class AssistantComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Input() deviceType: string;
|
@Input() deviceType: string;
|
||||||
|
@Input() hasPermissionToAccessAdminControl: boolean;
|
||||||
|
|
||||||
@Output() closed = new EventEmitter<void>();
|
@Output() closed = new EventEmitter<void>();
|
||||||
|
|
||||||
@ -87,6 +89,7 @@ export class AssistantComponent implements OnDestroy, OnInit {
|
|||||||
public placeholder = $localize`Find holding...`;
|
public placeholder = $localize`Find holding...`;
|
||||||
public searchFormControl = new FormControl('');
|
public searchFormControl = new FormControl('');
|
||||||
public searchResults: ISearchResults = {
|
public searchResults: ISearchResults = {
|
||||||
|
assetProfiles: [],
|
||||||
holdings: []
|
holdings: []
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -94,6 +97,7 @@ export class AssistantComponent implements OnDestroy, OnInit {
|
|||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
|
private adminService: AdminService,
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService
|
private dataService: DataService
|
||||||
) {}
|
) {}
|
||||||
@ -104,6 +108,7 @@ export class AssistantComponent implements OnDestroy, OnInit {
|
|||||||
map((searchTerm) => {
|
map((searchTerm) => {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.searchResults = {
|
this.searchResults = {
|
||||||
|
assetProfiles: [],
|
||||||
holdings: []
|
holdings: []
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -115,6 +120,7 @@ export class AssistantComponent implements OnDestroy, OnInit {
|
|||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
mergeMap(async (searchTerm) => {
|
mergeMap(async (searchTerm) => {
|
||||||
const result = <ISearchResults>{
|
const result = <ISearchResults>{
|
||||||
|
assetProfiles: [],
|
||||||
holdings: []
|
holdings: []
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -140,6 +146,7 @@ export class AssistantComponent implements OnDestroy, OnInit {
|
|||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.keyManager = new FocusKeyManager(this.assistantListItems).withWrap();
|
this.keyManager = new FocusKeyManager(this.assistantListItems).withWrap();
|
||||||
this.searchResults = {
|
this.searchResults = {
|
||||||
|
assetProfiles: [],
|
||||||
holdings: []
|
holdings: []
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -180,10 +187,23 @@ export class AssistantComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getSearchResults(aSearchTerm: string) {
|
private async getSearchResults(aSearchTerm: string) {
|
||||||
let holdings: Position[] = [];
|
let assetProfiles: ISearchResultItem[] = [];
|
||||||
|
let holdings: ISearchResultItem[] = [];
|
||||||
|
|
||||||
|
if (this.hasPermissionToAccessAdminControl) {
|
||||||
|
try {
|
||||||
|
assetProfiles = await lastValueFrom(
|
||||||
|
this.searchAssetProfiles(aSearchTerm)
|
||||||
|
);
|
||||||
|
assetProfiles = assetProfiles.slice(
|
||||||
|
0,
|
||||||
|
AssistantComponent.SEARCH_RESULTS_DEFAULT_LIMIT
|
||||||
|
);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
holdings = await lastValueFrom(this.searchHolding(aSearchTerm));
|
holdings = await lastValueFrom(this.searchHoldings(aSearchTerm));
|
||||||
holdings = holdings.slice(
|
holdings = holdings.slice(
|
||||||
0,
|
0,
|
||||||
AssistantComponent.SEARCH_RESULTS_DEFAULT_LIMIT
|
AssistantComponent.SEARCH_RESULTS_DEFAULT_LIMIT
|
||||||
@ -191,11 +211,46 @@ export class AssistantComponent implements OnDestroy, OnInit {
|
|||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
assetProfiles,
|
||||||
holdings
|
holdings
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private searchHolding(aSearchTerm: string) {
|
private searchAssetProfiles(
|
||||||
|
aSearchTerm: string
|
||||||
|
): Observable<ISearchResultItem[]> {
|
||||||
|
return this.adminService
|
||||||
|
.fetchAdminMarketData({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
id: aSearchTerm,
|
||||||
|
type: 'SEARCH_QUERY'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
take: AssistantComponent.SEARCH_RESULTS_DEFAULT_LIMIT
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
catchError(() => {
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
|
map(({ marketData }) => {
|
||||||
|
return marketData.map(
|
||||||
|
({ assetSubClass, currency, dataSource, name, symbol }) => {
|
||||||
|
return {
|
||||||
|
currency,
|
||||||
|
dataSource,
|
||||||
|
name,
|
||||||
|
symbol,
|
||||||
|
assetSubClassString: translate(assetSubClass)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
takeUntil(this.unsubscribeSubject)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private searchHoldings(aSearchTerm: string): Observable<ISearchResultItem[]> {
|
||||||
return this.dataService
|
return this.dataService
|
||||||
.fetchPositions({
|
.fetchPositions({
|
||||||
filters: [
|
filters: [
|
||||||
@ -211,7 +266,17 @@ export class AssistantComponent implements OnDestroy, OnInit {
|
|||||||
return EMPTY;
|
return EMPTY;
|
||||||
}),
|
}),
|
||||||
map(({ positions }) => {
|
map(({ positions }) => {
|
||||||
return positions;
|
return positions.map(
|
||||||
|
({ assetSubClass, currency, dataSource, name, symbol }) => {
|
||||||
|
return {
|
||||||
|
currency,
|
||||||
|
dataSource,
|
||||||
|
name,
|
||||||
|
symbol,
|
||||||
|
assetSubClassString: translate(assetSubClass)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
takeUntil(this.unsubscribeSubject)
|
takeUntil(this.unsubscribeSubject)
|
||||||
);
|
);
|
||||||
|
@ -45,8 +45,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="h6 mb-1 px-2" i18n>Holdings</div>
|
<div class="h6 mb-1 px-2" i18n>Holdings</div>
|
||||||
<gf-assistant-list-item
|
<gf-assistant-list-item
|
||||||
*ngFor="let holding of searchResults?.holdings"
|
*ngFor="let searchResultItem of searchResults?.holdings"
|
||||||
[holding]="holding"
|
mode="holding"
|
||||||
|
[item]="searchResultItem"
|
||||||
(clicked)="onCloseAssistant()"
|
(clicked)="onCloseAssistant()"
|
||||||
/>
|
/>
|
||||||
<ng-container *ngIf="searchResults?.holdings?.length === 0">
|
<ng-container *ngIf="searchResults?.holdings?.length === 0">
|
||||||
@ -62,5 +63,26 @@
|
|||||||
<div *ngIf="!isLoading" class="px-2 py-1" i18n>No entries...</div>
|
<div *ngIf="!isLoading" class="px-2 py-1" i18n>No entries...</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
<div *ngIf="hasPermissionToAccessAdminControl" class="mt-3">
|
||||||
|
<div class="h6 mb-1 px-2" i18n>Asset Profiles</div>
|
||||||
|
<gf-assistant-list-item
|
||||||
|
*ngFor="let searchResultItem of searchResults?.assetProfiles"
|
||||||
|
mode="assetProfile"
|
||||||
|
[item]="searchResultItem"
|
||||||
|
(clicked)="onCloseAssistant()"
|
||||||
|
/>
|
||||||
|
<ng-container *ngIf="searchResults?.assetProfiles?.length === 0">
|
||||||
|
<ngx-skeleton-loader
|
||||||
|
*ngIf="isLoading"
|
||||||
|
animation="pulse"
|
||||||
|
class="mx-2"
|
||||||
|
[theme]="{
|
||||||
|
height: '1.5rem',
|
||||||
|
width: '100%'
|
||||||
|
}"
|
||||||
|
></ngx-skeleton-loader>
|
||||||
|
<div *ngIf="!isLoading" class="px-2 py-1" i18n>No entries...</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
import { Position } from '@ghostfolio/common/interfaces';
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
|
export interface ISearchResultItem extends UniqueAsset {
|
||||||
|
assetSubClassString: string;
|
||||||
|
currency: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ISearchResults {
|
export interface ISearchResults {
|
||||||
holdings: Position[];
|
assetProfiles: ISearchResultItem[];
|
||||||
|
holdings: ISearchResultItem[];
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user