Browse Source

Feature/add support to manage tags in create or edit activity dialog (#1532)

* Add support to manage tags

* Update changelog
pull/1533/head^2
Thomas Kaul 2 years ago
committed by GitHub
parent
commit
49ce4803ce
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 6
      apps/api/src/app/order/order.service.ts
  3. 51
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  4. 32
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
  5. 5
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.scss

1
CHANGELOG.md

@ -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

6
apps/api/src/app/order/order.service.ts

@ -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,

51
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

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

32
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html

@ -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>

5
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.scss

@ -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…
Cancel
Save