Feature/add support to manage tags in create or edit activity dialog (#1532)
* Add support to manage tags * Update changelog
This commit is contained in:
parent
0b65d05013
commit
49ce4803ce
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Added support to manage the tags in the create or edit activity dialog
|
||||||
- Added the tags to the admin control panel
|
- Added the tags to the admin control panel
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
@ -362,6 +362,12 @@ export class OrderService {
|
|||||||
delete data.symbol;
|
delete data.symbol;
|
||||||
delete data.tags;
|
delete data.tags;
|
||||||
|
|
||||||
|
// Remove existing tags
|
||||||
|
await this.prismaService.order.update({
|
||||||
|
data: { tags: { set: [] } },
|
||||||
|
where
|
||||||
|
});
|
||||||
|
|
||||||
return this.prismaService.order.update({
|
return this.prismaService.order.update({
|
||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
|
import { COMMA, ENTER } from '@angular/cdk/keycodes';
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
|
ElementRef,
|
||||||
Inject,
|
Inject,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
ViewChild
|
ViewChild
|
||||||
@ -15,7 +17,7 @@ import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
|||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { translate } from '@ghostfolio/ui/i18n';
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
import { AssetClass, AssetSubClass, Type } from '@prisma/client';
|
import { AssetClass, AssetSubClass, Tag, Type } from '@prisma/client';
|
||||||
import { isUUID } from 'class-validator';
|
import { isUUID } from 'class-validator';
|
||||||
import { isString } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs';
|
import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs';
|
||||||
@ -23,6 +25,7 @@ import {
|
|||||||
catchError,
|
catchError,
|
||||||
debounceTime,
|
debounceTime,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
|
map,
|
||||||
startWith,
|
startWith,
|
||||||
switchMap,
|
switchMap,
|
||||||
takeUntil
|
takeUntil
|
||||||
@ -39,6 +42,7 @@ import { CreateOrUpdateActivityDialogParams } from './interfaces/interfaces';
|
|||||||
})
|
})
|
||||||
export class CreateOrUpdateActivityDialog implements OnDestroy {
|
export class CreateOrUpdateActivityDialog implements OnDestroy {
|
||||||
@ViewChild('autocomplete') autocomplete;
|
@ViewChild('autocomplete') autocomplete;
|
||||||
|
@ViewChild('tagInput') tagInput: ElementRef<HTMLInputElement>;
|
||||||
|
|
||||||
public activityForm: FormGroup;
|
public activityForm: FormGroup;
|
||||||
public assetClasses = Object.keys(AssetClass).map((assetClass) => {
|
public assetClasses = Object.keys(AssetClass).map((assetClass) => {
|
||||||
@ -51,8 +55,11 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
public currentMarketPrice = null;
|
public currentMarketPrice = null;
|
||||||
public filteredLookupItems: LookupItem[];
|
public filteredLookupItems: LookupItem[];
|
||||||
public filteredLookupItemsObservable: Observable<LookupItem[]>;
|
public filteredLookupItemsObservable: Observable<LookupItem[]>;
|
||||||
|
public filteredTagsObservable: Observable<Tag[]>;
|
||||||
public isLoading = false;
|
public isLoading = false;
|
||||||
public platforms: { id: string; name: string }[];
|
public platforms: { id: string; name: string }[];
|
||||||
|
public separatorKeysCodes: number[] = [ENTER, COMMA];
|
||||||
|
public tags: Tag[] = [];
|
||||||
public total = 0;
|
public total = 0;
|
||||||
public Validators = Validators;
|
public Validators = Validators;
|
||||||
|
|
||||||
@ -72,10 +79,11 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
this.locale = this.data.user?.settings?.locale;
|
this.locale = this.data.user?.settings?.locale;
|
||||||
this.dateAdapter.setLocale(this.locale);
|
this.dateAdapter.setLocale(this.locale);
|
||||||
|
|
||||||
const { currencies, platforms } = this.dataService.fetchInfo();
|
const { currencies, platforms, tags } = this.dataService.fetchInfo();
|
||||||
|
|
||||||
this.currencies = currencies;
|
this.currencies = currencies;
|
||||||
this.platforms = platforms;
|
this.platforms = platforms;
|
||||||
|
this.tags = tags;
|
||||||
|
|
||||||
this.activityForm = this.formBuilder.group({
|
this.activityForm = this.formBuilder.group({
|
||||||
accountId: [this.data.activity?.accountId, Validators.required],
|
accountId: [this.data.activity?.accountId, Validators.required],
|
||||||
@ -185,6 +193,15 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.filteredTagsObservable = this.activityForm.controls[
|
||||||
|
'tags'
|
||||||
|
].valueChanges.pipe(
|
||||||
|
startWith(this.activityForm.controls['tags'].value),
|
||||||
|
map((aTags: Tag[] | null) => {
|
||||||
|
return aTags ? this.filterTags(aTags) : this.tags.slice();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
this.activityForm.controls['type'].valueChanges
|
this.activityForm.controls['type'].valueChanges
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((type: Type) => {
|
.subscribe((type: Type) => {
|
||||||
@ -264,6 +281,16 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
return aLookupItem?.symbol ?? '';
|
return aLookupItem?.symbol ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onAddTag(event: MatAutocompleteSelectedEvent) {
|
||||||
|
this.activityForm.controls['tags'].setValue([
|
||||||
|
...(this.activityForm.controls['tags'].value ?? []),
|
||||||
|
this.tags.find(({ id }) => {
|
||||||
|
return id === event.option.value;
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
this.tagInput.nativeElement.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
public onBlurSymbol() {
|
public onBlurSymbol() {
|
||||||
const currentLookupItem = this.filteredLookupItems.find((lookupItem) => {
|
const currentLookupItem = this.filteredLookupItems.find((lookupItem) => {
|
||||||
return (
|
return (
|
||||||
@ -283,10 +310,18 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onCancel(): void {
|
public onCancel() {
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onRemoveTag(aTag: Tag) {
|
||||||
|
this.activityForm.controls['tags'].setValue(
|
||||||
|
this.activityForm.controls['tags'].value.filter(({ id }) => {
|
||||||
|
return id !== aTag.id;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public onSubmit() {
|
public onSubmit() {
|
||||||
const activity: CreateOrderDto | UpdateOrderDto = {
|
const activity: CreateOrderDto | UpdateOrderDto = {
|
||||||
accountId: this.activityForm.controls['accountId'].value,
|
accountId: this.activityForm.controls['accountId'].value,
|
||||||
@ -327,6 +362,16 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private filterTags(aTags: Tag[]) {
|
||||||
|
const tagIds = aTags.map((tag) => {
|
||||||
|
return tag.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.tags.filter((tag) => {
|
||||||
|
return !tagIds.includes(tag.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private updateSymbol(symbol: string) {
|
private updateSymbol(symbol: string) {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
|
@ -194,16 +194,38 @@
|
|||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div [ngClass]="{ 'd-none': tags?.length <= 0 }">
|
||||||
[ngClass]="{ 'd-none': activityForm.controls['tags']?.value?.length <= 0 }"
|
|
||||||
>
|
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Tags</mat-label>
|
<mat-label i18n>Tags</mat-label>
|
||||||
<mat-chip-list>
|
<mat-chip-list #tagsChipList>
|
||||||
<mat-chip *ngFor="let tag of activityForm.controls['tags']?.value">
|
<mat-chip
|
||||||
|
*ngFor="let tag of activityForm.controls['tags']?.value"
|
||||||
|
matChipRemove
|
||||||
|
[removable]="true"
|
||||||
|
(removed)="onRemoveTag(tag)"
|
||||||
|
>
|
||||||
{{ tag.name }}
|
{{ tag.name }}
|
||||||
|
<ion-icon class="ml-2" matPrefix name="close-outline"></ion-icon>
|
||||||
</mat-chip>
|
</mat-chip>
|
||||||
|
<input
|
||||||
|
#tagInput
|
||||||
|
name="close-outline"
|
||||||
|
[matAutocomplete]="autocompleteTags"
|
||||||
|
[matChipInputFor]="tagsChipList"
|
||||||
|
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||||
|
/>
|
||||||
</mat-chip-list>
|
</mat-chip-list>
|
||||||
|
<mat-autocomplete
|
||||||
|
#autocompleteTags="matAutocomplete"
|
||||||
|
(optionSelected)="onAddTag($event)"
|
||||||
|
>
|
||||||
|
<mat-option
|
||||||
|
*ngFor="let tag of filteredTagsObservable | async"
|
||||||
|
[value]="tag.id"
|
||||||
|
>
|
||||||
|
{{ tag.name }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-autocomplete>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,6 +20,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mat-chip {
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 1.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
.mat-form-field-appearance-outline {
|
.mat-form-field-appearance-outline {
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
.mat-form-field-suffix {
|
.mat-form-field-suffix {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user