diff --git a/config/config-dev.json b/config/config-dev.json index f52d6a685b9971d965c8d38de381ecd66c850919..806ab41cf63ec7f0e102bd4b1f71576d8dde4598 100644 --- a/config/config-dev.json +++ b/config/config-dev.json @@ -19,5 +19,8 @@ }, "credits": { "url": "https://kong-dev.alpha.grandlyon.com/credits/credits/" + }, + "reuses": { + "url": "https://kong-dev.alpha.grandlyon.com/reuses/" } } \ No newline at end of file diff --git a/config/config-rec.json b/config/config-rec.json index 099b28ecb05ef11881fc4754a20aa4713bf146be..1fd8cad9fe2f03503a70f12ef8f77ecb58035eaf 100644 --- a/config/config-rec.json +++ b/config/config-rec.json @@ -19,5 +19,8 @@ }, "credits": { "url": "https://kong-rec.alpha.grandlyon.com/credits/credits/" + }, + "reuses": { + "url": "https://kong-rec.alpha.grandlyon.com/reuses/" } } \ No newline at end of file diff --git a/src/app/app.routing.module.ts b/src/app/app.routing.module.ts index bebdc08e030368f0fe038587cf70ba4011daa88c..5f8dd9cba72a90a4d9b33b73b7b0fdbca1698d17 100644 --- a/src/app/app.routing.module.ts +++ b/src/app/app.routing.module.ts @@ -6,7 +6,7 @@ import { OrganizationFormComponent } from './components/organizations/edit/organ import { ResourcesComponent } from './components/resources/list/resources.component'; import { ResourceFormComponent } from './components/resources/edit/resource-form.component'; import { ResourceDetailComponent } from './components/resources/detail/resource-detail.component'; -import { FormatsComponent, FormatDetailComponent, FormatFormComponent, ChangelogDetailComponent, ChangelogFormComponent, CreditsComponent, CreditFormComponent, CreditDetailComponent } from './components'; +import { FormatsComponent, FormatDetailComponent, FormatFormComponent, ChangelogDetailComponent, ChangelogFormComponent, CreditsComponent, CreditFormComponent, CreditDetailComponent, ReusesComponent, ReuseFormComponent, ReuseDetailComponent } from './components'; import { AuthenticatedGuard } from './user/guards/authenticated.guard'; import { ChangelogComponent } from './components/changelog/list/changelog.component'; @@ -176,6 +176,38 @@ const appRoutes: Routes = [ title: 'Detail du crédit', }, }, + { + path: 'reutilisations', + component: ReusesComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Réutilisations', + }, + }, + { + path: 'reutilisations/new', + component: ReuseFormComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Nouvelle réutilisation', + }, + }, + { + path: 'reutilisations/:id/edit', + component: ReuseFormComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Modifier la réutilisation', + }, + }, + { + path: 'reutilisations/:id', + component: ReuseDetailComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Detail de la réutilisation', + }, + }, ]; @NgModule({ diff --git a/src/app/components/changelog/list/changelog.component.ts b/src/app/components/changelog/list/changelog.component.ts index ecd52fe85426612de5165cc5dc786d17f00a16cf..169fce8b9b7266bc9e950793d30384030e981082 100644 --- a/src/app/components/changelog/list/changelog.component.ts +++ b/src/app/components/changelog/list/changelog.component.ts @@ -65,16 +65,27 @@ export class ChangelogComponent implements OnInit, OnDestroy { private search() { this._changelogService.getChangelogs() - .subscribe((items: ChangelogRO) => { - this.changelogs = items.changelogs; - this.totalElement = items.totalCount; - - this.pageHeaderInfo.title = `${this.totalElement} changelogs trouvés`; - - this.paginator.limit = this._changelogService.limit; - this.paginator.pageIndex = this._changelogService.pageNumber; - this.paginator.length = items.totalCount; - }); + .subscribe( + (items: ChangelogRO) => { + this.changelogs = items.changelogs; + this.totalElement = items.totalCount; + + this.pageHeaderInfo.title = this.totalElement > 1 ? + `${this.totalElement} changelogs trouvés` : + `${this.totalElement} changelog trouvé`; + + this.paginator.limit = this._changelogService.limit; + this.paginator.pageIndex = this._changelogService.pageNumber; + this.paginator.length = items.totalCount; + }, + () => { + this.pageHeaderInfo.title = '0 changelog trouvé'; + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors du chargement des changelogs.', + }); + }, + ); } // When pagination is changed by user, we update datasetList with new pagination options diff --git a/src/app/components/credits/list/credits.component.ts b/src/app/components/credits/list/credits.component.ts index 0b23700d29a31c67ad889bffd30e48ee1e911137..d417e76fe35c8d1eba3c39f18c7f628a89c58bb9 100644 --- a/src/app/components/credits/list/credits.component.ts +++ b/src/app/components/credits/list/credits.component.ts @@ -21,7 +21,7 @@ export class CreditsComponent implements OnInit, OnDestroy { cancel: 'Annuler', continue: 'Supprimer', }; - credits: Credit[]; + credits: Credit[] = []; searchChangeSub: Subscription; // Paginator options @@ -65,17 +65,26 @@ export class CreditsComponent implements OnInit, OnDestroy { private search() { this._creditService.getCredits() - .subscribe((items: CreditRO) => { - this.credits = items.credits; - this.totalElement = items.totalCount; - - this.pageHeaderInfo.title = items.totalCount ? - `${this.totalElement} crédits trouvés` : '0 crédit trouvé'; - - this.paginator.limit = this._creditService.limit; - this.paginator.pageIndex = this._creditService.pageNumber; - this.paginator.length = items.totalCount; - }); + .subscribe( + (items: CreditRO) => { + this.credits = items.credits; + this.totalElement = items.totalCount; + + this.pageHeaderInfo.title = this.totalElement > 1 ? + `${this.totalElement} crédits trouvés` : `${this.totalElement} crédit trouvé`; + + this.paginator.limit = this._creditService.limit; + this.paginator.pageIndex = this._creditService.pageNumber; + this.paginator.length = items.totalCount; + }, + () => { + this.pageHeaderInfo.title = '0 crédit trouvé'; + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors du chargement des crédits.', + }); + }, + ); } // When pagination is changed by user, we update datasetList with new pagination options diff --git a/src/app/components/formats/list/formats.component.html b/src/app/components/formats/list/formats.component.html index 4bcbc8fc3cb0783d9b85aff51b31d1bf36c0c649..987f71d2484d077424d82ae18e4de0f52e8596c8 100644 --- a/src/app/components/formats/list/formats.component.html +++ b/src/app/components/formats/list/formats.component.html @@ -1,11 +1,11 @@ -<section class="section page-container" *ngIf="formats"> +<section class="section page-container"> <app-page-header [pageInfo]="pageHeaderInfo" [hideBackButton]="true"></app-page-header> <div class="add-item-link has-text-right"> <a class="button button-gl" [routerLink]="['new']"> Ajouter </a> </div> - <div class="table entity-list-table"> + <div class="table entity-list-table" *ngIf="formats"> <div class="header columns is-marginless"> <div class="column is-2"> <span (click)="sortBy('name')" class="is-sortable"> diff --git a/src/app/components/formats/list/formats.component.ts b/src/app/components/formats/list/formats.component.ts index 883b02a0613b8a1f53e8fcfcd7d723d04187c82d..93c3b7ad6b10e15a7e6fc0ce399497aab3ff7bd4 100644 --- a/src/app/components/formats/list/formats.component.ts +++ b/src/app/components/formats/list/formats.component.ts @@ -22,7 +22,7 @@ export class FormatsComponent implements OnInit, OnDestroy { cancel: 'Annuler', continue: 'Supprimer', }; - formats: Format[]; + formats: Format[] = []; searchChangeSub: Subscription; // Paginator options @@ -66,16 +66,28 @@ export class FormatsComponent implements OnInit, OnDestroy { private search() { this._formatService.getFormats() - .subscribe((items: FormatRO) => { - this.formats = items.formats; - this.totalElement = items.totalCount; - - this.pageHeaderInfo.title = `${this.totalElement} formats trouvés`; - - this.paginator.limit = this._formatService.limit; - this.paginator.pageIndex = this._formatService.pageNumber; - this.paginator.length = items.totalCount; - }); + .subscribe( + (items: FormatRO) => { + this.formats = items.formats; + this.totalElement = items.totalCount; + + this.pageHeaderInfo.title = `${this.totalElement} formats trouvés`; + this.pageHeaderInfo.title = this.totalElement > 1 ? + `${this.totalElement} formats trouvés` : + `${this.totalElement} format trouvé`; + + this.paginator.limit = this._formatService.limit; + this.paginator.pageIndex = this._formatService.pageNumber; + this.paginator.length = items.totalCount; + }, + () => { + this.pageHeaderInfo.title = '0 format trouvé'; + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors du chargement des formats.', + }); + }, + ); } // When pagination is changed by user, we update datasetList with new pagination options diff --git a/src/app/components/index.ts b/src/app/components/index.ts index 5783b1752ff7922c885d41496d135a65469faed4..554e32fe57819dfc71d90451b212a82077d06eab 100644 --- a/src/app/components/index.ts +++ b/src/app/components/index.ts @@ -21,6 +21,9 @@ import { ConfirmationModalComponent } from './confirmation-modal/confirmation-mo import { CreditDetailComponent } from './credits/detail/credit-detail.component'; import { CreditFormComponent } from './credits/edit/credit-form.component'; import { CreditsComponent } from './credits/list/credits.component'; +import { ReusesComponent } from './reuses/list/reuses.component'; +import { ReuseFormComponent } from './reuses/edit/reuse-form.component'; +import { ReuseDetailComponent } from './reuses/detail/reuse-detail.component'; export { MenuComponent, @@ -46,6 +49,9 @@ export { CreditDetailComponent, CreditFormComponent, CreditsComponent, + ReuseDetailComponent, + ReuseFormComponent, + ReusesComponent, }; // tslint:disable-next-line:variable-name @@ -73,4 +79,7 @@ export const AppComponents = [ CreditDetailComponent, CreditFormComponent, CreditsComponent, + ReuseDetailComponent, + ReuseFormComponent, + ReusesComponent, ]; diff --git a/src/app/components/menu/menu.component.html b/src/app/components/menu/menu.component.html index 1d0dc5d7e8948b536492d29c6afe1f77d9b33368..8ad8d8d15232a51649b9f2f707f550eab691888a 100644 --- a/src/app/components/menu/menu.component.html +++ b/src/app/components/menu/menu.component.html @@ -41,5 +41,12 @@ <span class="label-menu">Changelog</span> </a> </li> + <li><a [routerLink]="['/', 'reutilisations']" routerLinkActive="active-link"> + <span class="icon"> + <i class="fas fa-recycle"></i> + </span> + <span class="label-menu">Réutilisations</span> + </a> + </li> </ul> </aside> \ No newline at end of file diff --git a/src/app/components/organizations/edit/organization-form.component.ts b/src/app/components/organizations/edit/organization-form.component.ts index 7ffac9b3fd4bbd9cd32968b7ad643a63a5039008..10ad776b3840676437420bc006e92c577099c9ba 100644 --- a/src/app/components/organizations/edit/organization-form.component.ts +++ b/src/app/components/organizations/edit/organization-form.component.ts @@ -24,7 +24,6 @@ export class OrganizationFormComponent implements OnInit { existingImageUrl: null, isRequired: true, }; - logo: File; title: string; constructor( diff --git a/src/app/components/organizations/list/organizations.component.ts b/src/app/components/organizations/list/organizations.component.ts index 148054266254b56134c91e6b3584383a6c0aa49a..bfaffdf3c409d6df6992a40591b906f0571b465e 100644 --- a/src/app/components/organizations/list/organizations.component.ts +++ b/src/app/components/organizations/list/organizations.component.ts @@ -22,7 +22,7 @@ export class OrganizationsComponent implements OnInit, OnDestroy { cancel: 'Annuler', continue: 'Supprimer', }; - organizations: Organization[]; + organizations: Organization[] = []; searchChangeSub: Subscription; // Paginator options @@ -66,17 +66,26 @@ export class OrganizationsComponent implements OnInit, OnDestroy { private search() { this._organizationService.getOrganizations() - .subscribe((items: OrganizationRO) => { - this.organizations = items.organizations; - this.totalElement = items.totalCount; - - this.pageHeaderInfo.title = items.totalCount ? - `${this.totalElement} producteurs de données trouvés` : '0 producteur de données trouvé'; - - this.paginator.limit = this._organizationService.limit; - this.paginator.pageIndex = this._organizationService.pageNumber; - this.paginator.length = items.totalCount; - }); + .subscribe( + (items: OrganizationRO) => { + this.organizations = items.organizations; + this.totalElement = items.totalCount; + + this.pageHeaderInfo.title = items.totalCount ? + `${this.totalElement} producteurs de données trouvés` : `${this.totalElement} producteur de données trouvé`; + + this.paginator.limit = this._organizationService.limit; + this.paginator.pageIndex = this._organizationService.pageNumber; + this.paginator.length = items.totalCount; + }, + () => { + this.pageHeaderInfo.title = '0 producteur de données trouvé'; + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors du chargement des producteurs de données.', + }); + }, + ); } // When pagination is changed by user, we update datasetList with new pagination options diff --git a/src/app/components/resources/list/resources.component.ts b/src/app/components/resources/list/resources.component.ts index 5b4e8680d41de66e401685f2adb4a7a054b8ffb4..f37b37a6e0e1874407b906986921ae87570c2d44 100644 --- a/src/app/components/resources/list/resources.component.ts +++ b/src/app/components/resources/list/resources.component.ts @@ -22,7 +22,7 @@ export class ResourcesComponent implements OnInit, OnDestroy { cancel: 'Annuler', continue: 'Supprimer', }; - resources: Resource[]; + resources: Resource[] = []; searchChangeSub: Subscription; // Paginator options @@ -66,16 +66,28 @@ export class ResourcesComponent implements OnInit, OnDestroy { private search() { this._resourceService.getResources() - .subscribe((items: ResourceRO) => { - this.resources = items.resources; - this.totalElement = items.totalCount; - - this.pageHeaderInfo.title = `${this.totalElement} ressources trouvées`; - - this.paginator.limit = this._resourceService.limit; - this.paginator.pageIndex = this._resourceService.pageNumber; - this.paginator.length = items.totalCount; - }); + .subscribe( + (items: ResourceRO) => { + this.resources = items.resources; + this.totalElement = items.totalCount; + + this.pageHeaderInfo.title = `${this.totalElement} ressources trouvées`; + this.pageHeaderInfo.title = this.totalElement > 1 ? + `${this.totalElement} ressources trouvées` : + `${this.totalElement} ressource trouvée`; + + this.paginator.limit = this._resourceService.limit; + this.paginator.pageIndex = this._resourceService.pageNumber; + this.paginator.length = items.totalCount; + }, + () => { + this.pageHeaderInfo.title = '0 ressource trouvée'; + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors du chargement des ressources.', + }); + }, + ); } // When pagination is changed by user, we update datasetList with new pagination options diff --git a/src/app/components/reuses/detail/reuse-detail.component.html b/src/app/components/reuses/detail/reuse-detail.component.html new file mode 100644 index 0000000000000000000000000000000000000000..169e248e423742cfdb487f301fb9aab003bde4dc --- /dev/null +++ b/src/app/components/reuses/detail/reuse-detail.component.html @@ -0,0 +1,66 @@ +<section class="section page-container" *ngIf="reuse"> + <app-page-header [pageInfo]="{title: title}"></app-page-header> + + <div class="columns is-centered"> + <div class="column is-8"> + <div class="card"> + <header class="card-header"> + <p class="card-header-title has-text-centered"> + {{reuse.name}} + </p> + </header> + <div class="card-image"> + <figure class="image"> + <img [src]="reuse.logo" alt="Logo de la réutilisation"> + </figure> + </div> + <div class="card-content"> + <div class="content"> + <p> + <span class="has-text-weight-bold">Id: </span> + <span>{{reuse._id}}</span> + </p> + <p> + <span class="has-text-weight-bold">Créateur: </span> + <span>{{reuse.creator}}</span> + </p> + <p> + <span class="has-text-weight-bold">Date de création: </span> + <span>{{reuse.createDate}}</span> + </p> + <p> + <span class="has-text-weight-bold">Date de dernière mise à jour: </span> + <span>{{reuse.updateDate}}</span> + </p> + <div> + <p><span class="has-text-weight-bold">Statut:</span> {{ reuse.published ? 'Publié' : 'Brouillon' }} + </p> + </div> + <p> + <span class="has-text-weight-bold">Site web: </span> + <span><a [href]="reuse.website">{{reuse.website}}</a></span> + </p> + + <div class="list-container" *ngIf="reuse.reuseTypes && reuse.reuseTypes.length > 0"> + <span class="has-text-weight-bold">Type(s) de réutilisation: </span> + <ul> + <li *ngFor="let reuseType of reuse.reuseTypes"> + {{ reuseType }} + </li> + </ul> + </div> + + <div class="list-container" *ngIf="reuse.datasetsUsed && reuse.datasetsUsed.length > 0"> + <span class="has-text-weight-bold">Jeu(x) de données réutilisé(s): </span> + <ul> + <li *ngFor="let dataset of reuse.datasetsUsed"> + {{ dataset }} + </li> + </ul> + </div> + </div> + </div> + </div> + </div> + </div> +</section> \ No newline at end of file diff --git a/src/app/components/reuses/detail/reuse-detail.component.scss b/src/app/components/reuses/detail/reuse-detail.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..369170463fc9b68f9075aca173452f0c23f1765d --- /dev/null +++ b/src/app/components/reuses/detail/reuse-detail.component.scss @@ -0,0 +1,21 @@ +.card-header-title { + justify-content: center; +} + +figure { + text-align: center; +} + +figure img { + max-width: 150px; + display: inline-block; + margin-top: 20px; +} + +.list-container:not(:last-of-type) { + margin-bottom: 1rem; + + ul { + margin-top: 0.5rem; + } +} diff --git a/src/app/components/reuses/detail/reuse-detail.component.ts b/src/app/components/reuses/detail/reuse-detail.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..ffada589a724d80bbaef86d9556a373f4b0f9dd9 --- /dev/null +++ b/src/app/components/reuses/detail/reuse-detail.component.ts @@ -0,0 +1,30 @@ + +import { switchMap } from 'rxjs/operators'; +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, ParamMap } from '@angular/router'; +import { Reuse } from '../../../models/reuse.model'; +import { ReuseService } from '../../../services'; + +@Component({ + selector: 'app-reuse-detail', + templateUrl: './reuse-detail.component.html', + styleUrls: ['./reuse-detail.component.scss'], +}) +export class ReuseDetailComponent implements OnInit { + + reuse: Reuse; + title: string; + + constructor( + private _route: ActivatedRoute, + private _reuseService: ReuseService, + ) { + } + + ngOnInit(): void { + this.title = this._route.snapshot.data.title; + this._route.paramMap.pipe( + switchMap((params: ParamMap) => this._reuseService.findById(params.get('id')))) + .subscribe((reuse: Reuse) => this.reuse = reuse); + } +} diff --git a/src/app/components/reuses/edit/reuse-form.component.html b/src/app/components/reuses/edit/reuse-form.component.html new file mode 100644 index 0000000000000000000000000000000000000000..b3a448ebc261ec2c7fb5193164bc06679f8c6648 --- /dev/null +++ b/src/app/components/reuses/edit/reuse-form.component.html @@ -0,0 +1,125 @@ +<section class="section page-container" *ngIf="reuse"> + <form [formGroup]="form" (ngSubmit)="onSubmit()" class="columns is-centered is-marginless is-multiline"> + <div class="column is-12 header-with-publication-status"> + <app-page-header [pageInfo]="{title: title}"></app-page-header> + <div class="field status-field"> + <span class="fake-label" *ngIf="form.get('published').value === true">Publié</span> + <span class="fake-label" *ngIf="form.get('published').value === false">Brouillon</span> + <input id="published" type="checkbox" formControlName="published" class="switch is-rounded"> + <label for="published"></label> + </div> + </div> + <div class="column is-7"> + <input type="hidden" formControlName="_id"> + + <div class="field"> + <label class="label required" for="name">Nom</label> + <div class="control"> + <input class="input" type="text" formControlName="name" id="name" required> + </div> + <div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert alert-danger"> + <p *ngIf="name.errors['required']" class="help is-danger"> + Le nom de la réutilisation est obligatoire. + </p> + </div> + </div> + + <app-image-upload [fieldParams]="logoFieldParams" (fileChanged)="logoChanged($event)" + (imageRemoved)="removeLogo()"> + </app-image-upload> + + <div class="field"> + <label class="label required" for="name">Créateur</label> + <div class="control"> + <input class="input" type="text" formControlName="creator" id="creator" required> + </div> + <div *ngIf="creator.invalid && (creator.dirty || creator.touched)" class="alert alert-danger"> + <p *ngIf="creator.errors['required']" class="help is-danger"> + Le créateur est obligatoire. + </p> + </div> + </div> + + <div class="field"> + <label class="label required" for="name">Site web</label> + <div class="control"> + <input class="input" type="text" formControlName="website" id="website" required> + </div> + <div *ngIf="website.invalid && (website.dirty || website.touched)" class="alert alert-danger"> + <p *ngIf="website.errors['required']" class="help is-danger"> + Le site web est obligatoire. + </p> + </div> + </div> + + <div class="field"> + <div class="form-array-header"> + <label class="label">Type de réutilisation</label> + <span class="icon" tabindex=0 (click)="addReuseType()" (keyup.enter)="addReuseType()" + title="Ajouter un type de réutilisation"> + <i class="fas fa-plus"></i> + </span> + </div> + + <div formArrayName="reuseTypes"> + <div *ngFor="let reuseType of reuseTypes.controls; let i = index;" class="form-array-item"> + <div class="form-array-input-wrapper"> + <div class="field"> + <div class="control"> + <div class="select"> + <select type="text" [formControlName]="i" required> + <option hidden value="" disabled selected>Selectionnez un type</option> + <option *ngFor="let opt of reuseTypesList" [value]="opt.value">{{opt.label}}</option> + </select> + </div> + </div> + </div> + <div *ngIf="reuseType.invalid && (reuseType.dirty || reuseType.touched)" class="alert alert-danger"> + <p *ngIf="reuseType.hasError('required')" class="help is-danger"> + Vous devez saisir le type de réutilisation. + </p> + </div> + </div> + <span class="icon" tabindex=0 (click)="removeReuseType(i)" (keyup.enter)="removeReuseType(i)" + title="Supprimer le type de réutilisation"> + <i class="fas fa-trash"></i> + </span> + </div> + </div> + </div> + + <div class="field"> + <div class="form-array-header"> + <label class="label">Jeu(x) de données réutilisé(s)</label> + <span class="icon" tabindex=0 (click)="addDatasetUsed()" (keyup.enter)="addDatasetUsed()" + title="Ajouter un jeu de données réutilisé"> + <i class="fas fa-plus"></i> + </span> + </div> + + <div formArrayName="datasetsUsed"> + <div *ngFor="let datasetUsed of datasetsUsed.controls; let i = index;" class="form-array-item"> + <div class="form-array-input-wrapper"> + <div class="control"> + <input class="input" type="text" [formControlName]="i" required> + </div> + <div *ngIf="datasetUsed.invalid && (datasetUsed.dirty || datasetUsed.touched)" class="alert alert-danger"> + <p *ngIf="datasetUsed.hasError('required')" class="help is-danger"> + Vous devez saisir le slug du jeu de données réutilisé. + </p> + </div> + </div> + <span class="icon" tabindex=0 (click)="removeDatasetUsed(i)" (keyup.enter)="removeDatasetUsed(i)" + title="Supprimer le jeu de données réutilisé"> + <i class="fas fa-trash"></i> + </span> + </div> + </div> + </div> + + <div class="has-text-right"> + <button class="button button-gl" type="submit" [disabled]="formInvalid == true">Valider</button> + </div> + </div> + </form> +</section> \ No newline at end of file diff --git a/src/app/components/reuses/edit/reuse-form.component.scss b/src/app/components/reuses/edit/reuse-form.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..4bc5c21221ab9f7ba636ca989a4038e8b3db6af0 --- /dev/null +++ b/src/app/components/reuses/edit/reuse-form.component.scss @@ -0,0 +1,43 @@ +.full-width { + width: 100%; +} + +h1 { + text-align: center +} + +.icon { + cursor: pointer; + + &:hover { + .fa-plus { + color: lightblue; + } + + .fa-trash { + color: #d5232a; + } + } +} + +.form-array-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.5em; + + label { + margin-bottom: 0; + } +} + +.form-array-item { + display: flex; + align-items: center; + margin-bottom: 0.5em; +} + +.form-array-input-wrapper { + flex-grow: 1; + margin-right: 0.5rem; +} diff --git a/src/app/components/reuses/edit/reuse-form.component.ts b/src/app/components/reuses/edit/reuse-form.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..e98eb61d09ba1b84ff87d8ddcc72fe1cc3dbf9d2 --- /dev/null +++ b/src/app/components/reuses/edit/reuse-form.component.ts @@ -0,0 +1,244 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, ParamMap, Router } from '@angular/router'; +import { FormBuilder, FormGroup, Validators, FormArray, FormControl, MaxLengthValidator } from '@angular/forms'; +import { filter, switchMap, catchError, mergeMap } from 'rxjs/operators'; +import { NotificationService, ReuseService, MediaService } from 'src/app/services'; +import { Reuse } from '../../../models/reuse.model'; +import { IImageUploadFieldParams } from '../../../models/image-upload.model'; +import { throwError } from 'rxjs'; + +@Component({ + selector: 'app-reuse-form', + templateUrl: './reuse-form.component.html', + styleUrls: ['./reuse-form.component.scss'], +}) +export class ReuseFormComponent implements OnInit { + + reuse: Reuse = new Reuse(); + form: FormGroup; + logoFile: File; + logoFieldParams: IImageUploadFieldParams = { + inputName: 'logo', + label: 'Logo', + existingImageUrl: null, + isRequired: true, + }; + title: string; + + constructor( + private _reuseService: ReuseService, + private _mediaService: MediaService, + private _route: ActivatedRoute, + private _router: Router, + private _fb: FormBuilder, + private _notificationService: NotificationService, + ) { + } + + ngOnInit() { + this.title = this._route.snapshot.data.title; + this.initForm(); + + this._route.paramMap.pipe( + filter((paramMap: ParamMap) => (paramMap.get('id') !== null)), + switchMap((paramMap: ParamMap) => this._reuseService.findById(paramMap.get('id')))) + .subscribe((reuse: Reuse) => { + + this.reuse = reuse; + + this.logoFieldParams.existingImageUrl = reuse.logo; + + this.initForm(); + + }); + } + + initForm() { + this.form = this._fb.group({ + _id: [this.reuse._id], + name: [this.reuse.name, Validators.required], + creator: [this.reuse.creator, Validators.required], + logo: [this.reuse.logo], + website: [this.reuse.website, Validators.required], + published: [this.reuse.published, Validators.required], + reuseTypes: new FormArray(this.reuse.reuseTypes.map(major => new FormControl(major)), Validators.maxLength(3)), + datasetsUsed: new FormArray(this.reuse.datasetsUsed.map(minor => new FormControl(minor))), + }); + } + + onSubmit() { + console.log(this.form.value) + if (!this.formInvalid) { + this.reuse = new Reuse(this.form.value); + + if (this.reuse._id) { + if (this.logoFile) { + this._mediaService.uploadLogo(this.logoFile).pipe( + catchError(() => { + return throwError('Une erreur est survenue lors de l\'upload du logo de la réutilisation.'); + }), + mergeMap((response: any) => { + this.reuse.logo = response.mediaUrl; + return this._reuseService.update(this.reuse).pipe( + catchError(() => { + return throwError('Une erreur est survenue lors de la mise à jour de la réutilisation.'); + }), + ); + }), + ).subscribe( + (reuseUpdated) => { + this._notificationService.notify({ + message: 'La réutilisation a été mise à jour avec succès.', + type: 'success', + }); + this._router.navigate(['/reutilisations', reuseUpdated._id]); + }, + (err) => { + this._notificationService.notify({ + message: err, + type: 'error', + }); + }, + ); + } else { + return this._reuseService.update(this.reuse).subscribe( + (reuseUpdated) => { + this._notificationService.notify({ + message: 'La réutilisation a été mise à jour avec succès.', + type: 'success', + }); + this._router.navigate(['/reutilisations', reuseUpdated._id]); + }, + () => { + this._notificationService.notify({ + message: 'Une erreur est survenue lors de la mise à jour de la réutilisation.', + type: 'error', + }); + }, + ); + } + } else { + if (this.logoFile) { + this._mediaService.uploadLogo(this.logoFile).pipe( + catchError(() => { + return throwError('Une erreur est survenue lors de l\'upload du logo de la réutilisation.'); + }), + mergeMap((response: any) => { + this.reuse.logo = response.mediaUrl; + return this._reuseService.create(this.reuse).pipe( + catchError(() => { + return throwError('Une erreur est survenue lors de la création de la réutilisation.'); + }), + ); + }), + ).subscribe( + (reutilisationCreated) => { + this._notificationService.notify({ + message: 'La réutilisation a été créée avec succès.', + type: 'success', + }); + this._router.navigate(['/reutilisations', reutilisationCreated._id]); + }, + (err) => { + this._notificationService.notify({ + message: err, + type: 'error', + }); + }, + ); + } else { + return this._reuseService.create(this.reuse).subscribe( + (reuseCreated) => { + this._notificationService.notify({ + message: 'La réutilisation a été créée avec succès.', + type: 'success', + }); + this._router.navigate(['/reutilisations', reuseCreated._id]); + }, + () => { + this._notificationService.notify({ + message: 'Une erreur est survenue lors de la création de la réutilisation.', + type: 'error', + }); + }, + ); + } + } + } + } + + addReuseType() { + if (this.reuseTypes.length < this.reuseTypesList.length) { + this.reuseTypes.push(new FormControl('')); + } + } + + addDatasetUsed() { + this.datasetsUsed.push(new FormControl()); + } + + removeReuseType(index) { + this.reuseTypes.removeAt(index); + } + + removeDatasetUsed(index) { + this.datasetsUsed.removeAt(index); + } + + // Getters for each property + get name() { + return this.form.controls['name']; + } + + get creator() { + return this.form.controls['creator']; + } + + get website() { + return this.form.controls['website']; + } + + get published() { + return this.form.controls['published']; + } + + get reuseTypes(): FormArray { + return this.form.get('reuseTypes') as FormArray; + } + + get datasetsUsed(): FormArray { + return this.form.get('datasetsUsed') as FormArray; + } + + get formInvalid() { + return this.form.invalid; + } + + get reuseTypesList() { + return [ + { + value: 'app', + label: 'Application', + }, + { + value: 'web', + label: 'Web', + }, + { + value: 'article', + label: 'Article', + }, + ]; + } + + logoChanged(fileList: FileList) { + if (fileList && fileList.length > 0) { + this.logoFile = fileList[0]; + } + } + + removeLogo() { + this.form.get('logo').setValue(null); + } + +} diff --git a/src/app/components/reuses/list/reuses.component.html b/src/app/components/reuses/list/reuses.component.html new file mode 100644 index 0000000000000000000000000000000000000000..5f1019fb1b7da7879287bf6a68525b10514e0787 --- /dev/null +++ b/src/app/components/reuses/list/reuses.component.html @@ -0,0 +1,112 @@ +<section class="section page-container"> + <app-page-header [pageInfo]="pageHeaderInfo" [hideBackButton]="true"></app-page-header> + <div class="add-item-link has-text-right"> + <a class="button button-gl" [routerLink]="['new']"> + Ajouter + </a> + </div> + <div class="table entity-list-table" *ngIf="reuses"> + <div class="header columns is-marginless"> + <div class="column is-2"> + <span (click)="sortBy('name')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'name' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'name' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === 'name'}">Nom</span> + </span> + </div> + <div class="column is-1 has-text-centered"> + <span (click)="sortBy('published')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'published' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'published' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === published}">Publié</span> + </span> + </div> + <div class="column is-1 has-text-centered"> + <span class="column-title">Logo</span> + </div> + <div class="column is-2"> + <span (click)="sortBy('creator')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'creator' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'creator' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === creator}">Créateur</span> + </span> + </div> + <div class="column is-2"> + <span class="column-title">Site web</span> + </div> + <div class="column is-2"> + <span class="column-title">Type de réutilisation</span> + </div> + <div class="column is-offset-1 is-1 has-text-centered"> + <span class="column-title">Actions</span> + </div> + </div> + <div class="data-list"> + <div class="data columns is-multiline is-vcentered is-marginless" + *ngFor="let reuse of reuses; let i=index; let odd=odd; let even=even;" [ngClass]="{ odd: odd, even: even }"> + <div class="column is-2"> + <span>{{ reuse.name }}</span> + </div> + <div class="column is-1 has-text-centered"> + <span class="icon has-text-success" *ngIf="reuse.published"> + <i class="far fa-check-circle"></i> + </span> + <span class="icon has-text-danger" *ngIf="!reuse.published"> + <i class="far fa-times-circle"></i> + </span> + </div> + <div class="column is-1 has-text-centered"> + <img class="entity-logo-in-list" [src]="reuse.logo" alt="Logo de la réutilisation"> + </div> + <div class="column is-2"> + <span>{{ reuse.creator }}</span> + </div> + <div class="column is-2"> + <span>{{ reuse.website }}</span> + </div> + <div class="column is-2"> + <span>{{ reuse.reuseTypes.join(', ') }}</span> + </div> + <div class="column is-offset-1 is-1 has-text-centered actions"> + <app-crud-buttons [id]="reuse._id" (delete)="displayDeletePopup($event)"></app-crud-buttons> + </div> + </div> + </div> + <div class="columns is-marginless paginator"> + <div class="column"> + <app-paginator *ngIf="paginator.length > 0" [length]="paginator.length" [pageSize]="paginator.limit" + [pageSizeOptions]="paginator.pageSizeOptions" [pageIndex]="paginator.pageIndex" [pagesToShow]="5" + [showFirstLastButtons]="true" (page)="changePagination($event)"> + </app-paginator> + </div> + </div> + </div> +</section> + +<app-confirmation-modal (cancel)="objectToBeDeletedId=null" (continue)="deleteChangelog()" [texts]="deleteModalTexts" + [isOpened]="objectToBeDeletedId !== null"> +</app-confirmation-modal> \ No newline at end of file diff --git a/src/app/components/reuses/list/reuses.component.scss b/src/app/components/reuses/list/reuses.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/components/reuses/list/reuses.component.ts b/src/app/components/reuses/list/reuses.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..13b5572558af69a2970c10b65b1159c462ec8f03 --- /dev/null +++ b/src/app/components/reuses/list/reuses.component.ts @@ -0,0 +1,143 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { PaginatorOptions } from 'src/app/models/paginator-options.model'; +import { IPageHeaderInfo } from '../../../models/page.model'; +import { Subscription } from 'rxjs'; +import { ReuseService, NotificationService } from '../../../services'; +import { Reuse, ReuseRO } from '../../../models/reuse.model'; + +@Component({ + selector: 'app-reuses', + templateUrl: './reuses.component.html', + styleUrls: ['./reuses.component.scss'], +}) +export class ReusesComponent implements OnInit, OnDestroy { + + pageHeaderInfo: IPageHeaderInfo = { + title: '', + }; + objectToBeDeletedId = null; + deleteModalTexts = { + main: 'Si vous poursuivez, la réutilisation sera définitivement supprimée.', + cancel: 'Annuler', + continue: 'Supprimer', + }; + reuses: Reuse[] = []; + searchChangeSub: Subscription; + + // Paginator options + paginator: PaginatorOptions; + pageSize = 10; + pageSizeOptions = [5, 10, 25, 100]; + + sortValue: string; + + totalElement: number; + filters = { + name: '', + }; + where = {}; + + constructor( + private _reuseService: ReuseService, + private _notificationService: NotificationService, + ) { + this.paginator = { + pageIndex: this._reuseService.pageNumber, + length: 0, + limit: this._reuseService.limit, + pageSizeOptions: [5, 10, 20], + }; + } + + ngOnInit(): void { + this._reuseService.sortOptions = { + value: 'name', + order: 'desc', + }; + this.search(); + + this.searchChangeSub = this._reuseService.searchChange$.subscribe( + () => { + this.search(); + }, + ); + } + + private search() { + this._reuseService.getReuses() + .subscribe( + (items: ReuseRO) => { + this.reuses = items.reuses; + this.totalElement = items.totalCount; + + this.pageHeaderInfo.title = this.totalElement > 1 ? + `${this.totalElement} réutilisations trouvées` : + `${this.totalElement} réutilisation trouvée`; + + this.paginator.limit = this._reuseService.limit; + this.paginator.pageIndex = this._reuseService.pageNumber; + this.paginator.length = items.totalCount; + }, + () => { + this.pageHeaderInfo.title = '0 réutilisation trouvée'; + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors du chargement des réutilisations.', + }); + }, + ); + } + + changePagination(pageIndex) { + this._reuseService.paginationChanged(this.paginator.limit, pageIndex); + } + + changePageSize(pageSize) { + this._reuseService.paginationChanged(pageSize, 1); + } + + sortBy(key: string) { + if (this._reuseService.sortOptions.value === key) { + this._reuseService.reverseSortOrder(); + } else { + this._reuseService.sortOptions.value = key; + this._reuseService.sortOptions.order = 'asc'; + } + this.search(); + } + + get sortOptions() { + return this._reuseService.sortOptions; + } + + displayDeletePopup(id) { + this.objectToBeDeletedId = id; + } + + deleteChangelog() { + this._reuseService.delete(this.objectToBeDeletedId).subscribe( + () => { + this._notificationService.notify({ + type: 'success', + message: 'La réutilisation a été supprimé avec succès.', + }); + this._reuseService.pageNumber = 1; + this.search(); + }, + () => { + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors de la suppression de la réutilisation.', + }); + }, + () => { + this.objectToBeDeletedId = null; + }, + ); + + } + + ngOnDestroy() { + this.searchChangeSub.unsubscribe(); + } +} diff --git a/src/app/models/reuse.model.ts b/src/app/models/reuse.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..6b80b2315a1b1bbc04a043c644cab9d673583f91 --- /dev/null +++ b/src/app/models/reuse.model.ts @@ -0,0 +1,53 @@ +export class ReuseRO { + reuses: Reuse[]; + totalCount: number; + + constructor(reuses, totalCount) { + this.reuses = reuses; + this.totalCount = totalCount; + } +} + +export class Reuse { + _id: string; + name: string; + creator: string; + logo: string; + website: string; + reuseTypes: string[]; + datasetsUsed: string[]; + createDate: Date; + updateDate: Date; + published: boolean; + + constructor(reuse?: IReuse) { + this._id = reuse && reuse._id ? reuse._id : null; + this.name = reuse && reuse.name ? reuse.name : null; + this.creator = reuse && reuse.creator ? reuse.creator : null; + this.logo = reuse && reuse.logo ? reuse.logo : null; + this.website = reuse && reuse.website ? reuse.website : null; + this.reuseTypes = reuse && reuse.reuseTypes ? reuse.reuseTypes : []; + this.datasetsUsed = reuse && reuse.datasetsUsed ? reuse.datasetsUsed : []; + if (reuse && reuse.createDate) { + this.createDate = reuse.createDate; + } + + if (reuse && reuse.updateDate) { + this.updateDate = reuse.updateDate; + } + this.published = reuse && reuse.published ? reuse.published : false; + } +} + +export interface IReuse { + _id: string; + name: string; + creator: string; + logo: string; + website: string; + reuseTypes: string[]; + datasetsUsed: string[]; + createDate: Date; + updateDate: Date; + published: boolean; +} diff --git a/src/app/services/app-config.service.ts b/src/app/services/app-config.service.ts index 6d46d729c135f9121176f55f87d60ee3b835aeca..6fd575157106c73012ef4e51cc685656759ab2bc 100644 --- a/src/app/services/app-config.service.ts +++ b/src/app/services/app-config.service.ts @@ -22,6 +22,9 @@ export class AppConfig { credits: { url: string; }; + reuses: { + url: string; + }; } export let APP_CONFIG: AppConfig; diff --git a/src/app/services/index.ts b/src/app/services/index.ts index 484bd65dab315967dd84e70fd746163fe4b478c6..196da86731a0fd1ff8684d8b72e732bc05a46f3b 100644 --- a/src/app/services/index.ts +++ b/src/app/services/index.ts @@ -7,6 +7,7 @@ import { NavigationHistoryService } from './navigation-history.service'; import { ChangelogService } from './changelog.service'; import { CreditService } from './credit.service'; import { MediaService } from './media.service'; +import { ReuseService } from './reuse.service'; export { AppConfigService, @@ -18,6 +19,7 @@ export { ChangelogService, CreditService, MediaService, + ReuseService, }; // tslint:disable-next-line:variable-name @@ -31,4 +33,5 @@ export const AppServices = [ ChangelogService, CreditService, MediaService, + ReuseService, ]; diff --git a/src/app/services/reuse.service.ts b/src/app/services/reuse.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..2e63e82a9277daec777de588d2d87e7a9f8153b4 --- /dev/null +++ b/src/app/services/reuse.service.ts @@ -0,0 +1,92 @@ +import { Injectable } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { HttpClient } from '@angular/common/http'; +import { APP_CONFIG } from './app-config.service'; +import { ReuseRO, IReuse, Reuse } from '../models/reuse.model'; + +@Injectable() +export class ReuseService { + + reuseServiceUrl: string; + limit: number; + pageNumber: number; + sortOptions: { + value: string, + order: string, + }; + + private _searchChangeSubject: Subject<any>; + + constructor( + private _httpClient: HttpClient) { + this.reuseServiceUrl = `${APP_CONFIG.reuses.url}reuses/`; + this._searchChangeSubject = new Subject<any>(); + this.limit = 10; + this.pageNumber = 1; + } + + getReuses(): Observable<ReuseRO> { + let query = '?'; + query += `limit=${(this.limit ? this.limit : 20)}`; + query += `&offset=${(this.pageNumber ? (this.pageNumber - 1) * this.limit : 0)}`; + query += `&sort_by=${this.sortOptions.value}.${this.sortOptions.order}`; + + return this._httpClient.get<IReuse[]>(this.reuseServiceUrl + query, { observe: 'response' }).pipe( + map((response) => { + const totalCount = response.headers.get('Content-Range'); + const reuses = []; + response.body.forEach((reuse) => { + reuses.push(new Reuse(reuse)); + }); + return new ReuseRO(reuses, parseInt(totalCount, 10)); + })); + } + + findById(id): Observable<Reuse> { + return this._httpClient.get<IReuse>(this.reuseServiceUrl + id).pipe( + map((response) => { + return new Reuse(response); + }), + ); + } + + delete(id) { + return this._httpClient.delete(this.reuseServiceUrl + id, { withCredentials: true }); + } + + create(data) { + return this._httpClient.post<IReuse>(this.reuseServiceUrl, data, { withCredentials: true }).pipe( + map((response) => { + return new Reuse(response); + }), + ); + } + + update(data) { + return this._httpClient.put<IReuse>(this.reuseServiceUrl + data._id, data, { withCredentials: true }).pipe( + map((response) => { + return new Reuse(response); + }), + ); + } + + /* PAGINATION */ + paginationChanged(limit: number, pageNumber: number) { + this.limit = limit; + this.pageNumber = pageNumber; + this._searchChangeSubject.next(); + } + + reverseSortOrder(): void { + if (this.sortOptions.order === 'asc') { + this.sortOptions.order = 'desc'; + } else { + this.sortOptions.order = 'asc'; + } + } + + get searchChange$(): Observable<string> { + return this._searchChangeSubject.asObservable(); + } +} diff --git a/src/assets/config/config.json b/src/assets/config/config.json index f52d6a685b9971d965c8d38de381ecd66c850919..58c34dd6438b7815620c6f7c17588c12c3acdd8c 100644 --- a/src/assets/config/config.json +++ b/src/assets/config/config.json @@ -19,5 +19,8 @@ }, "credits": { "url": "https://kong-dev.alpha.grandlyon.com/credits/credits/" + }, + "reuses": { + "url": "http://localhost:3008/" } } \ No newline at end of file