Browse Source

Update Tags Selector and GfHoldingDetailDialogComponent

pull/3708/head
Daniel Idem 1 year ago
parent
commit
19184bb382
  1. 60
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
  2. 37
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
  3. 6
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  4. 6
      libs/ui/src/lib/tags-selector/tags-selector.component.html
  5. 107
      libs/ui/src/lib/tags-selector/tags-selector.component.ts

60
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts

@ -17,6 +17,7 @@ import { GfDataProviderCreditsComponent } from '@ghostfolio/ui/data-provider-cre
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart'; import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart'; import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart';
import { GfTagsSelectorComponent } from '@ghostfolio/ui/tags-selector';
import { GfValueComponent } from '@ghostfolio/ui/value'; import { GfValueComponent } from '@ghostfolio/ui/value';
import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { COMMA, ENTER } from '@angular/cdk/keycodes';
@ -26,19 +27,13 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef,
Inject, Inject,
OnDestroy, OnDestroy,
OnInit, OnInit
ViewChild
} from '@angular/core'; } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { import { MatAutocompleteModule } from '@angular/material/autocomplete';
MatAutocompleteModule,
MatAutocompleteSelectedEvent
} from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips';
import { import {
MAT_DIALOG_DATA, MAT_DIALOG_DATA,
MatDialogModule, MatDialogModule,
@ -52,8 +47,8 @@ import { Router } from '@angular/router';
import { Account, Tag } from '@prisma/client'; import { Account, Tag } from '@prisma/client';
import { format, isSameMonth, isToday, parseISO } from 'date-fns'; import { format, isSameMonth, isToday, parseISO } from 'date-fns';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Observable, of, Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { map, startWith, takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { HoldingDetailDialogParams } from './interfaces/interfaces'; import { HoldingDetailDialogParams } from './interfaces/interfaces';
@ -69,10 +64,10 @@ import { HoldingDetailDialogParams } from './interfaces/interfaces';
GfDialogHeaderModule, GfDialogHeaderModule,
GfLineChartComponent, GfLineChartComponent,
GfPortfolioProportionChartComponent, GfPortfolioProportionChartComponent,
GfTagsSelectorComponent,
GfValueComponent, GfValueComponent,
MatAutocompleteModule, MatAutocompleteModule,
MatButtonModule, MatButtonModule,
MatChipsModule,
MatDialogModule, MatDialogModule,
MatFormFieldModule, MatFormFieldModule,
MatTabsModule, MatTabsModule,
@ -85,8 +80,6 @@ import { HoldingDetailDialogParams } from './interfaces/interfaces';
templateUrl: 'holding-detail-dialog.html' templateUrl: 'holding-detail-dialog.html'
}) })
export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
@ViewChild('tagInput') tagInput: ElementRef<HTMLInputElement>;
public activityForm: FormGroup; public activityForm: FormGroup;
public accounts: Account[]; public accounts: Account[];
public activities: Activity[]; public activities: Activity[];
@ -103,7 +96,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
public dividendInBaseCurrencyPrecision = 2; public dividendInBaseCurrencyPrecision = 2;
public dividendYieldPercentWithCurrencyEffect: number; public dividendYieldPercentWithCurrencyEffect: number;
public feeInBaseCurrency: number; public feeInBaseCurrency: number;
public filteredTagsObservable: Observable<Tag[]> = of([]);
public firstBuyDate: string; public firstBuyDate: string;
public historicalDataItems: LineChartItem[]; public historicalDataItems: LineChartItem[];
public investment: number; public investment: number;
@ -305,17 +297,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
this.activityForm.setValue({ tags: this.tags }, { emitEvent: false }); this.activityForm.setValue({ tags: this.tags }, { emitEvent: false });
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.transactionCount = transactionCount; this.transactionCount = transactionCount;
this.totalItems = transactionCount; this.totalItems = transactionCount;
this.value = value; this.value = value;
@ -415,15 +396,8 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
}); });
} }
public onAddTag(event: MatAutocompleteSelectedEvent) { public onAddTag(event: Tag[]) {
this.activityForm.get('tags').setValue([ this.activityForm.get('tags').setValue(event);
...(this.activityForm.get('tags').value ?? []),
this.tagsAvailable.find(({ id }) => {
return id === event.option.value;
})
]);
this.tagInput.nativeElement.value = '';
} }
public onCloneActivity(aActivity: Activity) { public onCloneActivity(aActivity: Activity) {
@ -458,14 +432,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
}); });
} }
public onRemoveTag(aTag: Tag) {
this.activityForm.get('tags').setValue(
this.activityForm.get('tags').value.filter(({ id }) => {
return id !== aTag.id;
})
);
}
public onUpdateActivity(aActivity: Activity) { public onUpdateActivity(aActivity: Activity) {
this.router.navigate(['/portfolio', 'activities'], { this.router.navigate(['/portfolio', 'activities'], {
queryParams: { activityId: aActivity.id, editDialog: true } queryParams: { activityId: aActivity.id, editDialog: true }
@ -478,14 +444,4 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();
} }
private filterTags(aTags: Tag[]) {
const tagIds = aTags.map(({ id }) => {
return id;
});
return this.tagsAvailable.filter(({ id }) => {
return !tagIds.includes(id);
});
}
} }

37
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html

@ -373,38 +373,11 @@
}" }"
> >
<div class="col"> <div class="col">
<mat-form-field appearance="outline" class="w-100 without-hint"> <gf-tags-selector
<mat-label i18n>Tags</mat-label> [tags]="activityForm.get('tags')?.value"
<mat-chip-grid #tagsChipList> [tagsAvailable]="tagsAvailable"
@for (tag of activityForm.get('tags')?.value; track tag.id) { (tagsChanged)="onAddTag($event)"
<mat-chip-row />
matChipRemove
[removable]="true"
(removed)="onRemoveTag(tag)"
>
{{ tag.name }}
<ion-icon class="ml-2" matPrefix name="close-outline" />
</mat-chip-row>
}
<input
#tagInput
name="close-outline"
[matAutocomplete]="autocompleteTags"
[matChipInputFor]="tagsChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
/>
</mat-chip-grid>
<mat-autocomplete
#autocompleteTags="matAutocomplete"
(optionSelected)="onAddTag($event)"
>
@for (tag of filteredTagsObservable | async; track tag.id) {
<mat-option [value]="tag.id">
{{ tag.name }}
</mat-option>
}
</mat-autocomplete>
</mat-form-field>
</div> </div>
</div> </div>

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

@ -140,12 +140,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
updateAccountBalance: [false] updateAccountBalance: [false]
}); });
console.log(
'Initialized Activity Form: ',
this.activityForm.get('tags').value
);
console.log('Initialized Activity Tags Available: ', this.tagsAvailable);
this.activityForm.valueChanges this.activityForm.valueChanges
.pipe( .pipe(
// Slightly delay until the more specific form control value changes have // Slightly delay until the more specific form control value changes have

6
libs/ui/src/lib/tags-selector/tags-selector.component.html

@ -1,7 +1,7 @@
<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-grid #tagsChipList> <mat-chip-grid #tagsChipList>
@for (tag of fruits(); track tag.id) { @for (tag of tagsSignal(); track tag.id) {
<mat-chip-row <mat-chip-row
matChipRemove matChipRemove
[removable]="true" [removable]="true"
@ -16,16 +16,14 @@
name="close-outline" name="close-outline"
[matAutocomplete]="autocompleteTags" [matAutocomplete]="autocompleteTags"
[matChipInputFor]="tagsChipList" [matChipInputFor]="tagsChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[(ngModel)]="currentFruit" [(ngModel)]="currentFruit"
/> />
<!-- (matChipInputTokenEnd)="onAddTagInput($event)" -->
</mat-chip-grid> </mat-chip-grid>
<mat-autocomplete <mat-autocomplete
#autocompleteTags="matAutocomplete" #autocompleteTags="matAutocomplete"
(optionSelected)="onAddTag($event)" (optionSelected)="onAddTag($event)"
> >
@for (tag of filteredFruits(); track tag.id) { @for (tag of filteredTags(); track tag.id) {
<mat-option [value]="tag.id"> <mat-option [value]="tag.id">
{{ tag.name }} {{ tag.name }}
</mat-option> </mat-option>

107
libs/ui/src/lib/tags-selector/tags-selector.component.ts

@ -1,12 +1,6 @@
// 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 { CommonModule } from '@angular/common';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef,
Component, Component,
computed, computed,
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
@ -20,36 +14,19 @@ import {
signal, signal,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { import {
MatAutocompleteTrigger,
MatAutocompleteModule, MatAutocompleteModule,
MatAutocompleteSelectedEvent MatAutocompleteSelectedEvent
} from '@angular/material/autocomplete'; } from '@angular/material/autocomplete';
import { import { MatChipsModule } from '@angular/material/chips';
MatChipEditedEvent, import { MatFormFieldModule } from '@angular/material/form-field';
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 { MatIconModule } from '@angular/material/icon';
import { MatInput, MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { Tag } from '@prisma/client'; 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({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
// host: {
// '[attr.aria-describedBy]': 'describedBy',
// '[id]': 'id'
// },
imports: [ imports: [
CommonModule, CommonModule,
FormsModule, FormsModule,
@ -57,15 +34,8 @@ import { AbstractMatFormField } from '../shared/abstract-mat-form-field';
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,
MatChipsModule, MatChipsModule,
MatIconModule, MatIconModule
ReactiveFormsModule
], ],
// providers: [
// {
// provide: MatFormFieldControl,
// useExisting: GfTagsSelectorComponent
// }
// ],
schemas: [CUSTOM_ELEMENTS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-tags-selector', selector: 'gf-tags-selector',
standalone: true, standalone: true,
@ -73,9 +43,6 @@ import { AbstractMatFormField } from '../shared/abstract-mat-form-field';
templateUrl: 'tags-selector.component.html' templateUrl: 'tags-selector.component.html'
}) })
export class GfTagsSelectorComponent implements OnInit { export class GfTagsSelectorComponent implements OnInit {
public focus(): void {
throw new Error('Method not implemented.');
}
@Input() tags: Tag[]; @Input() tags: Tag[];
@Input() tagsAvailable: Tag[]; @Input() tagsAvailable: Tag[];
@ -83,75 +50,57 @@ export class GfTagsSelectorComponent implements OnInit {
@ViewChild('tagInput') tagInput: ElementRef<HTMLInputElement>; @ViewChild('tagInput') tagInput: ElementRef<HTMLInputElement>;
// public activityForm: FormGroup; readonly tagsSignal = signal([{ id: '', name: '' }]);
public filteredTagsObservable: Observable<Tag[]> = of([]);
public separatorKeysCodes: number[] = [COMMA, ENTER];
readonly fruits = signal([{ id: '', name: '' }]);
readonly currentFruit = model(''); readonly currentFruit = model('');
readonly filteredTags = computed(() => {
readonly filteredFruits = computed(() => {
const currentFruit = this.currentFruit().toLowerCase(); const currentFruit = this.currentFruit().toLowerCase();
const aTags = this.tagsAvailable const aTags = this.tagsAvailable ?? [{ id: '', name: '' }];
? this.tagsAvailable const bTags = this.tagsSignal() ?? [{ id: '', name: '' }];
: [{ id: '', name: '' }]; const cTags = aTags.filter((value) => !bTags.includes(value));
return currentFruit return currentFruit
? aTags.filter((tag) => tag.name.toLowerCase().includes(currentFruit)) ? cTags.filter((tag) => tag.name.toLowerCase().includes(currentFruit))
: this.tagsAvailable; : cTags;
}); });
public constructor() { public constructor() {
effect(() => { effect(() => {
if (this.fruits()) { if (this.tagsSignal()) {
console.log('Emit Fruits: ', this.fruits()); this.tagsChanged.emit(this.tagsSignal());
this.tagsChanged.emit(this.fruits());
} }
}); });
} }
ngOnInit() { ngOnInit() {
this.fruits.set(this.tags); this.tagsSignal.set(this.tags);
console.log('Tags Available : ', this.tagsAvailable);
console.log('Tags : ', this.tags);
} }
public onAddTag(event: MatAutocompleteSelectedEvent) { public onAddTag(event: MatAutocompleteSelectedEvent) {
if ( const tagId = event.option.value;
this.fruits() && const newTag = this.tagsAvailable.find(({ id }) => id === tagId);
this.fruits().some((el) => el.id === event.option.value)
) { if (this.tagsSignal()?.some((el) => el.id === tagId)) {
this.currentFruit.set(''); this.currentFruit.set('');
event.option.deselect(); event.option.deselect();
return; return;
} }
this.fruits() this.tagsSignal()
? this.fruits.update((fruits) => [ ? this.tagsSignal.update((tags) => [...tags, newTag])
...fruits, : this.tagsSignal.update(() => [newTag]);
this.tagsAvailable.find(({ id }) => {
return id === event.option.value;
})
])
: this.fruits.update(() => [
this.tagsAvailable.find(({ id }) => {
return id === event.option.value;
})
]);
this.currentFruit.set(''); this.currentFruit.set('');
event.option.deselect(); event.option.deselect();
} }
public onRemoveTag(aTag: Tag) { public onRemoveTag(aTag: Tag) {
this.fruits.update((fruits) => { this.tagsSignal.update((tagsSignal) => {
const index = fruits.indexOf(aTag); const index = tagsSignal.indexOf(aTag);
if (index < 0) { if (index < 0) {
return fruits; return tagsSignal;
} }
fruits.splice(index, 1); tagsSignal.splice(index, 1);
return [...fruits]; return [...tagsSignal];
}); });
} }
} }

Loading…
Cancel
Save