Browse Source

Feature/extend assistant by account selector (#2929)

* Add account selector to assistant

* Update changelog
pull/2930/head
Thomas Kaul 8 months ago
committed by GitHub
parent
commit
f3ee99fb2b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 4
      apps/api/src/app/user/update-user-setting.dto.ts
  3. 2
      apps/client/src/app/components/header/header.component.html
  4. 40
      apps/client/src/app/components/header/header.component.ts
  5. 2
      apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
  6. 28
      apps/client/src/app/services/user/user.service.ts
  7. 1
      libs/common/src/lib/interfaces/user-settings.interface.ts
  8. 32
      libs/ui/src/lib/assistant/assistant.component.ts
  9. 29
      libs/ui/src/lib/assistant/assistant.html
  10. 4
      libs/ui/src/lib/assistant/assistant.scss

1
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

4
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[];

2
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)"
/>
</mat-menu>
</li>

40
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();
}

2
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';

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

@ -47,18 +47,26 @@ export class UserService extends ObservableStore<UserStoreState> {
}
public getFilters() {
const filters: Filter[] = [];
const user = this.getState().user;
return user?.settings?.isExperimentalFeatures === true
? user.settings['filters.tags']
? <Filter[]>[
{
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() {

1
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;

32
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<void>();
@Output() dateRangeChanged = new EventEmitter<DateRange>();
@Output() selectedTagChanged = new EventEmitter<Tag>();
@Output() filtersChanged = new EventEmitter<Filter[]>();
@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<string>(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<string>(undefined),
tag: new FormControl<string>(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
},
{

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

@ -99,7 +99,9 @@
>
<mat-tab>
<ng-template mat-tab-label
><ion-icon class="mr-2" name="calendar-clear-outline" /><span i18n
><ion-icon name="calendar-clear-outline" /><span
class="d-none d-sm-block ml-2"
i18n
>Date Range</span
></ng-template
>
@ -118,7 +120,30 @@
</mat-tab>
<mat-tab>
<ng-template mat-tab-label
><ion-icon class="mr-2" name="pricetag-outline" /><span i18n
><ion-icon name="albums-outline" /><span
class="d-none d-sm-block ml-2"
i18n
>Accounts</span
></ng-template
>
<div class="p-3">
<mat-radio-group color="primary" formControlName="account">
<mat-radio-button class="d-flex flex-column" i18n [value]="null"
>No account</mat-radio-button
>
@for (account of accounts; track account.id) {
<mat-radio-button class="d-flex flex-column" [value]="account.id"
>{{ account.name }}</mat-radio-button
>
}
</mat-radio-group>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label
><ion-icon name="pricetag-outline" /><span
class="d-none d-sm-block ml-2"
i18n
>Tags</span
></ng-template
>

4
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;

Loading…
Cancel
Save