Feature/extend assistant by tag selector (#2838)
* Extend assistant by tag selector * Update changelog
This commit is contained in:
parent
2a4d7bf14f
commit
d7f72819de
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Extended the assistant by a tag selector (experimental)
|
||||||
- Added support to set a _CoinGecko_ Demo API key via environment variable (`API_KEY_COINGECKO_DEMO`)
|
- Added support to set a _CoinGecko_ Demo API key via environment variable (`API_KEY_COINGECKO_DEMO`)
|
||||||
- Added support to set a _CoinGecko_ Pro API key via environment variable (`API_KEY_COINGECKO_PRO`)
|
- Added support to set a _CoinGecko_ Pro API key via environment variable (`API_KEY_COINGECKO_PRO`)
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import type {
|
|||||||
ViewMode
|
ViewMode
|
||||||
} from '@ghostfolio/common/types';
|
} from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
|
IsArray,
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsISO8601,
|
IsISO8601,
|
||||||
IsIn,
|
IsIn,
|
||||||
@ -37,6 +38,10 @@ export class UpdateUserSettingDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
emergencyFund?: number;
|
emergencyFund?: number;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@IsOptional()
|
||||||
|
'filters.tags'?: string[];
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
isExperimentalFeatures?: boolean;
|
isExperimentalFeatures?: boolean;
|
||||||
|
@ -141,6 +141,7 @@
|
|||||||
[user]="user"
|
[user]="user"
|
||||||
(closed)="closeAssistant()"
|
(closed)="closeAssistant()"
|
||||||
(dateRangeChanged)="onDateRangeChange($event)"
|
(dateRangeChanged)="onDateRangeChange($event)"
|
||||||
|
(selectedTagChanged)="onSelectedTagChanged($event)"
|
||||||
/>
|
/>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</li>
|
</li>
|
||||||
|
@ -24,6 +24,7 @@ import { 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';
|
||||||
|
|
||||||
@ -173,6 +174,20 @@ 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();
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<mat-radio-group
|
<mat-radio-group
|
||||||
class="d-block text-nowrap"
|
class="d-block text-nowrap"
|
||||||
[formControl]="option"
|
[formControl]="optionFormControl"
|
||||||
(change)="onValueChange()"
|
(change)="onValueChange()"
|
||||||
>
|
>
|
||||||
<mat-radio-button
|
<mat-radio-button
|
||||||
|
@ -31,17 +31,17 @@ export class ToggleComponent implements OnChanges, OnInit {
|
|||||||
|
|
||||||
@Output() change = new EventEmitter<Pick<ToggleOption, 'value'>>();
|
@Output() change = new EventEmitter<Pick<ToggleOption, 'value'>>();
|
||||||
|
|
||||||
public option = new FormControl<string>(undefined);
|
public optionFormControl = new FormControl<string>(undefined);
|
||||||
|
|
||||||
public constructor() {}
|
public constructor() {}
|
||||||
|
|
||||||
public ngOnInit() {}
|
public ngOnInit() {}
|
||||||
|
|
||||||
public ngOnChanges() {
|
public ngOnChanges() {
|
||||||
this.option.setValue(this.defaultValue);
|
this.optionFormControl.setValue(this.defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onValueChange() {
|
public onValueChange() {
|
||||||
this.change.emit({ value: this.option.value });
|
this.change.emit({ value: this.optionFormControl.value });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 { User } from '@ghostfolio/common/interfaces';
|
import { Filter, 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';
|
||||||
@ -111,6 +111,8 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
|||||||
if (state?.user) {
|
if (state?.user) {
|
||||||
this.updateUser(state.user);
|
this.updateUser(state.user);
|
||||||
|
|
||||||
|
this.fetchActivities();
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -122,6 +124,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
|||||||
if (this.user?.settings?.isExperimentalFeatures === true) {
|
if (this.user?.settings?.isExperimentalFeatures === true) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchActivities({
|
.fetchActivities({
|
||||||
|
filters: this.userService.getFilters(),
|
||||||
skip: this.pageIndex * this.pageSize,
|
skip: this.pageIndex * this.pageSize,
|
||||||
sortColumn: this.sortColumn,
|
sortColumn: this.sortColumn,
|
||||||
sortDirection: this.sortDirection,
|
sortDirection: this.sortDirection,
|
||||||
|
@ -225,7 +225,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
private fetchDividendsAndInvestments() {
|
private fetchDividendsAndInvestments() {
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchDividends({
|
.fetchDividends({
|
||||||
filters: this.activeFilters,
|
filters:
|
||||||
|
this.activeFilters.length > 0
|
||||||
|
? this.activeFilters
|
||||||
|
: this.userService.getFilters(),
|
||||||
groupBy: this.mode,
|
groupBy: this.mode,
|
||||||
range: this.user?.settings?.dateRange
|
range: this.user?.settings?.dateRange
|
||||||
})
|
})
|
||||||
@ -238,7 +241,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchInvestments({
|
.fetchInvestments({
|
||||||
filters: this.activeFilters,
|
filters:
|
||||||
|
this.activeFilters.length > 0
|
||||||
|
? this.activeFilters
|
||||||
|
: this.userService.getFilters(),
|
||||||
groupBy: this.mode,
|
groupBy: this.mode,
|
||||||
range: this.user?.settings?.dateRange
|
range: this.user?.settings?.dateRange
|
||||||
})
|
})
|
||||||
@ -313,7 +319,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPortfolioPerformance({
|
.fetchPortfolioPerformance({
|
||||||
filters: this.activeFilters,
|
filters:
|
||||||
|
this.activeFilters.length > 0
|
||||||
|
? this.activeFilters
|
||||||
|
: this.userService.getFilters(),
|
||||||
range: this.user?.settings?.dateRange
|
range: this.user?.settings?.dateRange
|
||||||
})
|
})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
@ -358,7 +367,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPositions({
|
.fetchPositions({
|
||||||
filters: this.activeFilters,
|
filters:
|
||||||
|
this.activeFilters.length > 0
|
||||||
|
? this.activeFilters
|
||||||
|
: this.userService.getFilters(),
|
||||||
range: this.user?.settings?.dateRange
|
range: this.user?.settings?.dateRange
|
||||||
})
|
})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Analysis</h1>
|
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Analysis</h1>
|
||||||
<div *ngIf="!user?.settings?.isExperimentalFeatures" class="my-4 text-center">
|
@if (!user?.settings?.isExperimentalFeatures) {
|
||||||
|
<div class="my-4 text-center">
|
||||||
<gf-toggle
|
<gf-toggle
|
||||||
[defaultValue]="user?.settings?.dateRange"
|
[defaultValue]="user?.settings?.dateRange"
|
||||||
[isLoading]="isLoadingBenchmarkComparator || isLoadingInvestmentChart"
|
[isLoading]="isLoadingBenchmarkComparator || isLoadingInvestmentChart"
|
||||||
@ -14,6 +15,7 @@
|
|||||||
[placeholder]="placeholder"
|
[placeholder]="placeholder"
|
||||||
(valueChanged)="filters$.next($event)"
|
(valueChanged)="filters$.next($event)"
|
||||||
></gf-activities-filter>
|
></gf-activities-filter>
|
||||||
|
}
|
||||||
<div class="mb-5 row">
|
<div class="mb-5 row">
|
||||||
<div class="col-lg">
|
<div class="col-lg">
|
||||||
<gf-benchmark-comparator
|
<gf-benchmark-comparator
|
||||||
|
@ -4,7 +4,7 @@ import { MatDialog } from '@angular/material/dialog';
|
|||||||
import { ObservableStore } from '@codewithdan/observable-store';
|
import { ObservableStore } from '@codewithdan/observable-store';
|
||||||
import { SubscriptionInterstitialDialogParams } from '@ghostfolio/client/components/subscription-interstitial-dialog/interfaces/interfaces';
|
import { SubscriptionInterstitialDialogParams } from '@ghostfolio/client/components/subscription-interstitial-dialog/interfaces/interfaces';
|
||||||
import { SubscriptionInterstitialDialog } from '@ghostfolio/client/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component';
|
import { SubscriptionInterstitialDialog } from '@ghostfolio/client/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component';
|
||||||
import { User } from '@ghostfolio/common/interfaces';
|
import { Filter, User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
@ -46,6 +46,21 @@ export class UserService extends ObservableStore<UserStoreState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getFilters() {
|
||||||
|
const user = this.getState().user;
|
||||||
|
|
||||||
|
return user?.settings?.isExperimentalFeatures === true
|
||||||
|
? user.settings['filters.tags']
|
||||||
|
? <Filter[]>[
|
||||||
|
{
|
||||||
|
id: user.settings['filters.tags'][0],
|
||||||
|
type: 'TAG'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
public remove() {
|
public remove() {
|
||||||
this.setState({ user: null }, UserStoreActions.RemoveUser);
|
this.setState({ user: null }, UserStoreActions.RemoveUser);
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ export interface UserSettings {
|
|||||||
colorScheme?: ColorScheme;
|
colorScheme?: ColorScheme;
|
||||||
dateRange?: DateRange;
|
dateRange?: DateRange;
|
||||||
emergencyFund?: number;
|
emergencyFund?: number;
|
||||||
|
'filters.tags'?: string[];
|
||||||
isExperimentalFeatures?: boolean;
|
isExperimentalFeatures?: boolean;
|
||||||
isRestrictedView?: boolean;
|
isRestrictedView?: boolean;
|
||||||
language?: string;
|
language?: string;
|
||||||
|
@ -75,7 +75,6 @@ export class ActivitiesTableLazyComponent
|
|||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
public isUUID = isUUID;
|
public isUUID = isUUID;
|
||||||
public routeQueryParams: Subscription;
|
public routeQueryParams: Subscription;
|
||||||
public searchKeywords: string[] = [];
|
|
||||||
public selectedRows = new SelectionModel<Activity>(true, []);
|
public selectedRows = new SelectionModel<Activity>(true, []);
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -182,16 +181,8 @@ export class ActivitiesTableLazyComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onExport() {
|
public onExport() {
|
||||||
if (this.searchKeywords.length > 0) {
|
|
||||||
this.export.emit(
|
|
||||||
this.dataSource.filteredData.map((activity) => {
|
|
||||||
return activity.id;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.export.emit();
|
this.export.emit();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public onExportDraft(aActivityId: string) {
|
public onExportDraft(aActivityId: string) {
|
||||||
this.exportDrafts.emit([aActivityId]);
|
this.exportDrafts.emit([aActivityId]);
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
EventEmitter,
|
EventEmitter,
|
||||||
HostListener,
|
HostListener,
|
||||||
Input,
|
Input,
|
||||||
|
OnChanges,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
Output,
|
Output,
|
||||||
@ -22,6 +23,7 @@ import { DataService } from '@ghostfolio/client/services/data.service';
|
|||||||
import { User } from '@ghostfolio/common/interfaces';
|
import { 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 { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs';
|
import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
catchError,
|
catchError,
|
||||||
@ -41,7 +43,7 @@ import { ISearchResultItem, ISearchResults } from './interfaces/interfaces';
|
|||||||
styleUrls: ['./assistant.scss'],
|
styleUrls: ['./assistant.scss'],
|
||||||
templateUrl: './assistant.html'
|
templateUrl: './assistant.html'
|
||||||
})
|
})
|
||||||
export class AssistantComponent implements OnDestroy, OnInit {
|
export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
|
||||||
@HostListener('document:keydown', ['$event']) onKeydown(
|
@HostListener('document:keydown', ['$event']) onKeydown(
|
||||||
event: KeyboardEvent
|
event: KeyboardEvent
|
||||||
) {
|
) {
|
||||||
@ -80,6 +82,7 @@ export class AssistantComponent implements 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>();
|
||||||
|
|
||||||
@ViewChild('menuTrigger') menuTriggerElement: MatMenuTrigger;
|
@ViewChild('menuTrigger') menuTriggerElement: MatMenuTrigger;
|
||||||
@ViewChild('search', { static: true }) searchElement: ElementRef;
|
@ViewChild('search', { static: true }) searchElement: ElementRef;
|
||||||
@ -98,6 +101,8 @@ export class AssistantComponent implements OnDestroy, OnInit {
|
|||||||
assetProfiles: [],
|
assetProfiles: [],
|
||||||
holdings: []
|
holdings: []
|
||||||
};
|
};
|
||||||
|
public tags: Tag[] = [];
|
||||||
|
public tagsFormControl = new FormControl<string>(undefined);
|
||||||
|
|
||||||
private keyManager: FocusKeyManager<AssistantListItemComponent>;
|
private keyManager: FocusKeyManager<AssistantListItemComponent>;
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -109,6 +114,15 @@ export class AssistantComponent implements OnDestroy, OnInit {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
|
const { tags } = this.dataService.fetchInfo();
|
||||||
|
|
||||||
|
this.tags = tags.map(({ id, name }) => {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: translate(name)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
this.searchFormControl.valueChanges
|
this.searchFormControl.valueChanges
|
||||||
.pipe(
|
.pipe(
|
||||||
map((searchTerm) => {
|
map((searchTerm) => {
|
||||||
@ -148,6 +162,12 @@ export class AssistantComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ngOnChanges() {
|
||||||
|
this.tagsFormControl.setValue(
|
||||||
|
this.user?.settings?.['filters.tags']?.[0] ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public async initialize() {
|
public async initialize() {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.keyManager = new FocusKeyManager(this.assistantListItems).withWrap();
|
this.keyManager = new FocusKeyManager(this.assistantListItems).withWrap();
|
||||||
@ -181,6 +201,16 @@ export class AssistantComponent implements OnDestroy, OnInit {
|
|||||||
this.closed.emit();
|
this.closed.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onTagChange() {
|
||||||
|
const selectedTag = this.tags.find(({ id }) => {
|
||||||
|
return id === this.tagsFormControl.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.selectedTagChanged.emit(selectedTag);
|
||||||
|
|
||||||
|
this.onCloseAssistant();
|
||||||
|
}
|
||||||
|
|
||||||
public setIsOpen(aIsOpen: boolean) {
|
public setIsOpen(aIsOpen: boolean) {
|
||||||
this.isOpen = aIsOpen;
|
this.isOpen = aIsOpen;
|
||||||
}
|
}
|
||||||
|
@ -88,8 +88,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
*ngIf="!(isLoading || searchFormControl.value) && user?.settings?.isExperimentalFeatures"
|
*ngIf="!(isLoading || searchFormControl.value) && user?.settings?.isExperimentalFeatures"
|
||||||
|
class="filter-container"
|
||||||
|
>
|
||||||
|
<mat-tab-group
|
||||||
|
animationDuration="0"
|
||||||
|
mat-align-tabs="start"
|
||||||
|
[mat-stretch-tabs]="false"
|
||||||
|
(click)="$event.stopPropagation();"
|
||||||
>
|
>
|
||||||
<mat-tab-group mat-align-tabs="start" [mat-stretch-tabs]="false">
|
|
||||||
<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 class="mr-2" name="calendar-clear-outline" /><span i18n
|
||||||
@ -104,5 +110,28 @@
|
|||||||
></gf-toggle>
|
></gf-toggle>
|
||||||
</div>
|
</div>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
|
<mat-tab>
|
||||||
|
<ng-template mat-tab-label
|
||||||
|
><ion-icon class="mr-2" name="pricetag-outline" /><span i18n
|
||||||
|
>Tags</span
|
||||||
|
></ng-template
|
||||||
|
>
|
||||||
|
<div class="p-3">
|
||||||
|
<mat-radio-group
|
||||||
|
color="primary"
|
||||||
|
[formControl]="tagsFormControl"
|
||||||
|
(change)="onTagChange()"
|
||||||
|
>
|
||||||
|
<mat-radio-button class="d-flex flex-column" i18n [value]="null"
|
||||||
|
>No tag</mat-radio-button
|
||||||
|
>
|
||||||
|
@for (tag of tags; track tag.id) {
|
||||||
|
<mat-radio-button class="d-flex flex-column" [value]="tag.id"
|
||||||
|
>{{ tag.name }}</mat-radio-button
|
||||||
|
>
|
||||||
|
}
|
||||||
|
</mat-radio-group>
|
||||||
|
</div>
|
||||||
|
</mat-tab>
|
||||||
</mat-tab-group>
|
</mat-tab-group>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatRadioModule } from '@angular/material/radio';
|
||||||
import { MatTabsModule } from '@angular/material/tabs';
|
import { MatTabsModule } from '@angular/material/tabs';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
||||||
@ -19,6 +20,7 @@ import { AssistantComponent } from './assistant.component';
|
|||||||
GfAssistantListItemModule,
|
GfAssistantListItemModule,
|
||||||
GfToggleModule,
|
GfToggleModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatRadioModule,
|
||||||
MatTabsModule,
|
MatTabsModule,
|
||||||
NgxSkeletonLoaderModule,
|
NgxSkeletonLoaderModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
.filter-container {
|
||||||
|
::ng-deep {
|
||||||
|
label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.result-container {
|
.result-container {
|
||||||
max-height: 15rem;
|
max-height: 15rem;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user