Browse Source

Feature/extend search in assistant by accounts (#5356)

* Extend search in assistant by accounts

* Update changelog
pull/5370/head^2
David Requeno 4 days ago
committed by GitHub
parent
commit
473c474845
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 17
      libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.component.ts
  3. 63
      libs/ui/src/lib/assistant/assistant.component.ts
  4. 28
      libs/ui/src/lib/assistant/assistant.html
  5. 1
      libs/ui/src/lib/assistant/enums/search-mode.ts
  6. 10
      libs/ui/src/lib/assistant/interfaces/interfaces.ts

4
CHANGELOG.md

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Included accounts in the search results of the assistant
### Changed ### Changed
- Migrated the prompt dialog component from `ngModel` to form control - Migrated the prompt dialog component from `ngModel` to form control

17
libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.component.ts

@ -50,20 +50,27 @@ export class GfAssistantListItemComponent
public constructor(private changeDetectorRef: ChangeDetectorRef) {} public constructor(private changeDetectorRef: ChangeDetectorRef) {}
public ngOnChanges() { public ngOnChanges() {
if (this.item?.mode === SearchMode.ASSET_PROFILE) { if (this.item?.mode === SearchMode.ACCOUNT) {
this.queryParams = {
accountDetailDialog: true,
accountId: this.item.id
};
this.routerLink = internalRoutes.accounts.routerLink;
} else if (this.item?.mode === SearchMode.ASSET_PROFILE) {
this.queryParams = { this.queryParams = {
assetProfileDialog: true, assetProfileDialog: true,
dataSource: this.item?.dataSource, dataSource: this.item.dataSource,
symbol: this.item?.symbol symbol: this.item.symbol
}; };
this.routerLink = this.routerLink =
internalRoutes.adminControl.subRoutes.marketData.routerLink; internalRoutes.adminControl.subRoutes.marketData.routerLink;
} else if (this.item?.mode === SearchMode.HOLDING) { } else if (this.item?.mode === SearchMode.HOLDING) {
this.queryParams = { this.queryParams = {
dataSource: this.item?.dataSource, dataSource: this.item.dataSource,
holdingDetailDialog: true, holdingDetailDialog: true,
symbol: this.item?.symbol symbol: this.item.symbol
}; };
this.routerLink = []; this.routerLink = [];

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

@ -153,14 +153,16 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
}); });
public holdings: PortfolioPosition[] = []; public holdings: PortfolioPosition[] = [];
public isLoading = { public isLoading = {
accounts: false,
assetProfiles: false, assetProfiles: false,
holdings: false, holdings: false,
quickLinks: false quickLinks: false
}; };
public isOpen = false; public isOpen = false;
public placeholder = $localize`Find holding or page...`; public placeholder = $localize`Find account, holding or page...`;
public searchFormControl = new FormControl(''); public searchFormControl = new FormControl('');
public searchResults: ISearchResults = { public searchResults: ISearchResults = {
accounts: [],
assetProfiles: [], assetProfiles: [],
holdings: [], holdings: [],
quickLinks: [] quickLinks: []
@ -199,11 +201,13 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
.pipe( .pipe(
map((searchTerm) => { map((searchTerm) => {
this.isLoading = { this.isLoading = {
accounts: true,
assetProfiles: true, assetProfiles: true,
holdings: true, holdings: true,
quickLinks: true quickLinks: true
}; };
this.searchResults = { this.searchResults = {
accounts: [],
assetProfiles: [], assetProfiles: [],
holdings: [], holdings: [],
quickLinks: [] quickLinks: []
@ -217,6 +221,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
distinctUntilChanged(), distinctUntilChanged(),
switchMap((searchTerm) => { switchMap((searchTerm) => {
const results = { const results = {
accounts: [],
assetProfiles: [], assetProfiles: [],
holdings: [], holdings: [],
quickLinks: [] quickLinks: []
@ -226,6 +231,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
return of(results).pipe( return of(results).pipe(
tap(() => { tap(() => {
this.isLoading = { this.isLoading = {
accounts: false,
assetProfiles: false, assetProfiles: false,
holdings: false, holdings: false,
quickLinks: false quickLinks: false
@ -234,6 +240,25 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
); );
} }
// Accounts
const accounts$: Observable<Partial<ISearchResults>> =
this.searchAccounts(searchTerm).pipe(
map((accounts) => ({
accounts: accounts.slice(
0,
GfAssistantComponent.SEARCH_RESULTS_DEFAULT_LIMIT
)
})),
catchError((error) => {
console.error('Error fetching accounts for assistant:', error);
return of({ accounts: [] as ISearchResultItem[] });
}),
tap(() => {
this.isLoading.accounts = false;
this.changeDetectorRef.markForCheck();
})
);
// Asset profiles // Asset profiles
const assetProfiles$: Observable<Partial<ISearchResults>> = this const assetProfiles$: Observable<Partial<ISearchResults>> = this
.hasPermissionToAccessAdminControl .hasPermissionToAccessAdminControl
@ -299,13 +324,14 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
); );
// Merge all results // Merge all results
return merge(quickLinks$, assetProfiles$, holdings$).pipe( return merge(accounts$, assetProfiles$, holdings$, quickLinks$).pipe(
scan( scan(
(acc: ISearchResults, curr: Partial<ISearchResults>) => ({ (acc: ISearchResults, curr: Partial<ISearchResults>) => ({
...acc, ...acc,
...curr ...curr
}), }),
{ {
accounts: [],
assetProfiles: [], assetProfiles: [],
holdings: [], holdings: [],
quickLinks: [] quickLinks: []
@ -323,6 +349,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
error: (error) => { error: (error) => {
console.error('Assistant search stream error:', error); console.error('Assistant search stream error:', error);
this.searchResults = { this.searchResults = {
accounts: [],
assetProfiles: [], assetProfiles: [],
holdings: [], holdings: [],
quickLinks: [] quickLinks: []
@ -331,6 +358,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
}, },
complete: () => { complete: () => {
this.isLoading = { this.isLoading = {
accounts: false,
assetProfiles: false, assetProfiles: false,
holdings: false, holdings: false,
quickLinks: false quickLinks: false
@ -451,12 +479,14 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
public initialize() { public initialize() {
this.isLoading = { this.isLoading = {
accounts: true,
assetProfiles: true, assetProfiles: true,
holdings: true, holdings: true,
quickLinks: true quickLinks: true
}; };
this.keyManager = new FocusKeyManager(this.assistantListItems).withWrap(); this.keyManager = new FocusKeyManager(this.assistantListItems).withWrap();
this.searchResults = { this.searchResults = {
accounts: [],
assetProfiles: [], assetProfiles: [],
holdings: [], holdings: [],
quickLinks: [] quickLinks: []
@ -472,6 +502,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
}); });
this.isLoading = { this.isLoading = {
accounts: false,
assetProfiles: false, assetProfiles: false,
holdings: false, holdings: false,
quickLinks: false quickLinks: false
@ -564,6 +595,34 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
}); });
} }
private searchAccounts(aSearchTerm: string): Observable<ISearchResultItem[]> {
return this.dataService
.fetchAccounts({
filters: [
{
id: aSearchTerm,
type: 'SEARCH_QUERY'
}
]
})
.pipe(
catchError(() => {
return EMPTY;
}),
map(({ accounts }) => {
return accounts.map(({ id, name }) => {
return {
id,
name,
routerLink: internalRoutes.accounts.routerLink,
mode: SearchMode.ACCOUNT as const
};
});
}),
takeUntil(this.unsubscribeSubject)
);
}
private searchAssetProfiles( private searchAssetProfiles(
aSearchTerm: string aSearchTerm: string
): Observable<ISearchResultItem[]> { ): Observable<ISearchResultItem[]> {

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

@ -39,9 +39,11 @@
@if (searchFormControl.value) { @if (searchFormControl.value) {
<div class="overflow-auto py-2 result-container"> <div class="overflow-auto py-2 result-container">
@if ( @if (
!isLoading.accounts &&
!isLoading.assetProfiles && !isLoading.assetProfiles &&
!isLoading.holdings && !isLoading.holdings &&
!isLoading.quickLinks && !isLoading.quickLinks &&
searchResults.accounts?.length === 0 &&
searchResults.assetProfiles?.length === 0 && searchResults.assetProfiles?.length === 0 &&
searchResults.holdings?.length === 0 && searchResults.holdings?.length === 0 &&
searchResults.quickLinks?.length === 0 searchResults.quickLinks?.length === 0
@ -76,6 +78,32 @@
} }
</div> </div>
} }
@if (isLoading.accounts || searchResults?.accounts?.length !== 0) {
<div>
<div class="font-weight-bold px-3 text-muted title" i18n>
Accounts
</div>
@for (
searchResultItem of searchResults.accounts;
track searchResultItem
) {
<gf-assistant-list-item
[item]="searchResultItem"
(clicked)="onCloseAssistant()"
/>
}
@if (isLoading.accounts) {
<ngx-skeleton-loader
animation="pulse"
class="mx-3"
[theme]="{
height: '1.5rem',
width: '100%'
}"
/>
}
</div>
}
@if (isLoading.holdings || searchResults?.holdings?.length !== 0) { @if (isLoading.holdings || searchResults?.holdings?.length !== 0) {
<div> <div>
<div class="font-weight-bold px-3 text-muted title" i18n> <div class="font-weight-bold px-3 text-muted title" i18n>

1
libs/ui/src/lib/assistant/enums/search-mode.ts

@ -1,4 +1,5 @@
export enum SearchMode { export enum SearchMode {
ACCOUNT = 'account',
ASSET_PROFILE = 'assetProfile', ASSET_PROFILE = 'assetProfile',
HOLDING = 'holding', HOLDING = 'holding',
QUICK_LINK = 'quickLink' QUICK_LINK = 'quickLink'

10
libs/ui/src/lib/assistant/interfaces/interfaces.ts

@ -1,8 +1,14 @@
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { DateRange } from '@ghostfolio/common/types'; import { AccountWithValue, DateRange } from '@ghostfolio/common/types';
import { SearchMode } from '../enums/search-mode'; import { SearchMode } from '../enums/search-mode';
export interface IAccountSearchResultItem
extends Pick<AccountWithValue, 'id' | 'name'> {
mode: SearchMode.ACCOUNT;
routerLink: string[];
}
export interface IAssetSearchResultItem extends AssetProfileIdentifier { export interface IAssetSearchResultItem extends AssetProfileIdentifier {
assetSubClassString: string; assetSubClassString: string;
currency: string; currency: string;
@ -22,10 +28,12 @@ export interface IQuickLinkSearchResultItem {
} }
export type ISearchResultItem = export type ISearchResultItem =
| IAccountSearchResultItem
| IAssetSearchResultItem | IAssetSearchResultItem
| IQuickLinkSearchResultItem; | IQuickLinkSearchResultItem;
export interface ISearchResults { export interface ISearchResults {
accounts: ISearchResultItem[];
assetProfiles: ISearchResultItem[]; assetProfiles: ISearchResultItem[];
holdings: ISearchResultItem[]; holdings: ISearchResultItem[];
quickLinks: ISearchResultItem[]; quickLinks: ISearchResultItem[];

Loading…
Cancel
Save