Browse Source

Created Tags selector and updated CreateOrUpdateActivityDialog

pull/3708/head
Daniel Idem 1 year ago
parent
commit
c974ba0f11
  1. 57
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  2. 35
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
  3. 2
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.module.ts
  4. 1
      libs/ui/src/lib/tags-selector/index.ts
  5. 34
      libs/ui/src/lib/tags-selector/tags-selector.component.html
  6. 0
      libs/ui/src/lib/tags-selector/tags-selector.component.scss
  7. 157
      libs/ui/src/lib/tags-selector/tags-selector.component.ts

57
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<HTMLInputElement>;
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<Tag[]> = 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();

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

@ -379,38 +379,11 @@
</mat-form-field>
</div>
<div class="mb-3" [ngClass]="{ 'd-none': tagsAvailable?.length < 1 }">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Tags</mat-label>
<mat-chip-grid #tagsChipList>
@for (tag of activityForm.get('tags')?.value; track tag.id) {
<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"
<gf-tags-selector
[tags]="activityForm.get('tags')?.value"
[tagsAvailable]="tagsAvailable"
(tagsChanged)="onTagsChanged($event)"
/>
</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 class="d-flex" mat-dialog-actions>

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

1
libs/ui/src/lib/tags-selector/index.ts

@ -0,0 +1 @@
export * from './tags-selector.component';

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

@ -0,0 +1,34 @@
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Tags</mat-label>
<mat-chip-grid #tagsChipList>
@for (tag of fruits(); track tag.id) {
<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"
[(ngModel)]="currentFruit"
/>
<!-- (matChipInputTokenEnd)="onAddTagInput($event)" -->
</mat-chip-grid>
<mat-autocomplete
#autocompleteTags="matAutocomplete"
(optionSelected)="onAddTag($event)"
>
@for (tag of filteredFruits(); track tag.id) {
<mat-option [value]="tag.id">
{{ tag.name }}
</mat-option>
}
</mat-autocomplete>
</mat-form-field>

0
libs/ui/src/lib/tags-selector/tags-selector.component.scss

157
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<Tag[]>();
@ViewChild('tagInput') tagInput: ElementRef<HTMLInputElement>;
// public activityForm: FormGroup;
public filteredTagsObservable: Observable<Tag[]> = 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];
});
}
}
Loading…
Cancel
Save