Feature/extract tags selector to reusable component (#4256)
* feat(ui): create gf-tags-selector component * feat(ui): implement gf-tags-selector in activity dialog * feat(ui): implement gf-tags-selector in holding detail dialog * Update changelogpull/4272/head^2
parent
9905c428af
commit
d711fed4f5
@ -0,0 +1 @@
|
||||
export * from './tags-selector.component';
|
@ -0,0 +1,32 @@
|
||||
<mat-form-field appearance="outline" class="w-100 without-hint">
|
||||
<mat-label i18n>Tags</mat-label>
|
||||
<mat-chip-grid #tagsChipList>
|
||||
@for (tag of tagsSelected(); track tag.id) {
|
||||
<mat-chip-row
|
||||
matChipRemove
|
||||
[removable]="true"
|
||||
(removed)="onRemoveTag(tag)"
|
||||
>
|
||||
{{ tag.name }}
|
||||
<ion-icon matChipTrailingIcon name="close-outline" />
|
||||
</mat-chip-row>
|
||||
}
|
||||
<input
|
||||
#tagInput
|
||||
[formControl]="tagInputControl"
|
||||
[matAutocomplete]="autocompleteTags"
|
||||
[matChipInputFor]="tagsChipList"
|
||||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||
/>
|
||||
</mat-chip-grid>
|
||||
<mat-autocomplete
|
||||
#autocompleteTags="matAutocomplete"
|
||||
(optionSelected)="onAddTag($event)"
|
||||
>
|
||||
@for (tag of filteredOptions | async; track tag.id) {
|
||||
<mat-option [value]="tag.id">
|
||||
{{ tag.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
import { COMMA, ENTER } from '@angular/cdk/keycodes';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
signal,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import {
|
||||
MatAutocompleteModule,
|
||||
MatAutocompleteSelectedEvent
|
||||
} from '@angular/material/autocomplete';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { Tag } from '@prisma/client';
|
||||
import { BehaviorSubject, Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatAutocompleteModule,
|
||||
MatChipsModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
selector: 'gf-tags-selector',
|
||||
styleUrls: ['./tags-selector.component.scss'],
|
||||
templateUrl: 'tags-selector.component.html'
|
||||
})
|
||||
export class GfTagsSelectorComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() tags: Tag[];
|
||||
@Input() tagsAvailable: Tag[];
|
||||
|
||||
@Output() tagsChanged = new EventEmitter<Tag[]>();
|
||||
|
||||
@ViewChild('tagInput') tagInput: ElementRef<HTMLInputElement>;
|
||||
|
||||
public filteredOptions: Subject<Tag[]> = new BehaviorSubject([]);
|
||||
public readonly separatorKeysCodes: number[] = [COMMA, ENTER];
|
||||
public readonly tagInputControl = new FormControl('');
|
||||
public readonly tagsSelected = signal<Tag[]>([]);
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor() {
|
||||
this.tagInputControl.valueChanges
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((value) => {
|
||||
this.filteredOptions.next(this.filterTags(value));
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.tagsSelected.set(this.tags);
|
||||
this.updateFilters();
|
||||
}
|
||||
|
||||
public ngOnChanges() {
|
||||
this.tagsSelected.set(this.tags);
|
||||
this.updateFilters();
|
||||
}
|
||||
|
||||
public onAddTag(event: MatAutocompleteSelectedEvent) {
|
||||
const tag = this.tagsAvailable.find(({ id }) => {
|
||||
return id === event.option.value;
|
||||
});
|
||||
|
||||
this.tagsSelected.update((tags) => {
|
||||
return [...(tags ?? []), tag];
|
||||
});
|
||||
|
||||
this.tagsChanged.emit(this.tagsSelected());
|
||||
this.tagInput.nativeElement.value = '';
|
||||
this.tagInputControl.setValue(undefined);
|
||||
}
|
||||
|
||||
public onRemoveTag(tag: Tag) {
|
||||
this.tagsSelected.update((tags) => {
|
||||
return tags.filter(({ id }) => {
|
||||
return id !== tag.id;
|
||||
});
|
||||
});
|
||||
|
||||
this.tagsChanged.emit(this.tagsSelected());
|
||||
this.updateFilters();
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private filterTags(query: string = ''): Tag[] {
|
||||
const tags = this.tagsSelected() ?? [];
|
||||
const tagIds = tags.map(({ id }) => {
|
||||
return id;
|
||||
});
|
||||
|
||||
return this.tagsAvailable.filter(({ id, name }) => {
|
||||
return (
|
||||
!tagIds.includes(id) && name.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private updateFilters() {
|
||||
this.filteredOptions.next(this.filterTags());
|
||||
}
|
||||
}
|
Loading…
Reference in new issue