diff --git a/src/frontend/app/administration/administration.module.ts b/src/frontend/app/administration/administration.module.ts index 5f80de907c15e443ad9dda9b7cee978a0c3c2437..d1a1a2ff1d0059154ec531199a51b715b61df94c 100755 --- a/src/frontend/app/administration/administration.module.ts +++ b/src/frontend/app/administration/administration.module.ts @@ -47,6 +47,7 @@ import { NotificationAdministrationComponent } from './notification/notification import { NotificationsAdministrationComponent } from './notification/notifications-administration.component'; import { ParameterAdministrationComponent } from './parameter/parameter-administration.component'; import { ParametersAdministrationComponent } from './parameter/parameters-administration.component'; +import { ParametersCustomizationComponent } from './parameter/customization/parameters-customization.component'; import { PrioritiesAdministrationComponent } from './priority/priorities-administration.component'; import { PriorityAdministrationComponent } from './priority/priority-administration.component'; import { SecuritiesAdministrationComponent } from './security/securities-administration.component'; @@ -124,6 +125,7 @@ import {EntitiesExportComponent} from './entity/export/entities-export.component NotificationsAdministrationComponent, ParameterAdministrationComponent, ParametersAdministrationComponent, + ParametersCustomizationComponent, PrioritiesAdministrationComponent, PriorityAdministrationComponent, SecuritiesAdministrationComponent, diff --git a/src/frontend/app/administration/parameter/customization/parameters-customization.component.html b/src/frontend/app/administration/parameter/customization/parameters-customization.component.html new file mode 100644 index 0000000000000000000000000000000000000000..f8f07ad89e6a8cf7a55a361849509f8f953fc739 --- /dev/null +++ b/src/frontend/app/administration/parameter/customization/parameters-customization.component.html @@ -0,0 +1,55 @@ +<form [formGroup]="stepFormGroup" style="display: contents;"> + <div class="col-md-6"> + <mat-form-field appearance="outline"> + <mat-label>{{'lang.applicationName' | translate}}</mat-label> + <input matInput formControlName="appName"> + </mat-form-field> + <div>{{'lang.loginMsg' | translate}} : </div> + <textarea style="padding-top: 10px;" name="loginpage_message" id="loginpage_message" + formControlName="loginpage_message"></textarea> + <br /> + <br /> + <div>{{'lang.homeMsg' | translate}} : </div> + <textarea style="padding-top: 10px;" name="homepage_message" id="homepage_message" + formControlName="homepage_message"></textarea> + <br /> + <br /> + </div> + <div class="col-md-6"> + <div>{{'lang.chooseLogo' | translate}} : </div> + <div> + <mat-card style="width: 350px;background-size: 100%;cursor: pointer;" matRipple> + <img [src]="logoURL()" (click)="clickLogoButton(uploadLogo)" style="width: 100%;" /> + <input type="file" name="files[]" #uploadLogo (change)="uploadTrigger($event, 'logo')" + accept="image/svg+xml" style="display: none;"> + </mat-card> + </div> + <br /> + <div>{{'lang.chooseLoginBg' | translate}} : </div> + <div class="backgroundList"> + <mat-card (click)="selectBg('../rest/images?image=loginPage')" style="opacity: 0.3;" class="backgroundItem" + [class.disabled]="stepFormGroup.controls['bodyLoginBackground'].disabled" + [class.selected]="stepFormGroup.controls['bodyLoginBackground'].value === '../rest/images?image=loginPage'" + style="background:url(../rest/images?image=loginPage);background-size: cover;"> + </mat-card> + <mat-card (click)="selectBg('assets/bodylogin.jpg')" style="opacity: 0.3;" class="backgroundItem" + [class.disabled]="stepFormGroup.controls['bodyLoginBackground'].disabled" + [class.selected]="stepFormGroup.controls['bodyLoginBackground'].value === 'assets/bodylogin.jpg'" + style="background:url(assets/bodylogin.jpg);background-size: cover;"> + </mat-card> + <mat-card *ngFor="let background of backgroundList" (click)="selectBg(background.url)" + style="opacity: 0.3;" class="backgroundItem" + [class.selected]="background.url === stepFormGroup.controls['bodyLoginBackground'].value" + [class.disabled]="stepFormGroup.controls['bodyLoginBackground'].disabled" + [style.background]="'url('+background.url+')'"> + </mat-card> + <mat-card *ngIf="!stepFormGroup.controls['bodyLoginBackground'].disabled" + style="opacity: 0.3;display: flex;align-items: center;justify-content: center;" + class="backgroundItem" (click)="uploadFile.click()"> + <input type="file" name="files[]" #uploadFile (change)="uploadTrigger($event, 'bg')" + accept="image/jpeg" style="display: none;"> + <i class="fa fa-plus" style="font-size: 30px;color: #666;"></i> + </mat-card> + </div> + </div> +</form> \ No newline at end of file diff --git a/src/frontend/app/administration/parameter/customization/parameters-customization.component.scss b/src/frontend/app/administration/parameter/customization/parameters-customization.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..cd0bf27be23d474d5bf4293908c3abf330493a65 --- /dev/null +++ b/src/frontend/app/administration/parameter/customization/parameters-customization.component.scss @@ -0,0 +1,31 @@ +@import '../../../../css/vars.scss'; + +.backgroundList { + display: grid; + grid-template-columns: repeat(5, 1fr); + grid-gap: 10px; +} + +.selected { + transition: all 0.3s; + opacity: 1 !important; + border: solid 10px $secondary !important; +} + +.backgroundItem { + border: solid 0px $secondary; + opacity: 0.5; + transition: all 0.3s; + cursor: pointer; + height: 120px; + background-size: cover !important; +} + +.disabled { + cursor: default !important; +} + +.backgroundItem:not(.disabled):hover { + transition: all 0.3s; + opacity: 1 !important; +} \ No newline at end of file diff --git a/src/frontend/app/administration/parameter/customization/parameters-customization.component.ts b/src/frontend/app/administration/parameter/customization/parameters-customization.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..e50147f4fbf23e19e85df8687ca4b7ced426dd79 --- /dev/null +++ b/src/frontend/app/administration/parameter/customization/parameters-customization.component.ts @@ -0,0 +1,211 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { FormBuilder, FormGroup, Validators, ValidatorFn } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { NotificationService } from '../../../../service/notification/notification.service'; +import { ScanPipe } from 'ngx-pipes'; +import { debounceTime, tap, catchError, exhaustMap } from 'rxjs/operators'; +import { HttpClient } from '@angular/common/http'; +import { of } from 'rxjs/internal/observable/of'; + +declare var tinymce: any; + +@Component({ + selector: 'app-parameters-customization', + templateUrl: './parameters-customization.component.html', + styleUrls: ['./parameters-customization.component.scss'], + providers: [ScanPipe] +}) +export class ParametersCustomizationComponent implements OnInit, OnDestroy { + stepFormGroup: FormGroup; + readonlyState: boolean = false; + + backgroundList: any[] = []; + + constructor( + private translate: TranslateService, + private _formBuilder: FormBuilder, + private notify: NotificationService, + private sanitizer: DomSanitizer, + private scanPipe: ScanPipe, + public http: HttpClient, + ) { + const valIdentifier: ValidatorFn[] = [Validators.pattern(/^[a-zA-Z0-9_\-]*$/), Validators.required]; + + this.stepFormGroup = this._formBuilder.group({ + appName: ['', Validators.required], + loginpage_message: [''], + homepage_message: [''], + bodyLoginBackground: ['../rest/images?image=loginPage'], + uploadedLogo: ['../rest/images?image=logo'], + }); + + this.backgroundList = Array.from({ length: 16 }).map((_, i) => { + return { + filename: `${i + 1}.jpg`, + url: `assets/${i + 1}.jpg`, + }; + }); + } + + async ngOnInit(): Promise<void> { + await this.getParameters(); + } + + getParameters() { + return new Promise((resolve) => { + this.http.get('../rest/parameters').pipe( + tap((data: any) => { + this.stepFormGroup.controls['homepage_message'].setValue(data.parameters.filter((item: any) => item.id === 'homepage_message')[0].value); + this.stepFormGroup.controls['loginpage_message'].setValue(data.parameters.filter((item: any) => item.id === 'loginpage_message')[0].value); + }), + exhaustMap(() => this.http.get('../rest/authenticationInformations')), + tap((data: any) => { + this.stepFormGroup.controls['appName'].setValue(data.applicationName); + setTimeout(() => { + + this.stepFormGroup.controls['appName'].valueChanges.pipe( + debounceTime(500), + tap(() => this.saveParameter('appName')) + ).subscribe(); + + + this.stepFormGroup.controls['homepage_message'].valueChanges.pipe( + debounceTime(500), + tap(() => this.saveParameter('homepage_message')) + ).subscribe(); + + this.stepFormGroup.controls['loginpage_message'].valueChanges.pipe( + debounceTime(500), + tap(() => this.saveParameter('loginpage_message')) + ).subscribe(); + this.initMce(); + }, 0); + }), + catchError((err: any) => { + this.notify.handleSoftErrors(err); + return of(false); + }) + ).subscribe(); + }); + } + + isValidStep() { + return this.stepFormGroup === undefined ? false : this.stepFormGroup.valid; + } + + initMce(readonly = false) { + tinymce.init({ + selector: 'textarea', + setup: (editor) => { + editor.on('keyup', (e) => { + this.stepFormGroup.controls[e.target.dataset.id].setValue(tinymce.get(e.target.dataset.id).getContent()); + }); + }, + base_url: '../node_modules/tinymce/', + height: '150', + suffix: '.min', + language: this.translate.instant('lang.langISO').replace('-', '_'), + language_url: `../node_modules/tinymce-i18n/langs/${this.translate.instant('lang.langISO').replace('-', '_')}.js`, + menubar: false, + statusbar: false, + readonly: readonly, + plugins: [ + 'autolink' + ], + external_plugins: { + 'maarch_b64image': '../../src/frontend/plugins/tinymce/maarch_b64image/plugin.min.js' + }, + toolbar_sticky: true, + toolbar_drawer: 'floating', + toolbar: !readonly ? 'undo redo | fontselect fontsizeselect | bold italic underline strikethrough forecolor | maarch_b64image | \ + alignleft aligncenter alignright alignjustify \ + bullist numlist outdent indent | removeformat' : '' + }); + } + + uploadTrigger(fileInput: any, mode: string) { + if (fileInput.target.files && fileInput.target.files[0]) { + const res = this.canUploadFile(fileInput.target.files[0], mode); + if (res === true) { + const reader = new FileReader(); + + reader.readAsDataURL(fileInput.target.files[0]); + reader.onload = (value: any) => { + if (mode === 'logo') { + this.stepFormGroup.controls['uploadedLogo'].setValue(value.target.result); + } else { + const img = new Image(); + img.onload = (imgDim: any) => { + if (imgDim.target.width < 1920 || imgDim.target.height < 1080) { + this.notify.error(this.scanPipe.transform(this.translate.instant('lang.badImageResolution'), ['1920x1080'])); + } else { + this.backgroundList.push({ + filename: value.target.result, + url: value.target.result, + }); + this.stepFormGroup.controls['bodyLoginBackground'].setValue(value.target.result); + this.saveParameter('bodyLoginBackground'); + } + }; + img.src = value.target.result; + } + }; + } else { + this.notify.error(res); + } + } + } + + canUploadFile(file: any, mode: string) { + const allowedExtension = mode !== 'logo' ? ['image/jpg', 'image/jpeg'] : ['image/svg+xml']; + + if (mode === 'logo') { + if (file.size > 5000000) { + return this.scanPipe.transform(this.translate.instant('lang.maxFileSizeExceeded'), ['5mo']); + } else if (allowedExtension.indexOf(file.type) === -1) { + return this.scanPipe.transform(this.translate.instant('lang.onlyExtensionsAllowed'), [allowedExtension.join(', ')]); + } + } else { + if (file.size > 10000000) { + return this.scanPipe.transform(this.translate.instant('lang.maxFileSizeExceeded'), ['10mo']); + } else if (allowedExtension.indexOf(file.type) === -1) { + return this.scanPipe.transform(this.translate.instant('lang.onlyExtensionsAllowed'), [allowedExtension.join(', ')]); + } + } + return true; + } + + logoURL() { + return this.sanitizer.bypassSecurityTrustUrl(this.stepFormGroup.controls['uploadedLogo'].value); + } + + selectBg(content: string) { + if (!this.stepFormGroup.controls['bodyLoginBackground'].disabled) { + this.stepFormGroup.controls['bodyLoginBackground'].setValue(content); + this.saveParameter('bodyLoginBackground'); + } + } + + clickLogoButton(uploadLogo: any) { + if (!this.stepFormGroup.controls['uploadedLogo'].disabled) { + uploadLogo.click(); + } + } + + saveParameter(parameterId: string) { + const param = { + param_value_string: this.stepFormGroup.controls[parameterId].value + }; + this.http.put('../rest/parameters/' + parameterId, param) + .subscribe(() => { + this.notify.success(this.translate.instant('lang.parameterUpdated')); + }, (err) => { + this.notify.error(err.error.errors); + }); + } + + ngOnDestroy(): void { + tinymce.remove(); + } +} diff --git a/src/frontend/app/administration/parameter/parameters-administration.component.html b/src/frontend/app/administration/parameter/parameters-administration.component.html index 3c1b00e3fdef4137b9092e5c46157fa9fc590b27..22243e809147429eea76517cb11979ca3fbed343 100755 --- a/src/frontend/app/administration/parameter/parameters-administration.component.html +++ b/src/frontend/app/administration/parameter/parameters-administration.component.html @@ -29,52 +29,66 @@ <mat-spinner style="margin:auto;"></mat-spinner> </div> <mat-card *ngIf="!loading" class="card-app-content"> - <div class="row"> - <div class="col-md-6 col-xs-6"> - <mat-form-field> - <input matInput [formControl]="adminService.getFilterField()" placeholder="{{'lang.filterBy' | translate}}"> - </mat-form-field> - </div> - <div class="col-md-6 col-xs-6"> - <mat-paginator #paginator [length]="100" [hidePageSize]="true" [pageSize]="10"> - </mat-paginator> - </div> - </div> - <mat-table #table [dataSource]="adminService.getDataSource()" matSort matSortDisableClear> - <ng-container matColumnDef="id"> - <mat-header-cell *matHeaderCellDef mat-sort-header>{{'lang.id' | translate}}</mat-header-cell> - <mat-cell *matCellDef="let element"> - {{element.id}} </mat-cell> - </ng-container> - <ng-container matColumnDef="description"> - <mat-header-cell *matHeaderCellDef mat-sort-header - [class.hide-for-mobile]="appService.getViewMode()">{{'lang.description' | translate}} - </mat-header-cell> - <mat-cell *matCellDef="let element" [class.hide-for-mobile]="appService.getViewMode()"> - {{element.description}} </mat-cell> - </ng-container> - <ng-container matColumnDef="value"> - <mat-header-cell *matHeaderCellDef mat-sort-header>{{'lang.value' | translate}}</mat-header-cell> - <mat-cell *matCellDef="let element"> - {{element.value}} </mat-cell> - </ng-container> - <ng-container matColumnDef="actions"> - <mat-header-cell *matHeaderCellDef></mat-header-cell> - <mat-cell *matCellDef="let element" style="justify-content: flex-end;"> - <button mat-icon-button color="warn" matTooltip="{{'lang.delete' | translate}}" - (click)="$event.stopPropagation();deleteParameter(element.id)"> - <mat-icon class="fa fa-trash-alt fa-2x" aria-hidden="true"></mat-icon> - </button> - </mat-cell> - </ng-container> - <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> - <mat-row *matRowDef="let row; columns: displayedColumns;" - routerLink="/administration/parameters/{{row.id}}" style="cursor:pointer;" - matTooltip="{{'lang.view' | translate}}"></mat-row> - </mat-table> - <div class="mat-paginator" - style="min-height:48px;min-height: 48px;display: flex;justify-content: end;align-items: center;padding-right: 20px;"> - {{parameters.length}} {{'lang.parameters' | translate}}</div> + <mat-tab-group> + <mat-tab [label]="'lang.advancedParameters' | translate"> + <div class="row"> + <div class="col-md-6 col-xs-6"> + <mat-form-field> + <input matInput [formControl]="adminService.getFilterField()" + placeholder="{{'lang.filterBy' | translate}}"> + </mat-form-field> + </div> + <div class="col-md-6 col-xs-6"> + <mat-paginator #paginator [length]="100" [hidePageSize]="true" [pageSize]="10"> + </mat-paginator> + </div> + </div> + <mat-table #table [dataSource]="adminService.getDataSource()" matSort matSortDisableClear> + <ng-container matColumnDef="id"> + <mat-header-cell *matHeaderCellDef mat-sort-header>{{'lang.id' | translate}} + </mat-header-cell> + <mat-cell *matCellDef="let element"> + {{element.id}} </mat-cell> + </ng-container> + <ng-container matColumnDef="description"> + <mat-header-cell *matHeaderCellDef mat-sort-header + [class.hide-for-mobile]="appService.getViewMode()"> + {{'lang.description' | translate}} + </mat-header-cell> + <mat-cell *matCellDef="let element" + [class.hide-for-mobile]="appService.getViewMode()"> + {{element.description}} </mat-cell> + </ng-container> + <ng-container matColumnDef="value"> + <mat-header-cell *matHeaderCellDef mat-sort-header>{{'lang.value' | translate}} + </mat-header-cell> + <mat-cell *matCellDef="let element"> + {{element.value}} </mat-cell> + </ng-container> + <ng-container matColumnDef="actions"> + <mat-header-cell *matHeaderCellDef></mat-header-cell> + <mat-cell *matCellDef="let element" style="justify-content: flex-end;"> + <button mat-icon-button color="warn" matTooltip="{{'lang.delete' | translate}}" + (click)="$event.stopPropagation();deleteParameter(element.id)"> + <mat-icon class="fa fa-trash-alt fa-2x" aria-hidden="true"></mat-icon> + </button> + </mat-cell> + </ng-container> + <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> + <mat-row *matRowDef="let row; columns: displayedColumns;" + routerLink="/administration/parameters/{{row.id}}" style="cursor:pointer;" + matTooltip="{{'lang.view' | translate}}"></mat-row> + </mat-table> + <div class="mat-paginator" + style="min-height:48px;min-height: 48px;display: flex;justify-content: end;align-items: center;padding-right: 20px;"> + {{parameters.length}} {{'lang.parameters' | translate}}</div> + </mat-tab> + <mat-tab [label]="'lang.customization' | translate"> + <ng-template matTabContent> + <app-parameters-customization></app-parameters-customization> + </ng-template> + </mat-tab> + </mat-tab-group> </mat-card> </div> </div> diff --git a/src/frontend/app/administration/parameter/parameters-administration.component.ts b/src/frontend/app/administration/parameter/parameters-administration.component.ts index 17bf432a234aa41ad8af936470d2cc6c141ef764..34ac27b17506ff0f00ad0c014a13bc4deb808164 100755 --- a/src/frontend/app/administration/parameter/parameters-administration.component.ts +++ b/src/frontend/app/administration/parameter/parameters-administration.component.ts @@ -49,7 +49,7 @@ export class ParametersAdministrationComponent implements OnInit { this.http.get('../rest/parameters') .subscribe((data: any) => { - this.parameters = data.parameters; + this.parameters = data.parameters.filter((item: any) => ['homepage_message', 'loginpage_message'].indexOf(item.id) === -1); this.loading = false; setTimeout(() => { this.adminService.setDataSource('admin_parameters', this.parameters, this.sort, this.paginator, this.filterColumns); @@ -63,7 +63,7 @@ export class ParametersAdministrationComponent implements OnInit { if (r) { this.http.delete('../rest/parameters/' + paramId) .subscribe((data: any) => { - this.parameters = data.parameters; + this.parameters = data.parameters.filter((item: any) => ['homeMessage', 'loginMessage'].indexOf(item) === -1); this.adminService.setDataSource('admin_parameters', this.parameters, this.sort, this.paginator, this.filterColumns); this.notify.success(this.translate.instant('lang.parameterDeleted')); }, (err) => { diff --git a/src/lang/lang-en.json b/src/lang/lang-en.json index 976e618c99955ceb1857b0b1de355c7ecfb6e02e..98ea157f648f96f7cfe1fd2d2a85a80a429f560d 100644 --- a/src/lang/lang-en.json +++ b/src/lang/lang-en.json @@ -1895,5 +1895,6 @@ "usersExport": "Export users", "hotkeyInfo": "Hold <b>SHIFT</b> to propagate your selections to sub-elements", "hotkeyMsg": "<b>SHIFT</b> Propagation to sub-elements", - "hotkeyTitle": "Hold the shift key" + "hotkeyTitle": "Hold the shift key", + "advancedParameters": "Advanced parameters" } \ No newline at end of file diff --git a/src/lang/lang-fr.json b/src/lang/lang-fr.json index 0cd6bdfb47055e4c7e2a026fcdc1df34917fa0cc..89dec32be4ad0934171abd38cbe34dc29888bb0a 100644 --- a/src/lang/lang-fr.json +++ b/src/lang/lang-fr.json @@ -1889,5 +1889,6 @@ "infoImportusers2": "Les colonnes <b>id</b> et <b>user_id</b> ne sont pas prises en compte lors de mise à jour d'utilisateurs", "hotkeyInfo": "Maintenez <b>SHIFT</b> pour propager vos selections aux sous-éléments", "hotkeyMsg": "<b>SHIFT</b> Propagation aux sous-élements", - "hotkeyTitle": "Maintient de la touche shift" + "hotkeyTitle": "Maintient de la touche shift", + "advancedParameters": "Paramètres avancés" } diff --git a/src/lang/lang-nl.json b/src/lang/lang-nl.json index da04095b780024ca952f6b7f6e26e15e30e5b05d..b9419eca673fb4d0751b99a8d0a2c84d73e45142 100644 --- a/src/lang/lang-nl.json +++ b/src/lang/lang-nl.json @@ -1906,5 +1906,6 @@ "usersExport": "Exporter des utilisateurs__TO_TRANSLATE", "hotkeyInfo": "Maintenez <b>SHIFT</b> pour propager vos selections aux sous-éléments__TO_TRANSLATE", "hotkeyMsg": "<b>SHIFT</b> Propagation aux sous-élements__TO_TRANSLATE", - "hotkeyTitle": "Maintient de la touche shift__TO_TRANSLATE" + "hotkeyTitle": "Maintient de la touche shift__TO_TRANSLATE", + "advancedParameters": "Paramètres avancés__TO_TRANSLATE" } \ No newline at end of file