Feature/extend assistant by account selector (#2929)
* Add account selector to assistant * Update changelog
This commit is contained in:
parent
3df8810412
commit
f3ee99fb2b
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Extended the assistant by an account selector (experimental)
|
||||||
- Added support to grant private access with permissions (experimental)
|
- Added support to grant private access with permissions (experimental)
|
||||||
- Added `permissions` to the `Access` model
|
- Added `permissions` to the `Access` model
|
||||||
|
|
||||||
|
@ -38,6 +38,10 @@ export class UpdateUserSettingDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
emergencyFund?: number;
|
emergencyFund?: number;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@IsOptional()
|
||||||
|
'filters.accounts'?: string[];
|
||||||
|
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
'filters.tags'?: string[];
|
'filters.tags'?: string[];
|
||||||
|
@ -141,7 +141,7 @@
|
|||||||
[user]="user"
|
[user]="user"
|
||||||
(closed)="closeAssistant()"
|
(closed)="closeAssistant()"
|
||||||
(dateRangeChanged)="onDateRangeChange($event)"
|
(dateRangeChanged)="onDateRangeChange($event)"
|
||||||
(selectedTagChanged)="onSelectedTagChanged($event)"
|
(filtersChanged)="onFiltersChanged($event)"
|
||||||
/>
|
/>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</li>
|
</li>
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { MatMenuTrigger } from '@angular/material/menu';
|
import { MatMenuTrigger } from '@angular/material/menu';
|
||||||
import { Router } from '@angular/router';
|
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 { 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 { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
@ -20,11 +21,10 @@ import {
|
|||||||
} from '@ghostfolio/client/services/settings-storage.service';
|
} from '@ghostfolio/client/services/settings-storage.service';
|
||||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.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 { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { DateRange } from '@ghostfolio/common/types';
|
import { DateRange } from '@ghostfolio/common/types';
|
||||||
import { AssistantComponent } from '@ghostfolio/ui/assistant/assistant.component';
|
import { AssistantComponent } from '@ghostfolio/ui/assistant/assistant.component';
|
||||||
import { Tag } from '@prisma/client';
|
|
||||||
import { EMPTY, Subject } from 'rxjs';
|
import { EMPTY, Subject } from 'rxjs';
|
||||||
import { catchError, takeUntil } from 'rxjs/operators';
|
import { catchError, takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@ -162,6 +162,34 @@ export class HeaderComponent implements OnChanges {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onFiltersChanged(filters: Filter[]) {
|
||||||
|
const userSetting: UpdateUserSettingDto = {};
|
||||||
|
|
||||||
|
for (const filter of filters) {
|
||||||
|
let filtersType: string;
|
||||||
|
|
||||||
|
if (filter.type === 'ACCOUNT') {
|
||||||
|
filtersType = 'accounts';
|
||||||
|
} else if (filter.type === 'TAG') {
|
||||||
|
filtersType = 'tags';
|
||||||
|
}
|
||||||
|
|
||||||
|
userSetting[`filters.${filtersType}`] = filter.id ? [filter.id] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataService
|
||||||
|
.putUserSetting(userSetting)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.userService.remove();
|
||||||
|
|
||||||
|
this.userService
|
||||||
|
.get()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public onMenuClosed() {
|
public onMenuClosed() {
|
||||||
this.isMenuOpen = false;
|
this.isMenuOpen = false;
|
||||||
}
|
}
|
||||||
@ -174,20 +202,6 @@ export class HeaderComponent implements OnChanges {
|
|||||||
this.assistantElement.initialize();
|
this.assistantElement.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSelectedTagChanged(tag: Tag) {
|
|
||||||
this.dataService
|
|
||||||
.putUserSetting({ 'filters.tags': tag ? [tag.id] : null })
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(() => {
|
|
||||||
this.userService.remove();
|
|
||||||
|
|
||||||
this.userService
|
|
||||||
.get()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public onSignOut() {
|
public onSignOut() {
|
||||||
this.signOut.next();
|
this.signOut.next();
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ import { ImpersonationStorageService } from '@ghostfolio/client/services/imperso
|
|||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
|
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
|
||||||
import { downloadAsFile } from '@ghostfolio/common/helper';
|
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 { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { DataSource, Order as OrderModel } from '@prisma/client';
|
import { DataSource, Order as OrderModel } from '@prisma/client';
|
||||||
import { format, parseISO } from 'date-fns';
|
import { format, parseISO } from 'date-fns';
|
||||||
|
@ -47,18 +47,26 @@ export class UserService extends ObservableStore<UserStoreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getFilters() {
|
public getFilters() {
|
||||||
|
const filters: Filter[] = [];
|
||||||
const user = this.getState().user;
|
const user = this.getState().user;
|
||||||
|
|
||||||
return user?.settings?.isExperimentalFeatures === true
|
if (user?.settings?.isExperimentalFeatures === true) {
|
||||||
? user.settings['filters.tags']
|
if (user.settings['filters.accounts']) {
|
||||||
? <Filter[]>[
|
filters.push({
|
||||||
{
|
id: user.settings['filters.accounts'][0],
|
||||||
id: user.settings['filters.tags'][0],
|
type: 'ACCOUNT'
|
||||||
type: 'TAG'
|
});
|
||||||
}
|
}
|
||||||
]
|
|
||||||
: []
|
if (user.settings['filters.tags']) {
|
||||||
: [];
|
filters.push({
|
||||||
|
id: user.settings['filters.tags'][0],
|
||||||
|
type: 'TAG'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
public remove() {
|
public remove() {
|
||||||
|
@ -7,6 +7,7 @@ export interface UserSettings {
|
|||||||
colorScheme?: ColorScheme;
|
colorScheme?: ColorScheme;
|
||||||
dateRange?: DateRange;
|
dateRange?: DateRange;
|
||||||
emergencyFund?: number;
|
emergencyFund?: number;
|
||||||
|
'filters.accounts'?: string[];
|
||||||
'filters.tags'?: string[];
|
'filters.tags'?: string[];
|
||||||
isExperimentalFeatures?: boolean;
|
isExperimentalFeatures?: boolean;
|
||||||
isRestrictedView?: boolean;
|
isRestrictedView?: boolean;
|
||||||
|
@ -19,10 +19,10 @@ import { FormBuilder, 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 { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.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 { DateRange } from '@ghostfolio/common/types';
|
||||||
import { translate } from '@ghostfolio/ui/i18n';
|
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 { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
catchError,
|
catchError,
|
||||||
@ -81,7 +81,7 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
|
|
||||||
@Output() closed = new EventEmitter<void>();
|
@Output() closed = new EventEmitter<void>();
|
||||||
@Output() dateRangeChanged = new EventEmitter<DateRange>();
|
@Output() dateRangeChanged = new EventEmitter<DateRange>();
|
||||||
@Output() selectedTagChanged = new EventEmitter<Tag>();
|
@Output() filtersChanged = new EventEmitter<Filter[]>();
|
||||||
|
|
||||||
@ViewChild('menuTrigger') menuTriggerElement: MatMenuTrigger;
|
@ViewChild('menuTrigger') menuTriggerElement: MatMenuTrigger;
|
||||||
@ViewChild('search', { static: true }) searchElement: ElementRef;
|
@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 static readonly SEARCH_RESULTS_DEFAULT_LIMIT = 5;
|
||||||
|
|
||||||
|
public accounts: Account[] = [];
|
||||||
public dateRangeFormControl = new FormControl<string>(undefined);
|
public dateRangeFormControl = new FormControl<string>(undefined);
|
||||||
public readonly dateRangeOptions = [
|
public readonly dateRangeOptions = [
|
||||||
{ label: $localize`Today`, value: '1d' },
|
{ label: $localize`Today`, value: '1d' },
|
||||||
@ -111,6 +112,7 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
{ label: $localize`Max`, value: 'max' }
|
{ label: $localize`Max`, value: 'max' }
|
||||||
];
|
];
|
||||||
public filterForm = this.formBuilder.group({
|
public filterForm = this.formBuilder.group({
|
||||||
|
account: new FormControl<string>(undefined),
|
||||||
tag: new FormControl<string>(undefined)
|
tag: new FormControl<string>(undefined)
|
||||||
});
|
});
|
||||||
public isLoading = false;
|
public isLoading = false;
|
||||||
@ -136,6 +138,7 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
const { tags } = this.dataService.fetchInfo();
|
const { tags } = this.dataService.fetchInfo();
|
||||||
|
|
||||||
|
this.accounts = this.user?.accounts;
|
||||||
this.tags = tags.map(({ id, name }) => {
|
this.tags = tags.map(({ id, name }) => {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@ -143,15 +146,19 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
this.filterForm
|
this.filterForm.valueChanges
|
||||||
.get('tag')
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.valueChanges.pipe(takeUntil(this.unsubscribeSubject))
|
.subscribe(({ account, tag }) => {
|
||||||
.subscribe((tagId) => {
|
this.filtersChanged.emit([
|
||||||
const tag = this.tags.find(({ id }) => {
|
{
|
||||||
return id === tagId;
|
id: account,
|
||||||
});
|
type: 'ACCOUNT'
|
||||||
|
},
|
||||||
this.selectedTagChanged.emit(tag);
|
{
|
||||||
|
id: tag,
|
||||||
|
type: 'TAG'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
this.onCloseAssistant();
|
this.onCloseAssistant();
|
||||||
});
|
});
|
||||||
@ -200,6 +207,7 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.filterForm.setValue(
|
this.filterForm.setValue(
|
||||||
{
|
{
|
||||||
|
account: this.user?.settings?.['filters.accounts']?.[0] ?? null,
|
||||||
tag: this.user?.settings?.['filters.tags']?.[0] ?? null
|
tag: this.user?.settings?.['filters.tags']?.[0] ?? null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -99,7 +99,9 @@
|
|||||||
>
|
>
|
||||||
<mat-tab>
|
<mat-tab>
|
||||||
<ng-template mat-tab-label
|
<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
|
>Date Range</span
|
||||||
></ng-template
|
></ng-template
|
||||||
>
|
>
|
||||||
@ -118,7 +120,30 @@
|
|||||||
</mat-tab>
|
</mat-tab>
|
||||||
<mat-tab>
|
<mat-tab>
|
||||||
<ng-template mat-tab-label
|
<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
|
>Tags</span
|
||||||
></ng-template
|
></ng-template
|
||||||
>
|
>
|
||||||
|
@ -2,6 +2,10 @@
|
|||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
.filter-container {
|
.filter-container {
|
||||||
|
.mat-mdc-tab-group {
|
||||||
|
max-height: 40vh;
|
||||||
|
}
|
||||||
|
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
label {
|
label {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user