diff --git a/src/frontend/app/app-common.module.ts b/src/frontend/app/app-common.module.ts index b0d382dfd84fd1cf701a35081073a262d292b1a1..e00d2a7cd24dd05c5971ddd2d7b56c74d360629f 100755 --- a/src/frontend/app/app-common.module.ts +++ b/src/frontend/app/app-common.module.ts @@ -54,7 +54,8 @@ import { ConfirmComponent } from '../plugins/modal/confir /*PLUGIN COMPONENT*/ import { PluginAutocomplete } from '../plugins/autocomplete/autocomplete.component'; import { PluginSelectSearchComponent } from '../plugins/select-search/select-search.component'; -import { FolderInputComponent } from '../app/folder/Indexing/folder-input.component'; +import { FolderInputComponent } from '../app/folder/indexing/folder-input.component'; +import { TagInputComponent } from '../app/tag/indexing/tag-input.component'; @@ -102,7 +103,8 @@ export class MyHammerConfig extends HammerGestureConfig { IndexingFormComponent, FieldListComponent, PluginSelectSearchComponent, - FolderInputComponent + FolderInputComponent, + TagInputComponent ], exports: [ CommonModule, @@ -136,7 +138,8 @@ export class MyHammerConfig extends HammerGestureConfig { IndexingFormComponent, FieldListComponent, PluginSelectSearchComponent, - FolderInputComponent + FolderInputComponent, + TagInputComponent ], providers: [ LatinisePipe, diff --git a/src/frontend/app/folder/indexing/folder-input.component.html b/src/frontend/app/folder/indexing/folder-input.component.html index b3211c58bcdee58c6b59ec9419dd1bb79ee852f7..55f40b782e3790da768337b54ac2264d2a29ff25 100644 --- a/src/frontend/app/folder/indexing/folder-input.component.html +++ b/src/frontend/app/folder/indexing/folder-input.component.html @@ -2,7 +2,7 @@ <input type="hidden" [formControl]="controlAutocomplete"> <mat-form-field floatLabel="never" class="input-form"> <mat-icon color="primary" class="fa fa-search" matPrefix style="padding-left: 20px;font-size: 15px;"></mat-icon> - <input type="text" #autoCompleteInput placeholder="Rechercher un dossier" matInput [formControl]="myControl" + <input type="text" #autoCompleteInput [placeholder]="lang.searchFolder" matInput [formControl]="myControl" [matAutocomplete]="auto" (click)="$event.stopPropagation()" (focus)="resetAutocomplete()" [required]="required"> <button [disabled]="!canAdd" type="button" matSuffix mat-icon-button (click)="addItem()"> @@ -10,7 +10,7 @@ </button> <mat-autocomplete #auto="matAutocomplete" (optionSelected)="selectOpt($event)"> <ng-container *ngIf="options.length > 0 && !loading"> - <mat-option *ngFor="let option of filteredOptions | async | sortBy: key" [value]="option"> + <mat-option *ngFor="let option of filteredOptions | async | sortBy: key" [value]="option" [title]="option[key]"> <span color="primary">{{option[key]}}</span> <small>{{option[subInfoKey]}}</small> </mat-option> </ng-container> @@ -26,8 +26,8 @@ <mat-chip-list *ngIf="controlAutocomplete.value.length > 0" class="mat-chip-list-stacked" color="default"> <mat-chip *ngFor="let item of controlAutocomplete.value;let i=index" class="activeListAutocomplete" color="default" [removable]="true" (removed)="removeItem(i)"> - <span style="display: flex;flex: 1;align-items: center;"> - <i class="fa fa-folder-open"></i> {{this.valuesToDisplay[item]}} + <span style="display: flex;flex: 1;align-items: center;" [title]="this.valuesToDisplay[item]"> + <i class="fa fa-folder-open" style="padding-right:5px;"></i> {{this.valuesToDisplay[item]}} </span> <mat-icon matChipRemove class="fa fa-times"></mat-icon> </mat-chip> diff --git a/src/frontend/app/folder/indexing/folder-input.component.ts b/src/frontend/app/folder/indexing/folder-input.component.ts index 7cb75e42992c7952366045993f566bde1dc75ef6..46305f6ac92ac1b04fbbdfa7c81c4056e48d1fde 100644 --- a/src/frontend/app/folder/indexing/folder-input.component.ts +++ b/src/frontend/app/folder/indexing/folder-input.component.ts @@ -176,7 +176,7 @@ export class FolderInputComponent implements OnInit { newElem[this.key] = this.myControl.value; - this.dialogRef = this.dialog.open(ConfirmComponent, { autoFocus: false, disableClose: true, data: { title: this.lang.confirm, msg: this.lang.folderCreateMsg + '<b>' + newElem[this.key] + '</b> ?' } }); + this.dialogRef = this.dialog.open(ConfirmComponent, { autoFocus: false, disableClose: true, data: { title: this.lang.confirm, msg: this.lang.createMsg + ' <b>' + newElem[this.key] + '</b> ?' } }); this.dialogRef.afterClosed().pipe( filter((data: string) => data === 'ok'), diff --git a/src/frontend/app/indexation/indexing-form/indexing-form.component.html b/src/frontend/app/indexation/indexing-form/indexing-form.component.html index e8f18d17fda6437a9733ee83e6b5ae07008d63e8..834429955f9a67a11562ae133931e4551da8bd88 100644 --- a/src/frontend/app/indexation/indexing-form/indexing-form.component.html +++ b/src/frontend/app/indexation/indexing-form/indexing-form.component.html @@ -15,7 +15,8 @@ <div *ngIf="this['indexingModels_'+category].length > 0 || adminMode" class="banner" [style.borderColor]="currentPriorityColor"> <div class="title" [style.color]="currentPriorityColor"> - {{lang[category] | uppercase}} <small *ngIf="category === 'mail'" [title]="lang.category_id">{{lang['indexing_' + currentCategory] | titlecase}}</small> + {{lang[category] | uppercase}} <small *ngIf="category === 'mail'" + [title]="lang.category_id">{{lang['indexing_' + currentCategory] | titlecase}}</small> <div class="title-divider"></div> </div> <div class="content"> @@ -145,6 +146,10 @@ <app-folder-input [control]="arrFormControl[field.identifier]" style="width:100%;"> </app-folder-input> </ng-container> + <ng-container *ngIf="field.identifier === 'tags'"> + <app-tag-input [control]="arrFormControl[field.identifier]" style="width:100%;"> + </app-tag-input> + </ng-container> </div> <div class="fieldState"> <i class="fas fa-asterisk" diff --git a/src/frontend/app/tag/indexing/tag-input.component.html b/src/frontend/app/tag/indexing/tag-input.component.html new file mode 100644 index 0000000000000000000000000000000000000000..81a3877b5aea43d5a66b6ba8ed25d3b01239da31 --- /dev/null +++ b/src/frontend/app/tag/indexing/tag-input.component.html @@ -0,0 +1,39 @@ +<form> + <input type="hidden" [formControl]="controlAutocomplete"> + <mat-form-field floatLabel="never" class="input-form"> + <mat-icon color="primary" class="fa fa-search" matPrefix style="padding-left: 20px;font-size: 15px;"></mat-icon> + <input type="text" #autoCompleteInput [placeholder]="lang.searchTag" matInput [formControl]="myControl" + [matAutocomplete]="auto" (click)="$event.stopPropagation()" (focus)="resetAutocomplete()" + [required]="required"> + <button [disabled]="!canAdd" type="button" matSuffix mat-icon-button (click)="addItem()"> + <mat-icon class="fa fa-plus" [title]="lang.add" [style.visibility]="canAdd ? 'visible' : 'hidden'"></mat-icon> + </button> + <mat-autocomplete #auto="matAutocomplete" (optionSelected)="selectOpt($event)"> + <ng-container *ngIf="options.length > 0 && !loading"> + <mat-option *ngFor="let option of filteredOptions | async | sortBy: key" [value]="option" [title]="option[key]"> + <span color="primary">{{option[key]}}</span> <small>{{option[subInfoKey]}}</small> + </mat-option> + </ng-container> + <mat-option class="autoCompleteInfoResult smallInputInfo" *ngIf="options.length === 0 && !loading" disabled + [innerHTML]="listInfo"> + </mat-option> + <mat-option *ngIf="loading" disabled> + <mat-spinner diameter="20"></mat-spinner> + </mat-option> + </mat-autocomplete> + </mat-form-field> + <div style="padding-top:10px;"> + <mat-chip-list *ngIf="controlAutocomplete.value.length > 0" class="mat-chip-list-stacked" color="default"> + <mat-chip *ngFor="let item of controlAutocomplete.value;let i=index" class="listAutocomplete" + color="default" [removable]="true" (removed)="removeItem(i)"> + <span style="display: flex;flex: 1;align-items: center;" [title]="this.valuesToDisplay[item]"> + <i class="fa fa-tag" style="padding-right:5px;"></i> {{this.valuesToDisplay[item]}} + </span> + <mat-icon matChipRemove class="fa fa-times"></mat-icon> + </mat-chip> + </mat-chip-list> + <div class="noResult" *ngIf="controlAutocomplete.value.length === 0"> + {{lang.noSelectedTag}} + </div> + </div> +</form> \ No newline at end of file diff --git a/src/frontend/app/tag/indexing/tag-input.component.scss b/src/frontend/app/tag/indexing/tag-input.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..a79e66f1bd541ae3c999758432924ac1c2de861b --- /dev/null +++ b/src/frontend/app/tag/indexing/tag-input.component.scss @@ -0,0 +1,25 @@ +@import '../../../css/vars.scss'; + +.smallInputInfo { + font-size: 9px; + white-space: normal; + line-height: 13px; + display: table-cell; + vertical-align: middle; + text-align: center; +} + +.mat-chip.mat-standard-chip { + color: white; + background-color: lighten($primary,10%); +} + +.noResult { + text-align: center; + font-style: italic; + opacity: 0.5; +} + +.listAutocomplete { + height: auto; +} \ No newline at end of file diff --git a/src/frontend/app/tag/indexing/tag-input.component.ts b/src/frontend/app/tag/indexing/tag-input.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..0f2071ac34b005fc4241bbe09a05e198de4c3187 --- /dev/null +++ b/src/frontend/app/tag/indexing/tag-input.component.ts @@ -0,0 +1,199 @@ +import { Component, OnInit, Input, ViewChild, ElementRef } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { LANG } from '../../translate.component'; +import { NotificationService } from '../../notification.service'; +import { HeaderService } from '../../../service/header.service'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { AppService } from '../../../service/app.service'; +import { SortPipe } from '../../../plugins/sorting.pipe'; +import { FormControl } from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { debounceTime, filter, distinctUntilChanged, tap, switchMap, exhaustMap, catchError } from 'rxjs/operators'; +import { LatinisePipe } from 'ngx-pipes'; +import { ConfirmComponent } from '../../../plugins/modal/confirm.component'; + +@Component({ + selector: 'app-tag-input', + templateUrl: "tag-input.component.html", + styleUrls: [ + 'tag-input.component.scss', + '../../indexation/indexing-form/indexing-form.component.scss' + ], + providers: [NotificationService, AppService, SortPipe] +}) + +export class TagInputComponent implements OnInit { + + lang: any = LANG; + + loading: boolean = false; + + key: string = 'idToDisplay'; + + canAdd: boolean = false; + + listInfo: string; + myControl = new FormControl(); + filteredOptions: Observable<string[]>; + options: any; + valuesToDisplay: any = {}; + dialogRef: MatDialogRef<any>; + newIds: number[] = []; + + + /** + * FormControl used when autocomplete is used in form and must be catched in a form control. + */ + @Input('control') controlAutocomplete: FormControl; + + @ViewChild('autoCompleteInput', { static: true }) autoCompleteInput: ElementRef; + + constructor( + public http: HttpClient, + private notify: NotificationService, + public dialog: MatDialog, + private headerService: HeaderService, + public appService: AppService, + private latinisePipe: LatinisePipe + ) { + + } + + ngOnInit() { + this.controlAutocomplete.setValue(this.controlAutocomplete.value === null || this.controlAutocomplete.value === '' ? [] : this.controlAutocomplete.value); + this.initFormValue(); + this.initAutocompleteRoute(); + } + + initAutocompleteRoute() { + this.listInfo = this.lang.autocompleteInfo; + this.options = []; + this.myControl.valueChanges + .pipe( + tap((value) => this.canAdd = value.length === 0 ? false : true), + debounceTime(300), + filter(value => value.length > 2), + distinctUntilChanged(), + tap(() => this.loading = true), + switchMap((data: any) => this.getDatas(data)), + tap((data: any) => { + if (data.length === 0) { + this.listInfo = this.lang.noAvailableValue; + } else { + this.listInfo = ''; + } + this.options = data; + this.filteredOptions = of(this.options); + this.loading = false; + }) + ).subscribe(); + } + + getDatas(data: string) { + return this.http.get('../../rest/autocomplete/tags', { params: { "search": data } }); + } + + selectOpt(ev: any) { + this.setFormValue(ev.option.value); + this.myControl.setValue(''); + + } + + initFormValue() { + + this.controlAutocomplete.value.forEach((ids: any) => { + this.http.get('../../rest/tags/' + ids).pipe( + tap((data) => { + for (var key in data) { + this.valuesToDisplay[data[key].id] = data[key].label; + } + }) + ).subscribe(); + }); + } + + setFormValue(item: any) { + if (this.controlAutocomplete.value.indexOf(item['id']) === -1) { + let arrvalue = []; + if (this.controlAutocomplete.value !== null) { + arrvalue = this.controlAutocomplete.value; + } + arrvalue.push(item['id']); + this.valuesToDisplay[item['id']] = item[this.key]; + this.controlAutocomplete.setValue(arrvalue); + } + } + + resetAutocomplete() { + this.options = []; + this.listInfo = this.lang.autocompleteInfo; + } + + private _filter(value: string): string[] { + if (typeof value === 'string') { + const filterValue = this.latinisePipe.transform(value.toLowerCase()); + return this.options.filter((option: any) => this.latinisePipe.transform(option[this.key].toLowerCase()).includes(filterValue)); + } else { + return this.options; + } + } + + unsetValue() { + this.controlAutocomplete.setValue(''); + this.myControl.setValue(''); + this.myControl.enable(); + } + + removeItem(index: number) { + + if (this.newIds.indexOf(this.controlAutocomplete.value[index]) === -1) { + let arrValue = this.controlAutocomplete.value; + this.controlAutocomplete.value.splice(index, 1); + this.controlAutocomplete.setValue(arrValue); + } else { + this.dialogRef = this.dialog.open(ConfirmComponent, { autoFocus: false, disableClose: true, data: { title: this.lang.confirm, msg: this.lang.folderDeleteWarnMsg + ' <b>' + this.valuesToDisplay[this.controlAutocomplete.value[index]] + '</b> ' + this.lang.folderDeleteWarnMsg2 } }); + + this.dialogRef.afterClosed().pipe( + filter((data: string) => data === 'ok'), + exhaustMap(() => this.http.delete('../../rest/tags/' + this.controlAutocomplete.value[index])), + tap((data: any) => { + let arrValue = this.controlAutocomplete.value; + this.controlAutocomplete.value.splice(index, 1); + this.controlAutocomplete.setValue(arrValue); + + this.notify.success(this.lang.tagDeleted); + }), + catchError((err: any) => { + this.notify.handleErrors(err); + return of(false); + }) + ).subscribe(); + } + } + + addItem() { + const newElem = {}; + + newElem[this.key] = this.myControl.value; + + this.dialogRef = this.dialog.open(ConfirmComponent, { autoFocus: false, disableClose: true, data: { title: this.lang.confirm, msg: this.lang.createMsg + ' <b>' + newElem[this.key] + '</b> ?' } }); + + this.dialogRef.afterClosed().pipe( + filter((data: string) => data === 'ok'), + exhaustMap(() => this.http.post('../../rest/tags', { label: newElem[this.key] })), + tap((data: any) => { + for (var key in data) { + newElem['id'] = data[key]; + this.newIds.push(data[key]); + } + this.setFormValue(newElem); + this.myControl.setValue(''); + this.notify.success(this.lang.tagAdded); + }), + catchError((err: any) => { + this.notify.handleErrors(err); + return of(false); + }) + ).subscribe(); + } +} \ No newline at end of file diff --git a/src/frontend/lang/lang-en.ts b/src/frontend/lang/lang-en.ts index 9a0c5b875cdeccc468d27fe1a33dcd69d26f14c3..846a1316870cc8c30627f112248e7b35740d2940 100755 --- a/src/frontend/lang/lang-en.ts +++ b/src/frontend/lang/lang-en.ts @@ -1137,8 +1137,12 @@ export const LANG_EN = { "indexing_outgoing" : "outgoing", "indexing_internal" : "internal", "indexing_ged_doc" : "GED", - "folderCreateMsg" : "Do want create", + "createMsg" : "Do want create", "folderDeleteWarnMsg" : "Do want delete", "folderDeleteWarnMsg2" : "previously created ?", "noSelectedFolder" : "No selected folder", + "noSelectedTag" : "No selected tag", + "searchTag" : "Search tag", + "tagAdded" : "Tag added", + "tagDeleted" : "Tag deleted", }; diff --git a/src/frontend/lang/lang-fr.ts b/src/frontend/lang/lang-fr.ts index 2178f046edbfebcbdc2cbbb66465010c47076378..9d78a9b27b37dc042a98601587dc8a89636ec580 100755 --- a/src/frontend/lang/lang-fr.ts +++ b/src/frontend/lang/lang-fr.ts @@ -1174,8 +1174,12 @@ export const LANG_FR = { "indexing_outgoing" : "départ", "indexing_internal" : "interne", "indexing_ged_doc" : "GED", - "folderCreateMsg" : "Voulez-vous créer", + "createMsg" : "Voulez-vous créer", "folderDeleteWarnMsg" : "Voulez-vous supprimer", "folderDeleteWarnMsg2" : "récemment créé ?", - "noSelectedFolder" : "Aucun dossier lié", + "noSelectedFolder" : "Aucun dossier associé", + "noSelectedTag" : "Aucun mot-clé associé", + "searchTag" : "Rechercher un mot-clé", + "tagAdded" : "Mot-clé ajouté", + "tagDeleted" : "Mot-clé supprimé", }; diff --git a/src/frontend/lang/lang-nl.ts b/src/frontend/lang/lang-nl.ts index f76bcb4f2ab639554fda7d92d7d09882c2bbd903..2ee881f42e6b4d1d843bc5d56ae3c640ac72a731 100755 --- a/src/frontend/lang/lang-nl.ts +++ b/src/frontend/lang/lang-nl.ts @@ -1163,8 +1163,12 @@ export const LANG_NL = { "indexing_outgoing" : "outgoing", //_TO_TRANSLATE "indexing_internal" : "internal", //_TO_TRANSLATE "indexing_ged_doc" : "GED", //_TO_TRANSLATE - "folderCreateMsg" : "Do want create", //_TO_TRANSLATE + "createMsg" : "Do want create", //_TO_TRANSLATE "folderDeleteWarnMsg" : "Do want delete", //_TO_TRANSLATE "folderDeleteWarnMsg2" : "previously created ?", //_TO_TRANSLATE "noSelectedFolder" : "No selected folder", //_TO_TRANSLATE + "noSelectedTag" : "No selected tag", //_TO_TRANSLATE + "searchTag" : "Search tag", //_TO_TRANSLATE + "tagAdded" : "Tag added", //_TO_TRANSLATE + "tagDeleted" : "Tag deleted", //_TO_TRANSLATE };