Skip to content
Commits on Source (7)
......@@ -2,6 +2,8 @@
All notable changes to [ngpr_prodige_contribution](https://gitlab.adullact.net/prodige/ngpr_prodige_contribution) project will be documented in this file.
## [5.0.9](https://gitlab.adullact.net/prodige/ngpr_prodige_contribution/compare/5.0.8...5.0.9) - 2023-03-23
## [5.0.8](https://gitlab.adullact.net/prodige/ngpr_prodige_contribution/compare/5.0.7...5.0.8) - 2023-03-21
## [5.0.7](https://gitlab.adullact.net/prodige/ngpr_prodige_contribution/compare/5.0.6...5.0.7) - 2023-03-20
......
5.0.8
\ No newline at end of file
5.0.9
\ No newline at end of file
......@@ -2,7 +2,9 @@
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 13.0.1.
to install see cicd/dev/README.md
### Installation
[documentation d'insatllation en développement](../cicd/dev/README.md).
## Development server
......
......@@ -8,6 +8,7 @@
"start": "ng serve",
"build": "ng build",
"lint": "eslint \"src/app/**/*.ts\" --fix",
"lint_sonar": "eslint \"src/app/**/*.ts\" -f json -o report.json",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"e2e": "ng e2e",
......
......@@ -15,7 +15,7 @@ const routes: Routes = [
pathMatch: `full`,
canActivate: [CasGuard],
loadChildren: () => import( `./pages/home/home.module` ).then(({ HomeModule }) => HomeModule ),
resolve: [ContributionResolver],
resolve: { contribution: ContributionResolver },
}, {
path: `contribute/form/:id`,
canActivate: [CasGuard],
......
......@@ -4,17 +4,19 @@
<thead>
<tr>
<ng-container *ngFor="let field of this.fields | fieldFilter">
<th >{{field.alias}}</th>
<th >{{field.alias ? field.alias : field.name}}</th>
</ng-container>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let geoFeature of this.geoFeatures ; let i = index">
<tr >
<ng-container *ngFor="let field of this.fields | fieldFilter">
<td>{{geoFeature.properties[field.name]}}</td>
</ng-container>
</tr>
<ng-container *ngIf="withValue">
<ng-container *ngFor="let geoFeature of this.geoFeatures ; let i = index">
<tr >
<ng-container *ngFor="let field of this.fields | fieldFilter">
<td>{{geoFeature.properties[field.name]}}</td>
</ng-container>
</tr>
</ng-container>
</ng-container>
</tbody>
</table>
......
......@@ -16,6 +16,7 @@ export class FieldDisplayComponent extends BaseComponent implements OnInit {
@Input() fields: ContributionField[];
@Input() mapFile: string;
@Input() layerId: string;
@Input() withValue: boolean;
public geoFeatures: GeoFeature[] = [];
......
......@@ -31,6 +31,9 @@
<p class="my-0 text-danger">Valeur invalide</p>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'uniqTableName'">
<p class="my-0 text-danger">Le nom de table déjà utilisé</p>
</ng-container>
</ng-container>
</ng-container>
</div>
......
<ng-template #content let-modal>
<div class="modal-header">
<h5 class="modal-title">Confirmation</h5>
<button type="button" class="close" aria-label="Close" (click)="modal.close">
<button type="button" class="close" aria-label="Close" (click)="modal.close()">
<span aria-hidden="true">&times;</span>
</button>
</div>
......@@ -9,6 +9,6 @@
<p>{{message}}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" (click)="modal.close()">Ok</button>
<button type="button" class="btn btn-danger" i18n="@@Close" (click)="modal.close()">Fermer</button>
</div>
</ng-template>
<div class="modal-header">
<h5 class="modal-title">Confirmation</h5>
<button type="button" class="close" aria-label="Close" >
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<a class="nav-link btn btn-light" [href]="" i18n="@@checkInExpertMode">
Vérifier en mode expert
</a>
<a class="nav-link btn btn-light " i18n="@@ParameterDefaultRepresentation">
Paramétrer la représentation par défaut
</a>
<a class="nav-link btn btn-light " i18n="@@importOtherDataset">
Importer un autre jeu de données
</a>
<a class="nav-link btn btn-light " i18n="@@ContributionList">
Retourner à la liste des contributions
</a>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" i18n="@@Close">Fermer</button>
<a type="button" class="nav-link btn btn-light " i18n="@@Close">
Fermer
</a>
</div>
<ng-template #content let-modal>
<div class="modal-header">
<h5 class="modal-title">Confirmation</h5>
<button type="button" class="close" aria-label="Close" (click)="modal.close()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<a class="nav-link btn btn-light" [href]="this.url['expertMode']" i18n="@@checkInExpertMode">
Vérifier en mode expert
</a>
<a class="nav-link btn btn-light " [href]="this.url['defaultRepresentation']" i18n="@@ParameterDefaultRepresentation">
Paramétrer la représentation par défaut
</a>
<a class="nav-link btn btn-light " [href]="this.url['importOtherDataset']" i18n="@@importOtherDataset">
Importer un autre jeu de données
</a>
<a class="nav-link btn btn-light " [href]="this.url['contributionList']" i18n="@@ContributionList">
Retourner à la liste des contributions
</a>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" (click)="modal.close()" i18n="@@Close">Fermer</button>
</div>
</ng-template>
import { Component } from '@angular/core';
import { Component, ElementRef, ViewChild } from '@angular/core';
import { EnvService } from '../../services/env/env.service';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
@Component({
selector: `alk-modal-end-popin`,
......@@ -7,12 +8,30 @@ import { EnvService } from '../../services/env/env.service';
styleUrls: [`./modal-end-popin.component.scss`],
})
export class ModalEndPopinComponent {
@ViewChild( `content` ) modalContent: ElementRef;
private modalRef: NgbModalRef = null;
public url: {[key:string]: string} = {};
constructor(
private ngbModal: NgbModal,
private envService: EnvService,
) {
this.url[`expertMode`] = `${this.envService.catalogueUrl}/geonetwork/srv/fre/catalog.search#/metadata/ce0fea22-9fff-4022-8d62-7994333cbc62`;
this.url[`importOtherDataset`] = `${this.envService.urlContribution}`;
this.url[`contributionList`] = `${this.envService.catalogueUrl}/geonetwork/srv/fre/catalog.edit#/board?`
+ `sortBy=dateStamp&sortOrder=desc&isTemplate=%5B%22y%22,%22n%22%5D&resultType=manager&from=1&to=20`
+ `&query_string=%7B%22resourceType%22:%7B%22dataset%22:true%7D%7D&owner=40102`;
}
public open( uuid: string ): void{
/** */
if ( !this.modalRef ){
this.url[`expertMode`] = `${this.envService.catalogueUrl}/geonetwork/srv/fre/catalog.search#/metadata/${uuid}`;
this.url[`defaultRepresentation`] = `${this.envService.admincartoUrl}/edit_map/${uuid}`;
this.modalRef = this.ngbModal.open( this.modalContent );
}
this.modalRef.result.finally(() => this.modalRef = null );
}
}
import { RefinementCtx, z } from "zod";
import * as moment from 'moment';
import { contactValidator } from './contact';
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
// Extent
export const contributionExtentValidator = z.object({
......@@ -62,28 +63,7 @@ export const contributionValidator = z.object({
contact: contactValidator.array().optional(),
type: z.string().optional(),
isTabular: z.boolean().optional(),
status: z.string().transform(( val, ctx ) =>{
const strSplited = val.split( `/` );
if ( strSplited.length !== 4 ){
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `NOT VALID STATUS URI (/api/statuses/{id})`,
});
return z.NEVER;
}
const status = parseInt( strSplited[3], 10 );
if ( !isFinite( status ) && isNaN( status )){
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Not a number`,
});
return z.NEVER;
}
return status;
}),
status: z.string(),
});
export type ContributionExtent = z.infer<typeof contributionExtentValidator>;
......@@ -127,3 +107,20 @@ export const CONTRIBUTION_KEY_PATCH_TABULAIRE = [
`updatedAt`,
`type`,
];
export const CONTRIBUTION_STATUS = {
dataCreated: `/api/statuses/1`,
fileCompleted: `/api/statuses/2`,
created: `/api/statuses/3`,
edited: `/api/statuses/4`,
};
export function uniqTable(): ValidatorFn {
return ( control: AbstractControl ): ValidationErrors | null => {
const forbidden = control.value === `table`;
return forbidden ? { uniqTableName: { value: <string>control.value }} : null;
};
}
export const LOGGER_MESSAGE = {
roleLoadError: $localize`:@@roleLoadError:Erreur pendant le chargement des rôles`,
thesarusLoadError: $localize`:@@thesarusLoadError:Erreur pendant le chargement des Thesarus`,
contributionError: $localize`:@@contribtuionErrorLoad:Erreur pendant le chargement des données`,
};
......@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { EnvService } from '../env/env.service';
import { CasConnectApi, casConnectApiValidator } from '../../models/api/cas-connect-api';
import { catchError, from, Observable, of, switchMap } from 'rxjs';
import { catchError, map, Observable, of, switchMap } from 'rxjs';
@Injectable({
providedIn: `root`,
......@@ -21,8 +21,8 @@ export class CasService {
this.httpClient.jsonp<CasConnectApi>( `${this.envService.catalogueUrl}/prodige/connect`, `callback` ).pipe(
switchMap(() => this.httpClient.jsonp<CasConnectApi>( `${this.envService.urlAdmin}/prodige/connect`, `callback` )),
switchMap(() => this.httpClient.jsonp<CasConnectApi>( `${this.envService.urlContribution}/prodige/connect`, `callback` )),
switchMap(( casConnect ) => from( casConnectApiValidator.parseAsync( casConnect ))),
switchMap(( casConnected ) => of( casConnected.connected )),
map(( casConnect ) => casConnectApiValidator.parse( casConnect )),
map(( casConnected ) => casConnected.connected ),
catchError(( err ) => {
console.error( err );
return of( false );
......
......@@ -4,7 +4,7 @@ import {
ActivatedRouteSnapshot, Router,
} from '@angular/router';
import { catchError, Observable, of, switchMap, throwError } from 'rxjs';
import { Contribution } from '../models/contribution';
import { Contribution, CONTRIBUTION_STATUS } from '../models/contribution';
import { ContributionService } from './contribution.service';
@Injectable({
......@@ -15,15 +15,28 @@ export class ContributionResolver implements Resolve<Contribution|null> {
private contributionService: ContributionService,
private router: Router,
)
{ /* empty */ }
{}
resolve( route: ActivatedRouteSnapshot ): Observable<Contribution|null> {
const contribtuonId = route.paramMap.get( `id` );
if ( contribtuonId ){
return this.contributionService.loadContributionById( parseInt( contribtuonId, 10 )).pipe(
switchMap(( contribtuon ) => {
if ( contribtuon.status < 3 ){
if ( contribtuon.status === CONTRIBUTION_STATUS.dataCreated || contribtuon.status === CONTRIBUTION_STATUS.fileCompleted ){
return throwError(() => `Bad status` );
}
return of( contribtuon );
}),
catchError(( err ) => this.handleError( route, err as string )),
);
}
const uuid = route.paramMap.get( `uuid` );
if ( uuid ){
return this.contributionService.loadContributionByUuid( uuid ).pipe(
switchMap(( contribtuon ) => {
if ( contribtuon.status !== CONTRIBUTION_STATUS.edited ){
return throwError(() => `Bad status` );
}
......@@ -37,7 +50,6 @@ export class ContributionResolver implements Resolve<Contribution|null> {
}
handleError( route: ActivatedRouteSnapshot, errorResponse: string ): Observable<null> {
// TODO: bien gérer les erreurs
console.error( errorResponse );
this.router.navigate([`/home`]).catch( console.error );
......
import { Injectable } from '@angular/core';
import { Contribution, ContributionField, contributionValidator } from '../models/contribution';
import { map, Observable, ReplaySubject } from 'rxjs';
import {
Contribution,
ContributionField,
contributionValidator,
uniqTable,
} from '../models/contribution';
import { catchError, map, Observable, ReplaySubject, throwError } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { EnvService } from './env/env.service';
import * as moment from 'moment';
import { AbstractControl, FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { LoggerService } from '../components/logger/logger.service';
import { LOGGER_MESSAGE } from '../models/logger-message';
@Injectable({
providedIn: `root`,
......@@ -24,6 +31,7 @@ export class ContributionService {
private httpClient: HttpClient,
private envService: EnvService,
private fb: FormBuilder,
private loggerService: LoggerService,
) { /* empty */ }
public loadContributionById( contributionId: number ): Observable<Contribution>{
......@@ -66,7 +74,33 @@ export class ContributionService {
return contribution;
}),
catchError(( err ) => {
console.error( err );
this.loggerService.errorOnModal$( LOGGER_MESSAGE.contributionError );
return throwError(() => <unknown>err );
}),
);
}
public loadContributionByUuid( uuid: string ): Observable<Contribution>{
return this.httpClient.get<Contribution>(
`${this.envService.urlContribution}/api/data/page=1&itemsPage=30&metadataUuid=${uuid}`, { withCredentials: true, headers: this.headerGet },
)
.pipe(
map(( contribution ) =>{
this.setContribution( contribution );
return contributionValidator.parse( contribution );
}),
catchError(( err ) => {
console.error( err );
this.loggerService.errorOnModal$( LOGGER_MESSAGE.contributionError );
return throwError(() => <unknown>err );
}),
);
}
public getContribtuion$(): Observable<Contribution>{
......@@ -126,9 +160,10 @@ export class ContributionService {
const generalInformationForm: {[key:string]: AbstractControl, equivalentScale?: FormControl} = {
'table': new FormControl( this.contribution.table, [
Validators.required,
Validators.pattern( /^[a-zA-Z0-9_]+$/ ),
]),
Validators.required,
Validators.pattern( /^[a-zA-Z0-9_]+$/ ),
uniqTable(),
]),
'title': new FormControl( this.contribution.title, [Validators.required]),
'abstract': new FormControl( this.contribution.abstract, [Validators.required]),
'lineage': new FormControl( this.contribution.lineage ),
......
......@@ -31,7 +31,7 @@
</ul>
</div>
<div class="card-body shadow mx-auto">
<div class="container-file-input">
<div class="container-file-input" (drop)="onDrop($event)" (dragover)="onDragOver($event)">
<p class="fs-4 mb-0" i18n="@@homeFileInputDragDropDataset">Glisser / Déposer votre jeu de donnéees ici</p>
<small i18n="@@homeFileInputExplicationFormat">
Pour certains formats, il est nécessaire de charger l'ensemble des fichiers associés au format (ex: shapefile - shp, shx, dbf, prj)
......@@ -70,7 +70,8 @@
</div>
<div class="d-grid">
<button class="btn btn-outline-brand btn-lg my-3 px-5" i18n="ToStartUp"
[disabled]="!this.fileTransfertChecked || !this.transfertEnding" (click)="finalizeTransfert()" role="button">
[disabled]="!this.contribution && (!this.fileTransfertChecked || !this.transfertEnding)"
(click)="finalizeTransfert()" role="button">
Démarrer
<ng-container *ngIf="this.finalizeLoading" >
<i class="fas fa-circle-notch fa-spin upload-icon icon-color-loading" aria-hidden="true"></i>
......
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { HomeService } from "./home.service";
import { Router } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { IFileLoading } from '../../core/models/file-loading-type';
import { BaseComponent } from '../../core/components/base/base.component';
import { from, switchMap, takeUntil } from 'rxjs';
import { Contribution } from '../../core/models/contribution';
@Component({
selector: `alk-home`,
templateUrl: `./home.component.html`,
styleUrls: [`./home.component.scss`],
})
export class HomeComponent extends BaseComponent {
export class HomeComponent extends BaseComponent implements OnInit{
public fileToUploads!: FileList|null;
public fileLoading: IFileLoading = {};
......@@ -20,13 +21,25 @@ export class HomeComponent extends BaseComponent {
public finalizeLoading = false;
public contribution: Contribution = null;
constructor(
private homeService: HomeService,
private router: Router,
private activatedRoute: ActivatedRoute,
) {
super();
}
ngOnInit(): void {
this.activatedRoute.data.pipe(
takeUntil( this.endSubscriptions ),
)
.subscribe(({ contribution }) => {
this.contribution = <Contribution>contribution;
});
}
handleFileInput( event: Event ): boolean {
if ( !( event.target as HTMLInputElement ).files ) {
......@@ -54,7 +67,7 @@ export class HomeComponent extends BaseComponent {
this.fileTransfertChecked = true;
this.homeService
.postFileList( this.fileToUploads )
.postFileList( this.fileToUploads, this.contribution )
.pipe( takeUntil( this.endSubscriptions ))
.subscribe({
next: ( apiResponse ) => {
......@@ -74,10 +87,16 @@ export class HomeComponent extends BaseComponent {
}
finalizeTransfert(){
this.transfertEnding = false;
finalizeTransfert(): void{
this.finalizeLoading = true;
this.homeService.finaliseFileTransfert().pipe(
this.transfertEnding = false;
if ( this.contribution && !this.fileToUploads ){
this.router.navigate([ `contribute/form`, this.contribution.id ]).catch( console.error );
return;
}
this.homeService.finaliseFileTransfert( this.contribution ).pipe(
takeUntil( this.endSubscriptions ),
switchMap(( contributionId ) => from( this.router.navigate([ `contribute/form`, contributionId ]))),
)
......@@ -90,8 +109,16 @@ export class HomeComponent extends BaseComponent {
},
complete: () =>{
this.finalizeLoading = false;
this.finalizeLoading = false;
},
});
}
public onDrop( event: Event ){
console.log( `onDrop`, event );
}
public onDragOver( event: Event ){
console.log( `onDragOver`, event );
}
}
......@@ -11,6 +11,7 @@ import {
ResponsePatchData,
responsePatchDataValidator,
} from '../../core/models/api/response-patch-data';
import { Contribution } from '../../core/models/contribution';
@Injectable({
......@@ -34,29 +35,28 @@ export class HomeService {
* Envoie de fichier un par un
* @return un observable qui se déclenche pour chaque fichier envoyé au serveur
*/
postFileList( fileToUploads: FileList ): Observable<ResponseFileApi> {
return this.httpClient.post<ResponsePatchData>( `${this.envService.urlContribution}/api/data`, [],
{ withCredentials: true, headers: this.header })
.pipe(
take( 1 ),
map(( response ) => responsePatchDataValidator.parse( response )),
switchMap(( contribution ) => {
this.contributionId = contribution.id;
this.type = contribution[`@type`];
// conversion de Filelist en tableau
let idx = 0;
const obsList: Observable<ResponseFileApi>[] = [];
while ( idx < fileToUploads.length ) {
obsList.push( this.postFile( fileToUploads[idx]));
idx = idx + 1;
}
return merge( ...obsList );
}),
postFileList( fileToUploads: FileList, contribution?: Contribution ): Observable<ResponseFileApi> {
const getData = contribution ? of( <ResponsePatchData>contribution ) : this.createContribution();
return getData.pipe(
switchMap(( contribution ) => {
this.contributionId = contribution.id;
this.type = contribution[`@type`];
// conversion de Filelist en tableau
let idx = 0;
const obsList: Observable<ResponseFileApi>[] = [];
while ( idx < fileToUploads.length ) {
obsList.push( this.postFile( fileToUploads[idx]));
idx = idx + 1;
}
return merge( ...obsList );
}),
);
}
/**
* Renvoie la liste des nom de fichiers à partir d'un FileList
*/
......@@ -78,9 +78,11 @@ export class HomeService {
/**
* Pour finir l'envoi des fichier
*/
finaliseFileTransfert(): Observable<number> {
finaliseFileTransfert( contribution?: Contribution ): Observable<number> {
const urlType = contribution ? `recreate_data_sets` : `initialize`;
return this.httpClient.patch<ResponsePatchData>(
`${this.envService.urlContribution}/api/data/initialize/${this.contributionId}`, { status: `api/statuses/3`, '@type': this.type },
`${this.envService.urlContribution}/api/data/${urlType}/${this.contributionId}`, { status: `api/statuses/3`, '@type': this.type },
{ withCredentials: true, headers: this.headerInitialize },
).pipe(
take( 1 ),
......@@ -111,4 +113,12 @@ export class HomeService {
}),
);
}
private createContribution(): Observable<ResponsePatchData>{
return this.httpClient.post<ResponsePatchData>( `${this.envService.urlContribution}/api/data`, [],
{ withCredentials: true, headers: this.header })
.pipe(
map(( response ) => responsePatchDataValidator.parse( response )),
);
}
}
......@@ -3,9 +3,12 @@
<div class="text-center">
<span class="step-number" i18n="@@stepOne">Étape 1</span>
<h1 i18n="@@dataOverview">Aperçu des données</h1>
<p class="mb-4" i18n="@@overviewCheckSRID" >
<p class="mb-4" i18n="@@overviewCheckSRID" *ngIf="!this.contribution.isTabular">
Veuillez vérifier que le système de référence spatiale ainsi que les données récupérées sont valides.
</p>
<p class="mb-4" i18n="@@overviewCheckTabularData" *ngIf="this.contribution.isTabular">
Veuillez vérifier que les données récupérées sont valides.
</p>
</div>
<form class="row g-3" *ngIf="!this.contribution.isTabular" >
<div class="mb-3 col-lg-12">
......@@ -18,7 +21,7 @@
<ng-container *ngIf="canDisplayMap">
<div id="main-map" class="col col-md4">
<alk-ol-map class="visualiseur">
<div id="main-map-container" #olMap class="visualiseur"></div>
<div id="main-map-container" class="visualiseur"></div>
<div id="map-container"></div>
</alk-ol-map>
</div>
......@@ -124,7 +127,8 @@
</tbody>
</table>
</ng-container>
<alk-field-display [fields]="fieldDisplayData" [mapFile]="contribution.map" [layerId]="contribution.layerName"></alk-field-display>
<alk-field-display [fields]="fieldDisplayData" [withValue]="true" [mapFile]="contribution.map" [layerId]="contribution.layerName">
</alk-field-display>
</form>
</div>
<div class="d-flex justify-content-end mt-4">
......