From f3ee99fb2bb897a5ab818d6cbc8cb29933a3af9b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 27 Jan 2024 10:48:46 +0100 Subject: [PATCH] Feature/extend assistant by account selector (#2929) * Add account selector to assistant * Update changelog --- CHANGELOG.md | 1 + .../src/app/user/update-user-setting.dto.ts | 4 ++ .../components/header/header.component.html | 2 +- .../app/components/header/header.component.ts | 40 +++++++++++++------ .../activities/activities-page.component.ts | 2 +- .../src/app/services/user/user.service.ts | 28 ++++++++----- .../lib/interfaces/user-settings.interface.ts | 1 + .../src/lib/assistant/assistant.component.ts | 32 +++++++++------ libs/ui/src/lib/assistant/assistant.html | 29 +++++++++++++- libs/ui/src/lib/assistant/assistant.scss | 4 ++ 10 files changed, 104 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd84af247..778486db4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Extended the assistant by an account selector (experimental) - Added support to grant private access with permissions (experimental) - Added `permissions` to the `Access` model diff --git a/apps/api/src/app/user/update-user-setting.dto.ts b/apps/api/src/app/user/update-user-setting.dto.ts index b04f9a494..7d69f202d 100644 --- a/apps/api/src/app/user/update-user-setting.dto.ts +++ b/apps/api/src/app/user/update-user-setting.dto.ts @@ -38,6 +38,10 @@ export class UpdateUserSettingDto { @IsOptional() emergencyFund?: number; + @IsArray() + @IsOptional() + 'filters.accounts'?: string[]; + @IsArray() @IsOptional() 'filters.tags'?: string[]; diff --git a/apps/client/src/app/components/header/header.component.html b/apps/client/src/app/components/header/header.component.html index 8d08533e4..4b6d6dffc 100644 --- a/apps/client/src/app/components/header/header.component.html +++ b/apps/client/src/app/components/header/header.component.html @@ -141,7 +141,7 @@ [user]="user" (closed)="closeAssistant()" (dateRangeChanged)="onDateRangeChange($event)" - (selectedTagChanged)="onSelectedTagChanged($event)" + (filtersChanged)="onFiltersChanged($event)" /> diff --git a/apps/client/src/app/components/header/header.component.ts b/apps/client/src/app/components/header/header.component.ts index 19bd7e8fa..c706abe0a 100644 --- a/apps/client/src/app/components/header/header.component.ts +++ b/apps/client/src/app/components/header/header.component.ts @@ -11,6 +11,7 @@ import { import { MatDialog } from '@angular/material/dialog'; import { MatMenuTrigger } from '@angular/material/menu'; import { Router } from '@angular/router'; +import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto'; import { LoginWithAccessTokenDialog } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.component'; import { DataService } from '@ghostfolio/client/services/data.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; @@ -20,11 +21,10 @@ import { } from '@ghostfolio/client/services/settings-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { InfoItem, User } from '@ghostfolio/common/interfaces'; +import { Filter, InfoItem, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { DateRange } from '@ghostfolio/common/types'; import { AssistantComponent } from '@ghostfolio/ui/assistant/assistant.component'; -import { Tag } from '@prisma/client'; import { EMPTY, Subject } from 'rxjs'; import { catchError, takeUntil } from 'rxjs/operators'; @@ -162,21 +162,23 @@ export class HeaderComponent implements OnChanges { }); } - public onMenuClosed() { - this.isMenuOpen = false; - } + public onFiltersChanged(filters: Filter[]) { + const userSetting: UpdateUserSettingDto = {}; - public onMenuOpened() { - this.isMenuOpen = true; - } + for (const filter of filters) { + let filtersType: string; - public onOpenAssistant() { - this.assistantElement.initialize(); - } + if (filter.type === 'ACCOUNT') { + filtersType = 'accounts'; + } else if (filter.type === 'TAG') { + filtersType = 'tags'; + } + + userSetting[`filters.${filtersType}`] = filter.id ? [filter.id] : null; + } - public onSelectedTagChanged(tag: Tag) { this.dataService - .putUserSetting({ 'filters.tags': tag ? [tag.id] : null }) + .putUserSetting(userSetting) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => { this.userService.remove(); @@ -188,6 +190,18 @@ export class HeaderComponent implements OnChanges { }); } + public onMenuClosed() { + this.isMenuOpen = false; + } + + public onMenuOpened() { + this.isMenuOpen = true; + } + + public onOpenAssistant() { + this.assistantElement.initialize(); + } + public onSignOut() { this.signOut.next(); } diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts index d11429d3b..b125dd784 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts @@ -15,7 +15,7 @@ import { ImpersonationStorageService } from '@ghostfolio/client/services/imperso import { UserService } from '@ghostfolio/client/services/user/user.service'; import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { downloadAsFile } from '@ghostfolio/common/helper'; -import { Filter, User } from '@ghostfolio/common/interfaces'; +import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { DataSource, Order as OrderModel } from '@prisma/client'; import { format, parseISO } from 'date-fns'; diff --git a/apps/client/src/app/services/user/user.service.ts b/apps/client/src/app/services/user/user.service.ts index dd7e0ab58..539752f0a 100644 --- a/apps/client/src/app/services/user/user.service.ts +++ b/apps/client/src/app/services/user/user.service.ts @@ -47,18 +47,26 @@ export class UserService extends ObservableStore { } public getFilters() { + const filters: Filter[] = []; const user = this.getState().user; - return user?.settings?.isExperimentalFeatures === true - ? user.settings['filters.tags'] - ? [ - { - id: user.settings['filters.tags'][0], - type: 'TAG' - } - ] - : [] - : []; + if (user?.settings?.isExperimentalFeatures === true) { + if (user.settings['filters.accounts']) { + filters.push({ + id: user.settings['filters.accounts'][0], + type: 'ACCOUNT' + }); + } + + if (user.settings['filters.tags']) { + filters.push({ + id: user.settings['filters.tags'][0], + type: 'TAG' + }); + } + } + + return filters; } public remove() { diff --git a/libs/common/src/lib/interfaces/user-settings.interface.ts b/libs/common/src/lib/interfaces/user-settings.interface.ts index 4ac7c4726..a0599d132 100644 --- a/libs/common/src/lib/interfaces/user-settings.interface.ts +++ b/libs/common/src/lib/interfaces/user-settings.interface.ts @@ -7,6 +7,7 @@ export interface UserSettings { colorScheme?: ColorScheme; dateRange?: DateRange; emergencyFund?: number; + 'filters.accounts'?: string[]; 'filters.tags'?: string[]; isExperimentalFeatures?: boolean; isRestrictedView?: boolean; diff --git a/libs/ui/src/lib/assistant/assistant.component.ts b/libs/ui/src/lib/assistant/assistant.component.ts index 440035d4d..79ecce4b5 100644 --- a/libs/ui/src/lib/assistant/assistant.component.ts +++ b/libs/ui/src/lib/assistant/assistant.component.ts @@ -19,10 +19,10 @@ import { FormBuilder, FormControl } from '@angular/forms'; import { MatMenuTrigger } from '@angular/material/menu'; import { AdminService } from '@ghostfolio/client/services/admin.service'; import { DataService } from '@ghostfolio/client/services/data.service'; -import { User } from '@ghostfolio/common/interfaces'; +import { Filter, User } from '@ghostfolio/common/interfaces'; import { DateRange } from '@ghostfolio/common/types'; import { translate } from '@ghostfolio/ui/i18n'; -import { Tag } from '@prisma/client'; +import { Account, Tag } from '@prisma/client'; import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs'; import { catchError, @@ -81,7 +81,7 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit { @Output() closed = new EventEmitter(); @Output() dateRangeChanged = new EventEmitter(); - @Output() selectedTagChanged = new EventEmitter(); + @Output() filtersChanged = new EventEmitter(); @ViewChild('menuTrigger') menuTriggerElement: MatMenuTrigger; @ViewChild('search', { static: true }) searchElement: ElementRef; @@ -91,6 +91,7 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit { public static readonly SEARCH_RESULTS_DEFAULT_LIMIT = 5; + public accounts: Account[] = []; public dateRangeFormControl = new FormControl(undefined); public readonly dateRangeOptions = [ { label: $localize`Today`, value: '1d' }, @@ -111,6 +112,7 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit { { label: $localize`Max`, value: 'max' } ]; public filterForm = this.formBuilder.group({ + account: new FormControl(undefined), tag: new FormControl(undefined) }); public isLoading = false; @@ -136,6 +138,7 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit { public ngOnInit() { const { tags } = this.dataService.fetchInfo(); + this.accounts = this.user?.accounts; this.tags = tags.map(({ id, name }) => { return { id, @@ -143,15 +146,19 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit { }; }); - this.filterForm - .get('tag') - .valueChanges.pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((tagId) => { - const tag = this.tags.find(({ id }) => { - return id === tagId; - }); - - this.selectedTagChanged.emit(tag); + this.filterForm.valueChanges + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ account, tag }) => { + this.filtersChanged.emit([ + { + id: account, + type: 'ACCOUNT' + }, + { + id: tag, + type: 'TAG' + } + ]); this.onCloseAssistant(); }); @@ -200,6 +207,7 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit { this.filterForm.setValue( { + account: this.user?.settings?.['filters.accounts']?.[0] ?? null, tag: this.user?.settings?.['filters.tags']?.[0] ?? null }, { diff --git a/libs/ui/src/lib/assistant/assistant.html b/libs/ui/src/lib/assistant/assistant.html index 551a19b43..560705c0f 100644 --- a/libs/ui/src/lib/assistant/assistant.html +++ b/libs/ui/src/lib/assistant/assistant.html @@ -99,7 +99,9 @@ > Date Range @@ -118,7 +120,30 @@ Accounts +
+ + No account + @for (account of accounts; track account.id) { + {{ account.name }} + } + +
+
+ + Tags diff --git a/libs/ui/src/lib/assistant/assistant.scss b/libs/ui/src/lib/assistant/assistant.scss index 178d68daa..a2b6f1fa2 100644 --- a/libs/ui/src/lib/assistant/assistant.scss +++ b/libs/ui/src/lib/assistant/assistant.scss @@ -2,6 +2,10 @@ display: block; .filter-container { + .mat-mdc-tab-group { + max-height: 40vh; + } + ::ng-deep { label { margin-bottom: 0;