diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts index 323796bef..b1d8a9dc8 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts @@ -10,20 +10,17 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - ElementRef, Inject, - OnDestroy, - ViewChild + OnDestroy } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { AssetClass, AssetSubClass, Tag, Type } from '@prisma/client'; import { isUUID } from 'class-validator'; import { isAfter, isToday } from 'date-fns'; -import { EMPTY, Observable, Subject, lastValueFrom, of } from 'rxjs'; -import { catchError, delay, map, startWith, takeUntil } from 'rxjs/operators'; +import { EMPTY, Subject, lastValueFrom, of } from 'rxjs'; +import { catchError, delay, takeUntil } from 'rxjs/operators'; import { CreateOrUpdateActivityDialogParams } from './interfaces/interfaces'; @@ -35,9 +32,6 @@ import { CreateOrUpdateActivityDialogParams } from './interfaces/interfaces'; templateUrl: 'create-or-update-activity-dialog.html' }) export class CreateOrUpdateActivityDialog implements OnDestroy { - @ViewChild('symbolAutocomplete') symbolAutocomplete; - @ViewChild('tagInput') tagInput: ElementRef; - public activityForm: FormGroup; public assetClasses = Object.keys(AssetClass).map((assetClass) => { return { id: assetClass, label: translate(assetClass) }; @@ -48,7 +42,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { public currencies: string[] = []; public currentMarketPrice = null; public defaultDateFormat: string; - public filteredTagsObservable: Observable = of([]); public isLoading = false; public isToday = isToday; public mode: 'create' | 'update'; @@ -147,6 +140,12 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { updateAccountBalance: [false] }); + console.log( + 'Initialized Activity Form: ', + this.activityForm.get('tags').value + ); + console.log('Initialized Activity Tags Available: ', this.tagsAvailable); + this.activityForm.valueChanges .pipe( // Slightly delay until the more specific form control value changes have @@ -282,15 +281,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { this.changeDetectorRef.markForCheck(); }); - this.filteredTagsObservable = this.activityForm.controls[ - 'tags' - ].valueChanges.pipe( - startWith(this.activityForm.get('tags').value), - map((aTags: Tag[] | null) => { - return aTags ? this.filterTags(aTags) : this.tagsAvailable.slice(); - }) - ); - this.activityForm .get('type') .valueChanges.pipe(takeUntil(this.unsubscribeSubject)) @@ -438,27 +428,12 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { return isAfter(aDate, new Date(0)); } - public onAddTag(event: MatAutocompleteSelectedEvent) { - this.activityForm.get('tags').setValue([ - ...(this.activityForm.get('tags').value ?? []), - this.tagsAvailable.find(({ id }) => { - return id === event.option.value; - }) - ]); - - this.tagInput.nativeElement.value = ''; - } - public onCancel() { this.dialogRef.close(); } - public onRemoveTag(aTag: Tag) { - this.activityForm.get('tags').setValue( - this.activityForm.get('tags').value.filter(({ id }) => { - return id !== aTag.id; - }) - ); + public onTagsChanged(event: Tag[]) { + this.activityForm.get('tags').setValue([...event]); } public async onSubmit() { @@ -518,16 +493,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { this.unsubscribeSubject.complete(); } - private filterTags(aTags: Tag[]) { - const tagIds = aTags.map(({ id }) => { - return id; - }); - - return this.tagsAvailable.filter(({ id }) => { - return !tagIds.includes(id); - }); - } - private updateSymbol() { this.isLoading = true; this.changeDetectorRef.markForCheck(); diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html index 7795688c0..85fcf5a94 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -379,38 +379,11 @@
- - Tags - - @for (tag of activityForm.get('tags')?.value; track tag.id) { - - {{ tag.name }} - - - } - - - - @for (tag of filteredTagsObservable | async; track tag.id) { - - {{ tag.name }} - - } - - +
diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.module.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.module.ts index a4d28d0e0..ea6106a33 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.module.ts +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.module.ts @@ -1,5 +1,6 @@ import { GfAssetProfileIconComponent } from '@ghostfolio/client/components/asset-profile-icon/asset-profile-icon.component'; import { GfSymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete'; +import { GfTagsSelectorComponent } from '@ghostfolio/ui/tags-selector'; import { GfValueComponent } from '@ghostfolio/ui/value'; import { CommonModule } from '@angular/common'; @@ -24,6 +25,7 @@ import { CreateOrUpdateActivityDialog } from './create-or-update-activity-dialog FormsModule, GfAssetProfileIconComponent, GfSymbolAutocompleteComponent, + GfTagsSelectorComponent, GfValueComponent, MatAutocompleteModule, MatButtonModule, diff --git a/libs/ui/src/lib/tags-selector/index.ts b/libs/ui/src/lib/tags-selector/index.ts new file mode 100644 index 000000000..360bce671 --- /dev/null +++ b/libs/ui/src/lib/tags-selector/index.ts @@ -0,0 +1 @@ +export * from './tags-selector.component'; diff --git a/libs/ui/src/lib/tags-selector/tags-selector.component.html b/libs/ui/src/lib/tags-selector/tags-selector.component.html new file mode 100644 index 000000000..2b60801e6 --- /dev/null +++ b/libs/ui/src/lib/tags-selector/tags-selector.component.html @@ -0,0 +1,34 @@ + + Tags + + @for (tag of fruits(); track tag.id) { + + {{ tag.name }} + + + } + + + + + @for (tag of filteredFruits(); track tag.id) { + + {{ tag.name }} + + } + + diff --git a/libs/ui/src/lib/tags-selector/tags-selector.component.scss b/libs/ui/src/lib/tags-selector/tags-selector.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/libs/ui/src/lib/tags-selector/tags-selector.component.ts b/libs/ui/src/lib/tags-selector/tags-selector.component.ts new file mode 100644 index 000000000..9881d3de3 --- /dev/null +++ b/libs/ui/src/lib/tags-selector/tags-selector.component.ts @@ -0,0 +1,157 @@ +// import { CreateOrUpdateActivityDialogParams } from './interfaces/interfaces'; +import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; + +import { FocusMonitor } from '@angular/cdk/a11y'; +import { COMMA, ENTER } from '@angular/cdk/keycodes'; +import { CommonModule } from '@angular/common'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + computed, + CUSTOM_ELEMENTS_SCHEMA, + effect, + ElementRef, + EventEmitter, + Input, + model, + OnInit, + Output, + signal, + ViewChild +} from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { + MatAutocompleteTrigger, + MatAutocompleteModule, + MatAutocompleteSelectedEvent +} from '@angular/material/autocomplete'; +import { + MatChipEditedEvent, + MatChipInputEvent, + MatChipsModule +} from '@angular/material/chips'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { + MatFormFieldControl, + MatFormFieldModule +} from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInput, MatInputModule } from '@angular/material/input'; +import { Tag } from '@prisma/client'; +import { map, Observable, of, startWith, Subject } from 'rxjs'; + +import { translate } from '../i18n'; +import { AbstractMatFormField } from '../shared/abstract-mat-form-field'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + // host: { + // '[attr.aria-describedBy]': 'describedBy', + // '[id]': 'id' + // }, + imports: [ + CommonModule, + FormsModule, + MatAutocompleteModule, + MatFormFieldModule, + MatInputModule, + MatChipsModule, + MatIconModule, + ReactiveFormsModule + ], + // providers: [ + // { + // provide: MatFormFieldControl, + // useExisting: GfTagsSelectorComponent + // } + // ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + selector: 'gf-tags-selector', + standalone: true, + styleUrls: ['./tags-selector.component.scss'], + templateUrl: 'tags-selector.component.html' +}) +export class GfTagsSelectorComponent implements OnInit { + public focus(): void { + throw new Error('Method not implemented.'); + } + @Input() tags: Tag[]; + @Input() tagsAvailable: Tag[]; + + @Output() tagsChanged = new EventEmitter(); + + @ViewChild('tagInput') tagInput: ElementRef; + + // public activityForm: FormGroup; + public filteredTagsObservable: Observable = of([]); + public separatorKeysCodes: number[] = [COMMA, ENTER]; + + readonly fruits = signal([{ id: '', name: '' }]); + + readonly currentFruit = model(''); + + readonly filteredFruits = computed(() => { + const currentFruit = this.currentFruit().toLowerCase(); + const aTags = this.tagsAvailable + ? this.tagsAvailable + : [{ id: '', name: '' }]; + return currentFruit + ? aTags.filter((tag) => tag.name.toLowerCase().includes(currentFruit)) + : this.tagsAvailable; + }); + + public constructor() { + effect(() => { + if (this.fruits()) { + console.log('Emit Fruits: ', this.fruits()); + this.tagsChanged.emit(this.fruits()); + } + }); + } + + ngOnInit() { + this.fruits.set(this.tags); + + console.log('Tags Available : ', this.tagsAvailable); + console.log('Tags : ', this.tags); + } + + public onAddTag(event: MatAutocompleteSelectedEvent) { + if ( + this.fruits() && + this.fruits().some((el) => el.id === event.option.value) + ) { + this.currentFruit.set(''); + event.option.deselect(); + return; + } + + this.fruits() + ? this.fruits.update((fruits) => [ + ...fruits, + this.tagsAvailable.find(({ id }) => { + return id === event.option.value; + }) + ]) + : this.fruits.update(() => [ + this.tagsAvailable.find(({ id }) => { + return id === event.option.value; + }) + ]); + this.currentFruit.set(''); + event.option.deselect(); + } + + public onRemoveTag(aTag: Tag) { + this.fruits.update((fruits) => { + const index = fruits.indexOf(aTag); + if (index < 0) { + return fruits; + } + + fruits.splice(index, 1); + return [...fruits]; + }); + } +}