diff --git a/src/app/components/formats/detail/format-detail.component.html b/src/app/components/formats/detail/format-detail.component.html index 5b979c6ae2423ed464967f896ab6b68b29ead171..064569e10d7d0053978eb236b6998d0667cefdc2 100644 --- a/src/app/components/formats/detail/format-detail.component.html +++ b/src/app/components/formats/detail/format-detail.component.html @@ -27,4 +27,4 @@ </div> </div> </section> -</ng-container> +</ng-container> \ No newline at end of file diff --git a/src/app/components/formats/edit/format-form.component.html b/src/app/components/formats/edit/format-form.component.html index 767de654e6e0e0bf0f059b5312a9627c431aa551..dc9588a857641d4e0dcaff8b6343ca0fc2d66df4 100644 --- a/src/app/components/formats/edit/format-form.component.html +++ b/src/app/components/formats/edit/format-form.component.html @@ -1,6 +1,6 @@ <ng-container *ngIf="format"> - <app-back-button [route]="'/organizations'" [title]="'Retourner à la liste des formats'"></app-back-button> + <app-back-button [route]="'/formats'" [title]="'Retourner à la liste des formats'"></app-back-button> <h1>{{ title }}</h1> @@ -21,9 +21,15 @@ </div> <div class="field"> - <label class="label" for="mapServerType">Type MapServer</label> + <label class="label required" for="mapServerType">Type MapServer</label> <div class="control"> - <input class="input" type="text" [value]="format.mapServerType" formControlName="mapServerType" id="mapServerType"> + <input class="input" type="text" [value]="format.mapServerType" formControlName="mapServerType" + id="mapServerType"> + </div> + <div *ngIf="mapServerType.invalid && (mapServerType.dirty || mapServerType.touched)" class="alert alert-danger"> + <p *ngIf="mapServerType.errors['required']" class="help is-danger"> + Le nom du type MapServer est obligatoire. + </p> </div> </div> @@ -32,4 +38,4 @@ </div> </div> </form> -</ng-container> +</ng-container> \ No newline at end of file diff --git a/src/app/components/resources/detail/resource-detail.component.html b/src/app/components/resources/detail/resource-detail.component.html index 0c2b050155f08a112305ee8f7dd6b1c731a00926..ef28fb56a7711bb21f218d2385816738255c32df 100644 --- a/src/app/components/resources/detail/resource-detail.component.html +++ b/src/app/components/resources/detail/resource-detail.component.html @@ -12,7 +12,6 @@ </p> </header> <div class="card-content"> - <div class="content"> <p> <span class="has-text-weight-bold">Id: </span> @@ -20,67 +19,88 @@ </p> <p> <span class="has-text-weight-bold">Acronyme: </span> - <span>{{resource.acronym}}</span> + <span *ngIf="resource.acronym; else emptyAcronymWarning">{{resource.acronym}}</span> + <ng-template #emptyAcronymWarning> + <span class="empty-property">Non renseigné</span> + </ng-template> </p> + <p> <span class="has-text-weight-bold">Type: </span> - <span>{{resource.type}}</span> + <span *ngIf="resource.type; else emptyTypeWarning">{{resource.type}}</span> + <ng-template #emptyTypeWarning> + <span class="empty-property">Non renseigné</span> + </ng-template> </p> <div *ngIf="resource.description"> - <p class="has-text-weight-bold">Description:</p> - <p>{{resource.description}}</p> + <span class="has-text-weight-bold">Description: </span> + <span *ngIf="resource.description; else emptyDescriptionWarning">{{resource.description}}</span> + <ng-template #emptyDescriptionWarning> + <span class="empty-property">Non renseigné</span> + </ng-template> </div> <br> <p> - <span class="has-text-weight-bold">Requêtable:</span> - <span class="icon has-text-success" *ngIf="resource.isQueryable"> - <i class="far fa-check-circle"></i> - </span> - <span class="icon has-text-danger" *ngIf="!resource.isQueryable"> - <i class="far fa-times-circle"></i> + <span class="has-text-weight-bold">Requêtable: </span> + <span class="icon has-text-success" + [ngClass]="{'has-text-success': resource.isQueryable, 'has-text-danger': !resource.isQueryable}"> + <i class="far fa-check-circle" + [ngClass]="{'fa-check-circle': resource.isQueryable, 'fa-times-circle': !resource.isQueryable}"></i> </span> </p> <p> - <span class="has-text-weight-bold">Téléchargeable:</span> - <span class="icon has-text-success" *ngIf="resource.isDownloadable"> - <i class="far fa-check-circle"></i> - </span> - <span class="icon has-text-danger" *ngIf="!resource.isDownloadable"> - <i class="far fa-times-circle"></i> + <span class="has-text-weight-bold">Téléchargeable: </span> + <span class="icon has-text-success" + [ngClass]="{'has-text-success': resource.isDownloadable, 'has-text-danger': !resource.isDownloadable}"> + <i class="far fa-check-circle" + [ngClass]="{'fa-check-circle': resource.isDownloadable, 'fa-times-circle': !resource.isDownloadable}"></i> </span> </p> <p> - <span class="has-text-weight-bold">Standardisé:</span> - <span class="icon has-text-success" *ngIf="resource.isStandard"> - <i class="far fa-check-circle"></i> - </span> - <span class="icon has-text-danger" *ngIf="!resource.isStandard"> - <i class="far fa-times-circle"></i> + <span class="has-text-weight-bold">Standardisé: </span> + <span class="icon has-text-success" + [ngClass]="{'has-text-success': resource.isStandard, 'has-text-danger': !resource.isStandard}"> + <i class="far fa-check-circle" + [ngClass]="{'fa-check-circle': resource.isStandard, 'fa-times-circle': !resource.isStandard}"></i> </span> </p> - <div *ngIf="resource.description"> - <p class="has-text-weight-bold">Parametres URL:</p> - <p>{{resource.parametersUrl}}</p> + <div *ngIf="resource.parametersUrl"> + <span class="has-text-weight-bold">Parametres URL: </span> + <span *ngIf="resource.parametersUrl; else emptyParameterUrlWarning">{{resource.parametersUrl}}</span> + <ng-template #emptyParameterUrlWarning> + <span class="empty-property">Non renseigné</span> + </ng-template> </div> <br> - <div *ngIf="resource.description"> - <p class="has-text-weight-bold">Message d'alerte:</p> - <p>{{resource.messageWarning}}</p> + <div *ngIf="resource.messageWarning"> + <span class="has-text-weight-bold">Message d'alerte: </span> + <span *ngIf="resource.messageWarning; else emptyMessageWarning">{{resource.messageWarning}}</span> + <ng-template #emptyMessageWarning> + <span class="empty-property">Non renseigné</span> + </ng-template> </div> <br> - <!-- <div> + <div> <span class="has-text-weight-bold">Formats de sortie: </span> - <span *ngFor="let format of resource.outputFormats; let isLast=last"> - {{ format }}{{ isLast ? '' : ',' }} - </span> - </div> --> + <ul + *ngIf="resource.resourceFormats && resource.resourceFormats.length > 0; else noResourceFormatTemplate"> + <li *ngFor="let resourceFormat of resource.resourceFormats"> + {{ resourceFormat.format.name }}<span *ngIf="resourceFormat.isCuttable">, découpable</span><span + *ngIf="resourceFormat.isProjectable">, projectable</span> + </li> + </ul> + <ng-template #noResourceFormatTemplate> + <span class="empty-property">Non renseigné</span> + </ng-template> + </div> + </div> </div> @@ -92,4 +112,4 @@ </section> -</ng-container> +</ng-container> \ No newline at end of file diff --git a/src/app/components/resources/detail/resource-detail.component.scss b/src/app/components/resources/detail/resource-detail.component.scss index 163f99f474dc5696a1670be5273eef97a2efbd39..05012fa666610636fa011ff7250fcdc78764b468 100644 --- a/src/app/components/resources/detail/resource-detail.component.scss +++ b/src/app/components/resources/detail/resource-detail.component.scss @@ -1,3 +1,8 @@ .card-header-title { justify-content: center; -} \ No newline at end of file +} + +.empty-property { + font-style: italic; + color: #818080; +} diff --git a/src/app/components/resources/edit/resource-form.component.html b/src/app/components/resources/edit/resource-form.component.html index 7ecde6470aa1a968ef2be4992fb01051c340d2fd..9e24f8e03eb9db223d6f6fd09aaab963021582af 100644 --- a/src/app/components/resources/edit/resource-form.component.html +++ b/src/app/components/resources/edit/resource-form.component.html @@ -103,13 +103,73 @@ </div> </div> - - <!-- <div class="field"> - <label class="label" for="outputFormats">Formats de sortie (Séparer chaque format par une virgule)</label> - <div class="control"> - <input class="input" type="text" formControlName="outputFormats" id="outputFormats"> + <div class="field links"> + <div class="columns"> + <div class="column is-11"> + <label class="label">Formats</label> + </div> + <div class="column is-1"> + <span class="icon" (click)="addResourceFormat()" title="Ajouter un format"> + <i class="fas fa-plus"></i> + </span> + </div> + </div> + <div formArrayName="resourceFormats"> + <div *ngFor="let resourceFormat of formResourceFormats.controls; let i = index;" [formGroupName]="i" + class="columns is-multiline"> + <div class="field column is-5"> + <label class="label required" for="format">Format</label> + <div class="control"> + <div class="select"> + <select formControlName="formatId"> + <option [ngValue]="format.id" *ngFor="let format of formatsList"> {{format.name}}</option> + </select> + </div> + </div> + <div + *ngIf="resourceFormat['controls'].formatId.invalid && (resourceFormat['controls'].formatId.dirty || resourceFormat['controls'].formatId.touched)" + class="alert alert-danger"> + <p *ngIf="resourceFormat.hasError('required', 'format')" class="help is-danger"> + Le format est requis. + </p> + </div> + </div> + <div class="field column is-3"> + <label class="label required" for="isProjectable">Projectable</label> + <div class="control"> + <label class="radio"> + <input type="radio" formControlName="isProjectable" [value]="1"> + Oui + </label> + <label class="radio"> + <input type="radio" formControlName="isProjectable" [value]="0"> + Non + </label> + </div> + </div> + <div class="field column is-3"> + <label class="label required" for="isCuttable">Découpable</label> + <div class="control"> + <label class="radio"> + <input type="radio" formControlName="isCuttable" [value]="1"> + Oui + </label> + <label class="radio"> + <input type="radio" formControlName="isCuttable" [value]="0"> + Non + </label> + </div> + </div> + + <div class="column is-1"> + <span class="icon" (click)="removeResourceFormat(i)" title="Supprimer le format"> + <i class="fas fa-trash"></i> + </span> + </div> + </div> </div> - </div> --> + </div> + <br> <div class="has-text-right"> @@ -118,4 +178,4 @@ </div> </form> -</ng-container> +</ng-container> \ No newline at end of file diff --git a/src/app/components/resources/edit/resource-form.component.ts b/src/app/components/resources/edit/resource-form.component.ts index 2448c74d4f952a4d018b58754d381a14ccd7c022..e4756d8f7e5aa3cf6eea82fce3203cf6d037f8c9 100644 --- a/src/app/components/resources/edit/resource-form.component.ts +++ b/src/app/components/resources/edit/resource-form.component.ts @@ -1,9 +1,12 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; -import { Resource } from 'src/app/models/resource.model'; +import { Resource, IResource } from 'src/app/models/resource.model'; import { ResourceService } from 'src/app/services/resource.service'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { filter, switchMap } from 'rxjs/operators'; +import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms'; +import { filter, switchMap, mergeMap, merge, concatMap, map } from 'rxjs/operators'; +import { Format } from 'src/app/models/format.model'; +import { FormatService } from 'src/app/services'; +import { from, forkJoin, Observable, of } from 'rxjs'; @Component({ selector: 'app-resource-form', @@ -13,24 +16,30 @@ import { filter, switchMap } from 'rxjs/operators'; export class ResourceFormComponent implements OnInit { resource: Resource = new Resource(); + formatsList: Format[] = []; form: FormGroup; title: string; constructor( - private resourceService: ResourceService, - private route: ActivatedRoute, - private router: Router, + private _resourceService: ResourceService, + private _formatService: FormatService, + private _route: ActivatedRoute, + private _router: Router, private _fb: FormBuilder, ) { } ngOnInit() { - this.title = this.route.snapshot.data.title; + this.title = this._route.snapshot.data.title; this.initForm(); - this.route.paramMap.pipe( + this._formatService.getAllFormats().subscribe((res) => { + this.formatsList = res; + }); + + this._route.paramMap.pipe( filter((paramMap: ParamMap) => (paramMap.get('id') !== null)), - switchMap((paramMap: ParamMap) => this.resourceService.findById(paramMap.get('id')))) + switchMap((paramMap: ParamMap) => this._resourceService.findById(paramMap.get('id')))) .subscribe((resource: Resource) => { this.resource = resource; @@ -41,8 +50,19 @@ export class ResourceFormComponent implements OnInit { } initForm() { + const resourceFormats = new FormArray([]); + this.resource.resourceFormats.forEach((resourceFormat) => { + resourceFormats.push(this._fb.group({ + formatId: [resourceFormat.format.id, Validators.required], + isProjectable: resourceFormat.isProjectable, + isCuttable: resourceFormat.isCuttable, + id: resourceFormat.id, + })); + }); + this.form = this._fb.group( { + resourceFormats, id: [this.resource.id], name: [this.resource.name, Validators.required], acronym: [this.resource.acronym], @@ -58,17 +78,45 @@ export class ResourceFormComponent implements OnInit { onSubmit() { if (!this.formInvalid) { - this.resource = new Resource(this.form.value); - // this.resource.outputFormats = this.form.value.outputFormats.split(','); - this.resourceService.replaceOrCreate(this.resource) - .subscribe( - (resourceCreated) => { - this.router.navigate(['/resources', resourceCreated.id]); - }, - (err) => { - alert(err.message); - }, - ); + const resourceFormats = this.form.controls.resourceFormats['controls'].filter(e => e.dirty).map(e => e.value); + const newResource = new Resource(this.form.value); + let savedResource: Resource; + this._resourceService.replaceOrCreate(newResource).pipe( + mergeMap((resource) => { + savedResource = resource; + let actions: Observable<any>[] = []; + + const toAdd = resourceFormats.filter(e => !e.id); + const toUpdate = resourceFormats.filter(e => e.id) + const toDelete = this.resource.resourceFormats.map(e => e.id).filter((id) => { + return !this.form.controls.resourceFormats['controls'].map(e => e.value.id).find(rfId => rfId === id); + }); + + if (toAdd.length > 0) { + actions.push(this._resourceService.createResourceFormats(resource.id, toAdd)); + } + + if (toUpdate.length > 0) { + actions.push(this._resourceService.updateResourceFormats(resource.id, toUpdate)); + } + + if (toDelete.length > 0) { + actions.push(this._resourceService.deleteResourceFormats(resource.id, toDelete)); + } + + let res; + (actions.length > 0) ? res = forkJoin(actions) : res = of(null); + + return res; + }), + map(() => savedResource.id), + ).subscribe( + (resourceCreatedId) => { + this._router.navigate(['/resources', resourceCreatedId]); + }, + (err) => { + }, + ); } } @@ -80,6 +128,25 @@ export class ResourceFormComponent implements OnInit { return val; } + addResourceFormat() { + // if (!this.form.controls.links) { + // this.organization.links = []; + // } + (this.form.controls.resourceFormats as FormArray).push(this._fb.group({ + formatId: [null, Validators.required], + isProjectable: 0, + isCuttable: 0, + })); + } + + removeResourceFormat(index) { + (this.form.controls.resourceFormats as FormArray).removeAt(index); + } + + compareFormats(format1: Format, format2: Format) { + return format1 && format2 ? format1.id === format2.id : false; + } + // Getters for each property get name() { return this.form.controls['name']; @@ -97,6 +164,10 @@ export class ResourceFormComponent implements OnInit { return this.form.controls['description']; } + get formResourceFormats() { + return this.form.controls.resourceFormats as FormArray; + } + // get outputFormats() { // return this.form.controls['outputFormats']; // } diff --git a/src/app/components/resources/list/resources.component.html b/src/app/components/resources/list/resources.component.html index 534c762155075494584a8cee050d2585918db5bc..67f84ebc6389a162554ef6300e57f97b2ccc1b1c 100644 --- a/src/app/components/resources/list/resources.component.html +++ b/src/app/components/resources/list/resources.component.html @@ -15,7 +15,7 @@ <div class="header columns is-marginless"> <div class="column is-2 has-text-centered"> <span (click)="sortBy('name')" class="is-sortable"> - <span class="column-title" [ngClass]="{'active': sortOptions.value === name}">Name</span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === name}">Nom</span> <span *ngIf="sortOptions.value === 'name'" class="has-text-danger"> <i class="fas sort-order-icon" [ngClass]="{'fa-arrow-up': sortOptions.order === 'asc', 'fa-arrow-down': sortOptions.order === 'desc'}"></i> @@ -102,4 +102,4 @@ </div> </div> -</ng-container> +</ng-container> \ No newline at end of file diff --git a/src/app/models/resource.model.ts b/src/app/models/resource.model.ts index 17a726e9dde8a617d4264bcef4b1ac984a420899..fe8df0a3601619cca902abe6bc1a3a2f3f975eb9 100644 --- a/src/app/models/resource.model.ts +++ b/src/app/models/resource.model.ts @@ -1,5 +1,7 @@ +import { Format } from './format.model'; + export class Resource { - id?: number; + id?: string; name: string; acronym: string; type: string; @@ -9,8 +11,10 @@ export class Resource { isStandard: number; parametersUrl: string; messageWarning: string; + resourceFormats: IResourceFormat[]; constructor(resource?: IResource) { + if (resource) { if (resource.id) { this.id = resource.id; @@ -24,6 +28,7 @@ export class Resource { this.isStandard = resource.isStandard; this.parametersUrl = resource.parametersUrl; this.messageWarning = resource.messageWarning; + this.resourceFormats = resource.resourceFormats; } else { this.name = ''; this.acronym = ''; @@ -34,6 +39,7 @@ export class Resource { this.isStandard = 0; this.parametersUrl = ''; this.messageWarning = ''; + this.resourceFormats = []; } } } @@ -49,7 +55,7 @@ export class ResourceRO { } export interface IResource { - id: number; + id: string; name: string; acronym: string; type: string; @@ -59,11 +65,13 @@ export interface IResource { isStandard: number; parametersUrl: string; messageWarning: string; + resourceFormats: IResourceFormat[]; } -interface ILink { - id?: number; - name: string; - url: string; - organizationId: number; +export interface IResourceFormat { + format: Format; + formatId: string; + isProjectable: number; + isCuttable: number; + id?: string; } diff --git a/src/app/services/format.service.ts b/src/app/services/format.service.ts index b6186961049a0818ffee9dd07cc21045c48ce63d..722f25a8a0188f9c85742e343b50e66f59a167c7 100644 --- a/src/app/services/format.service.ts +++ b/src/app/services/format.service.ts @@ -26,7 +26,7 @@ export class FormatService { this.pageNumber = 1; } - getFormats(options?): Observable<FormatRO> { + getFormats(): Observable<FormatRO> { let query = '?'; query += `limit=${(this.limit ? this.limit : 20)}`; query += `&offset=${(this.pageNumber ? (this.pageNumber - 1) * this.limit : 0)}`; @@ -43,6 +43,12 @@ export class FormatService { })); } + getAllFormats(): Observable<Format[]> { + return this._httpClient.get<IFormat[]>(this.formatServiceUrl).pipe( + map(body => body.map(format => new Format(format))), + ); + } + findById(id): Observable<Format> { return this._httpClient.get<IFormat>(this.formatServiceUrl + id).pipe( map((response) => { diff --git a/src/app/services/resource.service.ts b/src/app/services/resource.service.ts index aa1f84743918583023c4ad43acc9ecdb5a80a936..b598eb31653bfe0f7fd8005c2d93a78bb996abf5 100644 --- a/src/app/services/resource.service.ts +++ b/src/app/services/resource.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; -import { Observable, Subject } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { Observable, Subject, from } from 'rxjs'; +import { map, concatMap } from 'rxjs/operators'; import { HttpClient } from '@angular/common/http'; -import { Resource, IResource, ResourceRO } from '../models/resource.model'; +import { Resource, IResource, ResourceRO, IResourceFormat } from '../models/resource.model'; import { APP_CONFIG } from './app-config.service'; @Injectable() @@ -26,7 +26,7 @@ export class ResourceService { this.pageNumber = 1; } - getResources(options?): Observable<ResourceRO> { + getResources(): Observable<ResourceRO> { let query = '?'; query += `limit=${(this.limit ? this.limit : 20)}`; query += `&offset=${(this.pageNumber ? (this.pageNumber - 1) * this.limit : 0)}`; @@ -70,6 +70,30 @@ export class ResourceService { ); } + createResourceFormats(resourceId: string, resourceFormats: IResourceFormat[]): Observable<IResourceFormat> { + return from(resourceFormats).pipe( + concatMap((rf) => { + return this._httpClient.post<IResourceFormat>(`${this.resourceServiceUrl}${resourceId}/formats`, rf); + }), + ); + } + + updateResourceFormats(resourceId: string, resourceFormats: IResourceFormat[]) { + return from(resourceFormats).pipe( + concatMap((rf) => { + return this._httpClient.put<IResourceFormat>(`${this.resourceServiceUrl}${resourceId}/formats/${rf.id}`, rf); + }), + ); + } + + deleteResourceFormats(resourceId: string, resourceFormatsId: string[]) { + return from(resourceFormatsId).pipe( + concatMap((rfId) => { + return this._httpClient.delete<IResourceFormat>(`${this.resourceServiceUrl}${resourceId}/formats/${rfId}`); + }), + ); + } + /* PAGINATION */ paginationChanged(limit: number, pageNumber: number) { this.limit = limit;