diff --git a/client/angular.json b/client/angular.json index 3c6e06942e831b94ea0acd7276d679ce69329a44..1df04ec6cef2a25f24731f07fae0448bf96b9fd1 100644 --- a/client/angular.json +++ b/client/angular.json @@ -28,7 +28,8 @@ "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", - "src/assets" + "src/assets", + { "glob": "**/*", "input": "node_modules/tinymce", "output": "/tinymce/" } ], "styles": [ "node_modules/@fortawesome/fontawesome-free/css/all.css", diff --git a/client/package.json b/client/package.json index c085d335281027eb0572f85cac2166d3d66ae3e1..7cb731f859fd11ffa5b83216bc87523a75d9b7ea 100644 --- a/client/package.json +++ b/client/package.json @@ -24,15 +24,18 @@ "@ngrx/router-store": "13.0.2", "@ngrx/store": "13.0.2", "@ngrx/store-devtools": "13.0.2", + "@tinymce/tinymce-angular": "^6.0.1", "bootstrap": "4.6.1", "d3": "^5.15.1", "file-saver": "^2.0.5", "keycloak-angular": "^9.1.0", "keycloak-js": "^16.1.1", "ngx-bootstrap": "^8.0.0", + "ngx-dynamic-hooks": "^2.0.3", "ngx-json-viewer": "^3.0.2", "ngx-toastr": "^14.2.1", "rxjs": "~7.5.0", + "tinymce": "^6.0.3", "tslib": "^2.3.0", "zone.js": "~0.11.4" }, diff --git a/client/src/app/admin/admin.component.html b/client/src/app/admin/admin.component.html index bd1e260acf97f000884a585cceef5991791c07e7..5c884c3b52f4a5225b2b6596965fb0ac8845b72c 100644 --- a/client/src/app/admin/admin.component.html +++ b/client/src/app/admin/admin.component.html @@ -1,17 +1,12 @@ <header> - <app-navbar - [links]="links" + <app-admin-navbar [isAuthenticated]="isAuthenticated | async" [userProfile]="userProfile | async" - [userRoles]="userRoles | async" - [baseHref]="getBaseHref()" [authenticationEnabled]="getAuthenticationEnabled()" - [adminRoles]="getAdminRoles()" - [url]="url | async" (login)="login()" (logout)="logout()" (openEditProfile)="openEditProfile()"> - </app-navbar> + </app-admin-navbar> </header> <main role="main" class="container-fluid pb-4"> <router-outlet></router-outlet> diff --git a/client/src/app/admin/admin.component.ts b/client/src/app/admin/admin.component.ts index d8f886749a1519025e28d8556ec5207b4e81095c..3c55077ed60f028e2af755b90491a0becd7770c4 100644 --- a/client/src/app/admin/admin.component.ts +++ b/client/src/app/admin/admin.component.ts @@ -11,7 +11,6 @@ import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { Store } from '@ngrx/store'; -import * as fromRouter from '@ngrx/router-store'; import { UserProfile } from 'src/app/auth/user-profile.model'; import * as authActions from 'src/app/auth/auth.actions'; @@ -25,7 +24,7 @@ import { AppConfigService } from 'src/app/app-config.service'; }) /** * @class - * @classdesc Portal home container. + * @classdesc Admin container. * * @implements OnInit */ @@ -33,20 +32,12 @@ export class AdminComponent implements OnInit { public favIcon: HTMLLinkElement = document.querySelector('#favicon'); public title: HTMLLinkElement = document.querySelector('#title'); public body: HTMLBodyElement = document.querySelector('body'); - public links = [ - { label: 'Instances', icon: 'fas fa-object-group', routerLink: 'instance/instance-list' }, - { label: 'Databases', icon: 'fas fa-database', routerLink: 'database/database-list'} - ]; public isAuthenticated: Observable<boolean>; public userProfile: Observable<UserProfile>; - public userRoles: Observable<string[]>; - public url: Observable<string>; constructor(private store: Store<{ }>, private config: AppConfigService) { this.isAuthenticated = store.select(authSelector.selectIsAuthenticated); this.userProfile = store.select(authSelector.selectUserProfile); - this.userRoles = store.select(authSelector.selectUserRoles); - this.url = store.select(fromRouter.getSelectors().selectUrl); } ngOnInit() { @@ -56,18 +47,10 @@ export class AdminComponent implements OnInit { Promise.resolve(null).then(() => this.store.dispatch(databaseActions.loadDatabaseList())); } - getBaseHref() { - return this.config.baseHref; - } - getAuthenticationEnabled() { return this.config.authenticationEnabled; } - getAdminRoles(): string[] { - return this.config.adminRoles; - } - login(): void { this.store.dispatch(authActions.login({ redirectUri: window.location.toString() })); } diff --git a/client/src/app/admin/admin.module.ts b/client/src/app/admin/admin.module.ts index fc26cfe972c1cf7866d9180daa6b057dc56ac238..432b436da0520a466142cc0557cbda93913ef495 100644 --- a/client/src/app/admin/admin.module.ts +++ b/client/src/app/admin/admin.module.ts @@ -15,6 +15,7 @@ import { EffectsModule } from '@ngrx/effects'; import { SharedModule } from 'src/app/shared/shared.module'; import { AdminSharedModule } from './admin-shared/admin-shared.module'; import { AdminRoutingModule, routedComponents } from './admin-routing.module'; +import { dummiesComponents } from './components'; import { adminReducer } from './admin.reducer'; import { adminEffects } from './store/effects'; import { adminServices } from './store/services'; @@ -28,7 +29,8 @@ import { adminServices } from './store/services'; EffectsModule.forFeature(adminEffects) ], declarations: [ - routedComponents + routedComponents, + dummiesComponents ], providers: [ adminServices diff --git a/client/src/app/shared/components/navbar.component.html b/client/src/app/admin/components/admin-navbar.component.html similarity index 63% rename from client/src/app/shared/components/navbar.component.html rename to client/src/app/admin/components/admin-navbar.component.html index 347d4b7b7f1480d137d18442569f7f47c6f58426..155e4ffc30fb23b2a7bd94425beda48fddc04b8e 100644 --- a/client/src/app/shared/components/navbar.component.html +++ b/client/src/app/admin/components/admin-navbar.component.html @@ -1,35 +1,31 @@ <nav class="navbar navbar-light bg-light navbar-expand-md fixed-top border-bottom"> <!-- Logo --> - <a *ngIf="!instance" href="{{ baseHref }}" class="navbar-brand"> + <a routerLink="/admin" class="navbar-brand"> <img src="assets/cesam_anis40.png" alt="ANIS logo" /> </a> - <a *ngIf="instance" routerLink="/instance/{{ instance.name }}" class="navbar-brand"> - <img *ngIf="instance.public" [src]="getLogoHref()" alt="Instance logo" /> - <img *ngIf="!instance.public" [src]="getLogoHref() | authImage | async" alt="Instance logo" /> - </a> - - <!-- Right Navigation --> + <!-- Navigation --> <div class="collapse navbar-collapse" id="navbarCollapse"> <ul class="navbar-nav mr-auto"> - <li *ngFor="let link of links" class="nav-item pr-3"> - <a class="nav-link" [routerLink]="link.routerLink" routerLinkActive="active"> - <span [ngClass]="link.icon"></span> {{ link.label }} + <li class="nav-item pr-3"> + <a class="nav-link" routerLink="instance/instance-list" routerLinkActive="active"> + <span class="fas fa-object-group"></span> Instances + </a> + </li> + <li class="nav-item pr-3"> + <a class="nav-link" routerLink="database/database-list" routerLinkActive="active"> + <span class="fas fa-database"></span> Databases </a> </li> </ul> <ul class="navbar-nav justify-content-end"> <li class="nav-item pr-3"> - <a *ngIf="isAdminRoute() || (instance && instance.back_to_portal)" class="nav-link" routerLink="/portal" routerLinkActive="active"> + <a class="nav-link" routerLink="/portal" routerLinkActive="active"> <span class="fa-solid fa-right-to-bracket"></span> Back to portal </a> </li> - <li *ngIf="!authenticationEnabled && !isAdminRoute()" class="nav-item pr-3"> - <a class="nav-link" routerLink="/admin" routerLinkActive="active"> - <span class="fas fa-tools"></span> Admin - </a> - </li> </ul> + <!-- sign in / sign out --> <button *ngIf="authenticationEnabled && !isAuthenticated" class="btn btn-outline-success my-2 my-sm-0" id="button-sign-in" @@ -38,7 +34,7 @@ </button> <span *ngIf="isAuthenticated" id="dropdown-menu" dropdown> <button id="button-basic" dropdownToggle type="button" class="btn btn-light" aria-controls="dropdown-basic"> - <span class="fa-stack" [ngStyle]="{ color: instance ? instance.design_color : '#7AC29A' }"> + <span class="fa-stack" [ngStyle]="{ color: '#7AC29A' }"> <span class="fas fa-circle fa-2x"></span> <span class="fas fa-user fa-stack-1x fa-inverse"></span> </span> @@ -51,9 +47,6 @@ </li> <li class="divider dropdown-divider"></li> <li role="menuitem"> - <a *ngIf="isAdmin() && !isAdminRoute()" class="dropdown-item pointer" routerLink="/admin"> - <span class="fas fa-tools"></span> Admin - </a> <a class="dropdown-item pointer" (click)="openEditProfile.emit()"> <span class="fas fa-id-card"></span> Edit profile </a> @@ -68,7 +61,7 @@ </span> </div> - <!-- Dropdown appearing on mobile only --> + <!-- Navigation Mobile --> <span dropdown> <button id="button-basic" dropdownToggle type="button" class="navbar-toggler" aria-controls="dropdown-basic"> <span class="fas fa-bars"></span> @@ -78,27 +71,24 @@ <li *ngIf="isAuthenticated" role="menuitem"> <span class="dropdown-item font-italic">{{ userProfile.email }}</span> </li> - <li *ngIf="isAuthenticated && links.length > 0" class="divider dropdown-divider"></li> - <li *ngFor="let link of links" role="menuitem"> - <a class="dropdown-item" [routerLink]="link.routerLink"> - <span [ngClass]="link.icon" class="fa-fw"></span> {{ link.label }} + <li *ngIf="isAuthenticated" class="divider dropdown-divider"></li> + <li role="menuitem"> + <a class="dropdown-item" routerLink="instance/instance-list"> + <span class="fas fa-object-group fa-fw"></span> Instances </a> </li> <li role="menuitem"> - <a *ngIf="!isPortalRoute()" class="dropdown-item" routerLink="/portal"> - <span class="fa-solid fa-right-to-bracket fa-fw"></span> Back to portal + <a class="dropdown-item" routerLink="database/database-list"> + <span class="fas fa-database fa-fw"></span> Databases </a> </li> - <li *ngIf="isAuthenticated || (!authenticationEnabled && !isAdminRoute())" class="divider dropdown-divider"></li> - <li *ngIf="!authenticationEnabled && !isAdminRoute()" role="menuitem"> - <a class="dropdown-item pointer" routerLink="/admin"> - <span class="fas fa-tools"></span> Admin + <li role="menuitem"> + <a class="dropdown-item" routerLink="/portal"> + <span class="fa-solid fa-right-to-bracket fa-fw"></span> Back to portal </a> </li> + <li *ngIf="isAuthenticated" class="divider dropdown-divider"></li> <li *ngIf="isAuthenticated" role="menuitem"> - <a *ngIf="isAdmin() && !isAdminRoute()" class="dropdown-item pointer" routerLink="/admin"> - <span class="fas fa-tools"></span> Admin - </a> <a class="dropdown-item pointer" (click)="openEditProfile.emit()"> <span class="fas fa-id-card"></span> Edit profile </a> diff --git a/client/src/app/shared/components/navbar.component.scss b/client/src/app/admin/components/admin-navbar.component.scss similarity index 100% rename from client/src/app/shared/components/navbar.component.scss rename to client/src/app/admin/components/admin-navbar.component.scss diff --git a/client/src/app/admin/components/admin-navbar.component.ts b/client/src/app/admin/components/admin-navbar.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..9e61c3efdf1d468c8bdfbd0aeecbb63457af637b --- /dev/null +++ b/client/src/app/admin/components/admin-navbar.component.ts @@ -0,0 +1,18 @@ +import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter } from '@angular/core'; + +import { UserProfile } from 'src/app/auth/user-profile.model'; + +@Component({ + selector: 'app-admin-navbar', + templateUrl: 'admin-navbar.component.html', + styleUrls: [ 'admin-navbar.component.scss' ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AdminNavbarComponent { + @Input() isAuthenticated: boolean; + @Input() userProfile: UserProfile = null; + @Input() authenticationEnabled: boolean; + @Output() login: EventEmitter<any> = new EventEmitter(); + @Output() logout: EventEmitter<any> = new EventEmitter(); + @Output() openEditProfile: EventEmitter<any> = new EventEmitter(); +} diff --git a/client/src/app/admin/components/index.ts b/client/src/app/admin/components/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..f66c3652c6a8d53e08910ceeddbbdfbb1e11b2c7 --- /dev/null +++ b/client/src/app/admin/components/index.ts @@ -0,0 +1,14 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { AdminNavbarComponent } from './admin-navbar.component'; + +export const dummiesComponents = [ + AdminNavbarComponent +]; diff --git a/client/src/app/admin/instance/components/instance-form.component.html b/client/src/app/admin/instance/components/instance-form.component.html index 531367d983834932b80a5f0aba596ae582197c62..0dc13e93526e8680cb447761feace9c965ad3ce9 100644 --- a/client/src/app/admin/instance/components/instance-form.component.html +++ b/client/src/app/admin/instance/components/instance-form.component.html @@ -114,31 +114,6 @@ (loadDirectory)="onChangeFileSelect($event)"> </app-path-select-form-control> </accordion-group> - <accordion-group heading="Home page" [isOpen]="true"> - <div class="form-group"> - <label for="home_component">Component</label> - <select class="form-control" name="home_component" formControlName="home_component"> - <option value="WelcomeComponent">WelcomeComponent</option> - </select> - </div> - <div formGroupName="home_component_config"> - <div class="form-group"> - <label for="home_component_text">Text</label> - <textarea class="form-control" id="home_component_text" formControlName="home_component_text" rows="3"></textarea> - </div> - <app-path-select-form-control - [form]="getHomeConfigFormGroup()" - [disabled]="isFilesPathEmpty()" - [controlName]="'home_component_logo'" - [controlLabel]="'Logo'" - [files]="files" - [filesIsLoading]="filesIsLoading" - [filesIsLoaded]="filesIsLoaded" - [selectType]="'file'" - (loadDirectory)="onChangeFileSelect($event)"> - </app-path-select-form-control> - </div> - </accordion-group> <accordion-group heading="Functionalities" [isOpen]="true"> <div class="custom-control custom-switch mb-2"> <input class="custom-control-input" type="checkbox" id="samp_enabled" name="samp_enabled" formControlName="samp_enabled"> diff --git a/client/src/app/admin/instance/components/instance-form.component.ts b/client/src/app/admin/instance/components/instance-form.component.ts index 22f1c6d21f082ebaba3c77343d9d1b79463b75fe..00cb22c3a827821bf1144b1624fce6c5b3b011db 100644 --- a/client/src/app/admin/instance/components/instance-form.component.ts +++ b/client/src/app/admin/instance/components/instance-form.component.ts @@ -41,13 +41,6 @@ export class InstanceFormComponent implements OnInit { design_background_color: new FormControl('#FFFFFF', [Validators.required]), design_logo: new FormControl(''), design_favicon: new FormControl(''), - home_component: new FormControl('WelcomeComponent', [Validators.required]), - home_component_config: new FormGroup({ - home_component_text: new FormControl(`AstroNomical Information System is a generic web tool aimed -at facilitating and homogenizing the implementation of astronomical data. It allows -the fast implementation of a project data exchange platform in a dedicated information system.`, [Validators.required]), - home_component_logo: new FormControl('home_component_logo.png', [Validators.required]) - }), samp_enabled: new FormControl(true), back_to_portal: new FormControl(true), search_by_criteria_allowed: new FormControl(true), diff --git a/client/src/app/admin/instance/containers/configure-instance.component.ts b/client/src/app/admin/instance/containers/configure-instance.component.ts index ace89da19d750cce42ac7d6458b2dd269ce02335..1facdceaa375aac63137b514d1b9edc9de8e9e98 100644 --- a/client/src/app/admin/instance/containers/configure-instance.component.ts +++ b/client/src/app/admin/instance/containers/configure-instance.component.ts @@ -13,6 +13,8 @@ import { Store } from '@ngrx/store'; import * as datasetFamilyActions from 'src/app/metamodel/actions/dataset-family.actions'; import * as datasetActions from 'src/app/metamodel/actions/dataset.actions'; import * as datasetGroupActions from 'src/app/metamodel/actions/dataset-group.actions'; +import * as webpageFamilyActions from 'src/app/metamodel/actions/webpage-family.actions'; +import * as webpageActions from 'src/app/metamodel/actions/webpage.actions'; @Component({ selector: 'app-configure-instance', @@ -25,5 +27,7 @@ export class ConfigureInstanceComponent implements OnInit { Promise.resolve(null).then(() => this.store.dispatch(datasetFamilyActions.loadDatasetFamilyList())); Promise.resolve(null).then(() => this.store.dispatch(datasetActions.loadDatasetList())); Promise.resolve(null).then(() => this.store.dispatch(datasetGroupActions.loadDatasetGroupList())); + Promise.resolve(null).then(() => this.store.dispatch(webpageFamilyActions.loadWebpageFamilyList())); + Promise.resolve(null).then(() => this.store.dispatch(webpageActions.loadWebpageList())); } } diff --git a/client/src/app/admin/instance/dataset/components/instance-buttons.component.html b/client/src/app/admin/instance/dataset/components/instance-buttons.component.html index 89218e04727878415c9c8a3856bf101097b9e6f0..cfa2e25ec324e12edae22d41f56cdcbd0b998ed7 100644 --- a/client/src/app/admin/instance/dataset/components/instance-buttons.component.html +++ b/client/src/app/admin/instance/dataset/components/instance-buttons.component.html @@ -1,10 +1,15 @@ <div class="btn-toolbar mb-3" role="toolbar" aria-label="Toolbar with button groups"> - <div class="btn-group mr-2" role="group" aria-label="First group"> + <div class="btn-group mr-2" role="group" aria-label="New Dataset family"> <button (click)="openModal(template); $event.stopPropagation()" title="Add new dataset family" class="btn btn-outline-success"> <span class="fas fa-plus"></span> New dataset family </button> </div> - <div class="btn-group mr-2" role="group" aria-label="Second group"> + <div class="btn-group mr-2" role="group" aria-label="Edit webpages"> + <button routerLink="/admin/instance/configure-instance/{{ instance.name }}/webpage" title="Edit instances webpages" class="btn btn-outline-primary"> + <span class="fas fa-file-lines"></span> Edit instance webpages + </button> + </div> + <div class="btn-group mr-2" role="group" aria-label="Handle dataset groups"> <button routerLink="/admin/instance/configure-instance/{{ instance.name }}/dataset-group" title="Handle groups" class="btn btn-outline-primary"> <span class="fas fa-users"></span> Handle dataset groups </button> diff --git a/client/src/app/admin/instance/instance-routing.module.ts b/client/src/app/admin/instance/instance-routing.module.ts index d7f78cc8e9869a9fe8744a108db608bfdb3fca2f..1c39ca08d56b458fb9c5b30a3d68447d038efeba 100644 --- a/client/src/app/admin/instance/instance-routing.module.ts +++ b/client/src/app/admin/instance/instance-routing.module.ts @@ -29,7 +29,8 @@ const routes: Routes = [ [ { path: '', redirectTo: 'dataset/dataset-list', pathMatch: 'full' }, { path: 'dataset', loadChildren: () => import('./dataset/dataset.module').then(m => m.DatasetModule) }, - { path: 'dataset-group', loadChildren: () => import('./dataset-group/dataset-group.module').then(m => m.DatasetGroupModule) } + { path: 'dataset-group', loadChildren: () => import('./dataset-group/dataset-group.module').then(m => m.DatasetGroupModule) }, + { path: 'webpage', loadChildren: () => import('./webpage/webpage.module').then(m => m.WebpageModule) }, ] }, ]; diff --git a/client/src/app/admin/instance/webpage/components/edit-webpage-family.component.html b/client/src/app/admin/instance/webpage/components/edit-webpage-family.component.html new file mode 100644 index 0000000000000000000000000000000000000000..b465fe5e7d0343a7d3ca979ea6d11a03689de402 --- /dev/null +++ b/client/src/app/admin/instance/webpage/components/edit-webpage-family.component.html @@ -0,0 +1,18 @@ +<button title="Edit this webpage family" (click)="openModal(template)" class="btn btn-outline-primary"> + <span class="fas fa-edit"></span> +</button> + +<ng-template #template> + <div class="modal-header"> + <h4 class="modal-title pull-left"><strong>Edit webpage family</strong></h4> + </div> + <div class="modal-body"> + <app-webpage-family-form [webpageFamily]="webpageFamily" (onSubmit)="onSubmit.emit($event)" #formEditWebpageFamily> + <button [disabled]="!formEditWebpageFamily.form.valid || formEditWebpageFamily.form.pristine" (click)="modalRef.hide()" type="submit" class="btn btn-primary"> + <span class="fa fa-database"></span> Edit webpage family + </button> + + <button (click)="modalRef.hide()" type="button" class="btn btn-danger">Cancel</button> + </app-webpage-family-form> + </div> +</ng-template> diff --git a/client/src/app/admin/instance/webpage/components/edit-webpage-family.component.ts b/client/src/app/admin/instance/webpage/components/edit-webpage-family.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..3333361a9b4e9b6f749221453edfe83cfd369e56 --- /dev/null +++ b/client/src/app/admin/instance/webpage/components/edit-webpage-family.component.ts @@ -0,0 +1,33 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Component, Input, ChangeDetectionStrategy, TemplateRef, Output, EventEmitter } from '@angular/core'; + +import { BsModalService } from 'ngx-bootstrap/modal'; +import { BsModalRef } from 'ngx-bootstrap/modal/bs-modal-ref.service'; + +import { WebpageFamily } from 'src/app/metamodel/models'; + +@Component({ + selector: 'app-edit-webpage-family', + templateUrl: 'edit-webpage-family.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class EditWebpageFamilyComponent { + @Input() webpageFamily: WebpageFamily; + @Output() onSubmit: EventEmitter<WebpageFamily> = new EventEmitter(); + + modalRef: BsModalRef; + + constructor(private modalService: BsModalService) { } + + openModal(template: TemplateRef<any>) { + this.modalRef = this.modalService.show(template); + } +} diff --git a/client/src/app/admin/instance/webpage/components/index.ts b/client/src/app/admin/instance/webpage/components/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..401f94b17e72569f4c4615a909480360b70ab631 --- /dev/null +++ b/client/src/app/admin/instance/webpage/components/index.ts @@ -0,0 +1,24 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { WebpageButtonsComponent } from './webpage-buttons.component'; +import { WebpageFamilyCardComponent } from './webpage-family-card.component'; +import { EditWebpageFamilyComponent } from './edit-webpage-family.component'; +import { WebpageFamilyFormComponent } from './webpage-family-form.component'; +import { WebpageCardComponent } from './webpage-card.component'; +import { WebpageFormComponent } from './webpage-form.component'; + +export const dummiesComponents = [ + WebpageButtonsComponent, + WebpageFamilyCardComponent, + EditWebpageFamilyComponent, + WebpageFamilyFormComponent, + WebpageCardComponent, + WebpageFormComponent +]; diff --git a/client/src/app/admin/instance/webpage/components/webpage-buttons.component.html b/client/src/app/admin/instance/webpage/components/webpage-buttons.component.html new file mode 100644 index 0000000000000000000000000000000000000000..b755d6b2da4daf7dbca7904df3879506ee09d378 --- /dev/null +++ b/client/src/app/admin/instance/webpage/components/webpage-buttons.component.html @@ -0,0 +1,22 @@ +<div class="btn-toolbar mb-3" role="toolbar"> + <div class="btn-group mr-2" role="group"> + <button (click)="openModal(template); $event.stopPropagation()" title="Add new webpage family" class="btn btn-outline-success"> + <span class="fas fa-plus"></span> New webpage family + </button> + </div> +</div> + +<ng-template #template> + <div class="modal-header"> + <h4 class="modal-title pull-left"><strong>Add a new webpage family</strong></h4> + </div> + <div class="modal-body"> + <app-webpage-family-form (onSubmit)="addWebpageFamily.emit($event)" #formAddWebpageFamily> + <button [disabled]="!formAddWebpageFamily.form.valid || formAddWebpageFamily.form.pristine" (click)="modalRef.hide()" type="submit" class="btn btn-primary"> + <span class="fa fa-database"></span> Add new webpage family + </button> + + <button (click)="modalRef.hide()" type="button" class="btn btn-danger">Cancel</button> + </app-webpage-family-form> + </div> +</ng-template> diff --git a/client/src/app/admin/instance/webpage/components/webpage-buttons.component.ts b/client/src/app/admin/instance/webpage/components/webpage-buttons.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..2c5ff64716d0c3af97c78b5388d56381cc13336a --- /dev/null +++ b/client/src/app/admin/instance/webpage/components/webpage-buttons.component.ts @@ -0,0 +1,32 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Component, Output, ChangeDetectionStrategy, EventEmitter, TemplateRef, Input } from '@angular/core'; + +import { BsModalService } from 'ngx-bootstrap/modal'; +import { BsModalRef } from 'ngx-bootstrap/modal/bs-modal-ref.service'; + +import { WebpageFamily } from 'src/app/metamodel/models'; + +@Component({ + selector: 'app-webpage-buttons', + templateUrl: 'webpage-buttons.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class WebpageButtonsComponent { + @Output() addWebpageFamily: EventEmitter<WebpageFamily> = new EventEmitter(); + + modalRef: BsModalRef; + + constructor(private modalService: BsModalService) { } + + openModal(template: TemplateRef<any>) { + this.modalRef = this.modalService.show(template); + } +} diff --git a/client/src/app/admin/instance/webpage/components/webpage-card.component.html b/client/src/app/admin/instance/webpage/components/webpage-card.component.html new file mode 100644 index 0000000000000000000000000000000000000000..96ec727676b0163e16c738392a1ecf073141df2f --- /dev/null +++ b/client/src/app/admin/instance/webpage/components/webpage-card.component.html @@ -0,0 +1,21 @@ +<div class="card"> + <div class="card-body"> + <h5 class="card-title font-weight-bold">{{ webpage.label }}</h5> + <ul class="card-text list-unstyled pl-4"> + <li>Icon: <span [ngClass]="webpage.icon"></span></li> + <li>Title: <span class="badge badge-secondary">{{ webpage.title }}</span></li> + <li>Display: <span class="badge badge-secondary">{{ webpage.display }}</span></li> + </ul> + </div> + <div class="card-footer bg-transparent text-right"> + <a title="Edit this webpage" routerLink="edit-webpage/{{webpage.id}}" class="btn btn-outline-primary"> + <span class="fas fa-edit"></span> + </a> + + <app-delete-btn + [type]="'webpage'" + [label]="webpage.label" + (confirm)="deleteWebpage.emit(webpage)"> + </app-delete-btn> + </div> +</div> diff --git a/client/src/app/admin/instance/webpage/components/webpage-card.component.scss b/client/src/app/admin/instance/webpage/components/webpage-card.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..2325ac153e67f992d8334bb20def08bf574a347b --- /dev/null +++ b/client/src/app/admin/instance/webpage/components/webpage-card.component.scss @@ -0,0 +1,12 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +.card { + height: 200px; +} \ No newline at end of file diff --git a/client/src/app/admin/instance/webpage/components/webpage-card.component.ts b/client/src/app/admin/instance/webpage/components/webpage-card.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef52c705f90cba6a19d1b9229872a02852ebcae1 --- /dev/null +++ b/client/src/app/admin/instance/webpage/components/webpage-card.component.ts @@ -0,0 +1,13 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; + +import { Webpage } from 'src/app/metamodel/models'; + +@Component({ + selector: 'app-webpage-card', + templateUrl: 'webpage-card.component.html', + styleUrls: [ 'webpage-card.component.scss' ] +}) +export class WebpageCardComponent { + @Input() webpage: Webpage; + @Output() deleteWebpage: EventEmitter<Webpage> = new EventEmitter(); +} diff --git a/client/src/app/admin/instance/webpage/components/webpage-family-card.component.html b/client/src/app/admin/instance/webpage/components/webpage-family-card.component.html new file mode 100644 index 0000000000000000000000000000000000000000..e84682a97849f925e8d080ae925f83d5d5310c17 --- /dev/null +++ b/client/src/app/admin/instance/webpage/components/webpage-family-card.component.html @@ -0,0 +1,40 @@ +<div class="card mb-3"> + <div class="card-header"> + <div class="row align-items-center"> + <div class="col-md-10"> + <span class="card-title font-weight-bold">{{ webpageFamily.label }}</span> + (Display : {{ webpageFamily.display }}, icon : <span [ngClass]="webpageFamily.icon"></span> ) + </div> + <div class="col-md-2 text-right"> + <app-edit-webpage-family + [webpageFamily]="webpageFamily" + (onSubmit)="editWebpageFamily.emit($event)"> + </app-edit-webpage-family> + + <app-delete-btn + [disabled]="nbWebpagesByWebpageFamily() > 0" + [type]="'webpage-family'" + [label]="webpageFamily.label" + (confirm)="deleteWebpageFamily.emit(webpageFamily)"> + </app-delete-btn> + </div> + </div> + </div> + <div class="card-body"> + <div class="row row-cols-1 row-cols-sm-2 row-cols-md-3"> + <div *ngFor="let webpage of (webpageList | webpageListByFamily:webpageFamily.id)" class="col mb-3"> + <app-webpage-card + [webpage]="webpage" + (deleteWebpage)="deleteWebpage.emit($event)"> + </app-webpage-card> + </div> + <div class="col h-100 d-table"> + <div routerLink="new-webpage" [queryParams]="{id_webpage_family: webpageFamily.id}" class="card card-add d-table-cell align-middle pointer" title="Add new webpage"> + <div class="card-body text-center"> + <span class="fas fa-plus fa-4x text-light"></span> + </div> + </div> + </div> + </div> + </div> +</div> diff --git a/client/src/app/admin/instance/webpage/components/webpage-family-card.component.scss b/client/src/app/admin/instance/webpage/components/webpage-family-card.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..0ac62a8554a7e9c2218b358116ba288d7ec9e8d4 --- /dev/null +++ b/client/src/app/admin/instance/webpage/components/webpage-family-card.component.scss @@ -0,0 +1,19 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +.card-add { + height: 200px; + background-color: #A8C96E; + transition: font-size 0.3s; +} + +.card-add:hover { + background-color: #9dc25b; + font-size: 20px; +} diff --git a/client/src/app/admin/instance/webpage/components/webpage-family-card.component.ts b/client/src/app/admin/instance/webpage/components/webpage-family-card.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..e3a48c07775c727bad9b547e9f87873668655cc5 --- /dev/null +++ b/client/src/app/admin/instance/webpage/components/webpage-family-card.component.ts @@ -0,0 +1,30 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Component, Input, Output, ChangeDetectionStrategy, EventEmitter } from '@angular/core'; + +import { WebpageFamily, Webpage } from 'src/app/metamodel/models'; + +@Component({ + selector: 'app-webpage-family-card', + templateUrl: 'webpage-family-card.component.html', + styleUrls: [ 'webpage-family-card.component.scss' ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class WebpageFamilyCardComponent { + @Input() webpageFamily: WebpageFamily; + @Input() webpageList: Webpage[]; + @Output() editWebpageFamily: EventEmitter<WebpageFamily> = new EventEmitter(); + @Output() deleteWebpageFamily: EventEmitter<WebpageFamily> = new EventEmitter(); + @Output() deleteWebpage: EventEmitter<Webpage> = new EventEmitter(); + + nbWebpagesByWebpageFamily(): number { + return this.webpageList.filter(webpage => webpage.id_webpage_family === this.webpageFamily.id).length; + } +} diff --git a/client/src/app/admin/instance/webpage/components/webpage-family-form.component.html b/client/src/app/admin/instance/webpage/components/webpage-family-form.component.html new file mode 100644 index 0000000000000000000000000000000000000000..12d5f5b845964d6b9b5bcd2c781b5a356e36cc0d --- /dev/null +++ b/client/src/app/admin/instance/webpage/components/webpage-family-form.component.html @@ -0,0 +1,17 @@ +<form [formGroup]="form" (ngSubmit)="submit()" novalidate> + <div class="form-group"> + <label for="label">Label</label> + <input type="text" class="form-control" id="label" name="label" formControlName="label"> + </div> + <div class="form-group"> + <label for="icon">Icon</label> + <input type="text" class="form-control" id="icon" name="icon" formControlName="icon"> + </div> + <div class="form-group"> + <label for="display">Display</label> + <input type="number" class="form-control" id="display" name="display" formControlName="display"> + </div> + <div class="form-group"> + <ng-content></ng-content> + </div> +</form> diff --git a/client/src/app/admin/instance/webpage/components/webpage-family-form.component.ts b/client/src/app/admin/instance/webpage/components/webpage-family-form.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..1449e3818a8511ddec635f3238e2f564be8333a8 --- /dev/null +++ b/client/src/app/admin/instance/webpage/components/webpage-family-form.component.ts @@ -0,0 +1,45 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import { WebpageFamily } from 'src/app/metamodel/models'; + +@Component({ + selector: 'app-webpage-family-form', + templateUrl: 'webpage-family-form.component.html' +}) +export class WebpageFamilyFormComponent implements OnInit { + @Input() webpageFamily: WebpageFamily; + @Output() onSubmit: EventEmitter<WebpageFamily> = new EventEmitter(); + + public form = new FormGroup({ + label: new FormControl('', [Validators.required]), + icon: new FormControl(null), + display: new FormControl('', [Validators.required]) + }); + + ngOnInit() { + if (this.webpageFamily) { + this.form.patchValue(this.webpageFamily); + } + } + + submit() { + if (this.webpageFamily) { + this.onSubmit.emit({ + ...this.webpageFamily, + ...this.form.value + }); + } else { + this.onSubmit.emit(this.form.value); + } + } +} diff --git a/client/src/app/admin/instance/webpage/components/webpage-form.component.html b/client/src/app/admin/instance/webpage/components/webpage-form.component.html new file mode 100644 index 0000000000000000000000000000000000000000..dd32c63dc6bf4c234bba7a80b738379bba5c285d --- /dev/null +++ b/client/src/app/admin/instance/webpage/components/webpage-form.component.html @@ -0,0 +1,33 @@ +<form [formGroup]="form" (ngSubmit)="submit()" novalidate> + <div class="form-group"> + <label for="label">Label</label> + <input type="text" class="form-control" id="label" name="label" formControlName="label"> + </div> + <div class="form-group"> + <label for="icon">Icon</label> + <input type="text" class="form-control" id="icon" name="icon" formControlName="icon"> + </div> + <div class="form-group"> + <label for="display">Display</label> + <input type="number" class="form-control" id="display" name="display" formControlName="display"> + </div> + <div class="form-group"> + <label for="title">Title</label> + <input type="text" class="form-control" id="title" name="title" formControlName="title"> + </div> + <div class="form-group"> + <label for="id_webpage_family">Family</label> + <select class="form-control" id="id_webpage_family" name="id_webpage_family" formControlName="id_webpage_family"> + <option></option> + <option *ngFor="let family of webpageFamilyList" [ngValue]="family.id">{{ family.label }}</option> + </select> + </div> + <div class="form-group"> + <label for="content">Content</label> + <editor [init]="getEditorConfig()" formControlName="content"> + </editor> + </div> + <div class="form-group pt-4"> + <ng-content></ng-content> + </div> +</form> diff --git a/client/src/app/admin/instance/webpage/components/webpage-form.component.ts b/client/src/app/admin/instance/webpage/components/webpage-form.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..e473ea5338604cc3fc87b8ee7d71d4e72b99ef58 --- /dev/null +++ b/client/src/app/admin/instance/webpage/components/webpage-form.component.ts @@ -0,0 +1,51 @@ +import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import { Webpage, WebpageFamily } from 'src/app/metamodel/models'; + +@Component({ + selector: 'app-webpage-form', + templateUrl: 'webpage-form.component.html' +}) +export class WebpageFormComponent implements OnInit { + @Input() webpage: Webpage; + @Input() webpageFamilyList: WebpageFamily[]; + @Input() idWebpageFamily: number; + @Output() onSubmit: EventEmitter<Webpage> = new EventEmitter(); + + public form = new FormGroup({ + label: new FormControl('', [Validators.required]), + icon: new FormControl(null), + display: new FormControl('', [Validators.required]), + title: new FormControl('', [Validators.required]), + content: new FormControl('', [Validators.required]), + id_webpage_family: new FormControl('', [Validators.required]) + }); + + ngOnInit() { + if (this.webpage) { + this.form.patchValue(this.webpage); + } + if (this.idWebpageFamily) { + this.form.controls.id_webpage_family.setValue(this.idWebpageFamily); + } + } + + submit() { + if (this.webpage) { + this.onSubmit.emit({ + ...this.webpage, + ...this.form.getRawValue() + }); + } else { + this.onSubmit.emit(this.form.getRawValue()); + } + } + + getEditorConfig() { + return { + plugins: "lists link image table code help wordcount media", + content_css: "https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" + }; + } +} diff --git a/client/src/app/admin/instance/webpage/containers/edit-webpage.component.html b/client/src/app/admin/instance/webpage/containers/edit-webpage.component.html new file mode 100644 index 0000000000000000000000000000000000000000..bafa14c209f1216f7fe95ffcc803ab0bf770a75d --- /dev/null +++ b/client/src/app/admin/instance/webpage/containers/edit-webpage.component.html @@ -0,0 +1,44 @@ +<app-spinner *ngIf="webpageListIsLoading | async"></app-spinner> + +<div *ngIf="webpageListIsLoaded | async" class="container-fluid"> + <nav aria-label="breadcrumb"> + <ol class="breadcrumb"> + <li class="breadcrumb-item"> + <a routerLink="/admin/instance/instance-list"> + Instances + </a> + </li> + <li class="breadcrumb-item"> + <a routerLink="/admin/instance/configure-instance/{{ instanceName | async }}"> + Configure instance {{ instanceName | async }} + </a> + </li> + <li class="breadcrumb-item active"> + <a routerLink="/admin/instance/configure-instance/{{ instanceName | async }}/webpage"> + Edit instance webpages + </a> + </li> + <li class="breadcrumb-item active" aria-current="page">Edit webpage {{ (webpage | async).label }}</li> + </ol> + </nav> +</div> + +<div class="container"> + <app-spinner *ngIf="(webpageListIsLoading | async) || (webpageFamilyListIsLoading | async)"></app-spinner> + + <div *ngIf="(webpageListIsLoaded | async) && (webpageFamilyListIsLoaded | async)" class="row"> + <div class="col-12"> + <app-webpage-form + [webpage]="webpage | async" + [webpageFamilyList]="webpageFamilyList | async" + (onSubmit)="editWebpage($event)" + #formWebpage> + <button [disabled]="!formWebpage.form.valid || formWebpage.form.pristine" type="submit" class="btn btn-primary"> + <span class="fa fa-database"></span> Edit webpage + </button> + + <a routerLink="/admin/instance/configure-instance/{{ instanceName | async }}/webpage" class="btn btn-danger">Cancel</a> + </app-webpage-form> + </div> + </div> +</div> diff --git a/client/src/app/admin/instance/webpage/containers/edit-webpage.component.ts b/client/src/app/admin/instance/webpage/containers/edit-webpage.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..7fe0d9426a4dcc27dee4acdd9892a5d49b48b457 --- /dev/null +++ b/client/src/app/admin/instance/webpage/containers/edit-webpage.component.ts @@ -0,0 +1,47 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Component } from '@angular/core'; + +import { Observable } from 'rxjs'; +import { Store } from '@ngrx/store'; + +import { WebpageFamily, Webpage } from 'src/app/metamodel/models'; +import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector'; +import * as webpageActions from 'src/app/metamodel/actions/webpage.actions'; +import * as webpageSelector from 'src/app/metamodel/selectors/webpage.selector'; +import * as webpageFamilySelector from 'src/app/metamodel/selectors/webpage-family.selector'; + +@Component({ + selector: 'app-edit-webpage', + templateUrl: 'edit-webpage.component.html' +}) +export class EditWebpageComponent { + public instanceName: Observable<string>; + public webpageListIsLoading: Observable<boolean>; + public webpageListIsLoaded: Observable<boolean>; + public webpage: Observable<Webpage>; + public webpageFamilyListIsLoading: Observable<boolean>; + public webpageFamilyListIsLoaded: Observable<boolean>; + public webpageFamilyList: Observable<WebpageFamily[]>; + + constructor(private store: Store<{ }>) { + this.instanceName = this.store.select(instanceSelector.selectInstanceNameByRoute); + this.webpageListIsLoading = store.select(webpageSelector.selectWebpageListIsLoading); + this.webpageListIsLoaded = store.select(webpageSelector.selectWebpageListIsLoaded); + this.webpage = this.store.select(webpageSelector.selectWebpageByRouteId); + this.webpageFamilyListIsLoading = store.select(webpageFamilySelector.selectWebpageFamilyListIsLoading); + this.webpageFamilyListIsLoaded = store.select(webpageFamilySelector.selectWebpageFamilyListIsLoaded); + this.webpageFamilyList = store.select(webpageFamilySelector.selectAllWebpageFamilies); + } + + editWebpage(webpage: Webpage) { + this.store.dispatch(webpageActions.editWebpage({ webpage })); + } +} diff --git a/client/src/app/admin/instance/webpage/containers/new-webpage.component.html b/client/src/app/admin/instance/webpage/containers/new-webpage.component.html new file mode 100644 index 0000000000000000000000000000000000000000..26af60c6ccd93d8f4ae85942a5a7a795f35ce3e4 --- /dev/null +++ b/client/src/app/admin/instance/webpage/containers/new-webpage.component.html @@ -0,0 +1,42 @@ +<div class="container-fluid"> + <nav aria-label="breadcrumb"> + <ol class="breadcrumb"> + <li class="breadcrumb-item"> + <a routerLink="/admin/instance/instance-list"> + Instances + </a> + </li> + <li class="breadcrumb-item"> + <a routerLink="/admin/instance/configure-instance/{{ instanceName | async }}"> + Configure instance {{ instanceName | async }} + </a> + </li> + <li class="breadcrumb-item active"> + <a routerLink="/admin/instance/configure-instance/{{ instanceName | async }}/webpage"> + Edit instance webpages + </a> + </li> + <li class="breadcrumb-item active" aria-current="page">New webpage</li> + </ol> + </nav> +</div> + +<div class="container"> + <app-spinner *ngIf="webpageFamilyListIsLoading | async"></app-spinner> + + <div *ngIf="webpageFamilyListIsLoaded | async" class="row"> + <div class="col-12"> + <app-webpage-form + [webpageFamilyList]="webpageFamilyList | async" + [idWebpageFamily]="idWebpageFamily | async" + (onSubmit)="addNewWebpage($event)" + #formWebpage> + <button [disabled]="!formWebpage.form.valid || formWebpage.form.pristine" type="submit" class="btn btn-primary"> + <span class="fa fa-database"></span> Add the new webpage + </button> + + <a routerLink="/admin/instance/configure-instance/{{ instanceName | async }}/webpage" class="btn btn-danger">Cancel</a> + </app-webpage-form> + </div> + </div> +</div> diff --git a/client/src/app/admin/instance/webpage/containers/new-webpage.component.ts b/client/src/app/admin/instance/webpage/containers/new-webpage.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..7e1aec308799fd42937c050ed357daea9e3265fe --- /dev/null +++ b/client/src/app/admin/instance/webpage/containers/new-webpage.component.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { WebpageFamily, Webpage } from 'src/app/metamodel/models'; +import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector'; +import * as webpageFamilySelector from 'src/app/metamodel/selectors/webpage-family.selector'; +import * as webpageActions from 'src/app/metamodel/actions/webpage.actions'; + +@Component({ + selector: 'app-new-webpage', + templateUrl: 'new-webpage.component.html' +}) +export class NewWebpageComponent { + public instanceName: Observable<string>; + public webpageFamilyListIsLoading: Observable<boolean>; + public webpageFamilyListIsLoaded: Observable<boolean>; + public webpageFamilyList: Observable<WebpageFamily[]>; + public idWebpageFamily: Observable<number>; + + constructor(private store: Store<{ }>, private route: ActivatedRoute) { + this.instanceName = this.store.select(instanceSelector.selectInstanceNameByRoute); + this.webpageFamilyListIsLoading = store.select(webpageFamilySelector.selectWebpageFamilyListIsLoading); + this.webpageFamilyListIsLoaded = store.select(webpageFamilySelector.selectWebpageFamilyListIsLoaded); + this.webpageFamilyList = store.select(webpageFamilySelector.selectAllWebpageFamilies); + } + + ngOnInit() { + this.idWebpageFamily = this.route.queryParamMap.pipe( + map(params => +params.get('id_webpage_family')) + ); + } + + addNewWebpage(webpage: Webpage) { + this.store.dispatch(webpageActions.addWebpage({ webpage })); + } +} diff --git a/client/src/app/admin/instance/webpage/containers/webpage-list.component.html b/client/src/app/admin/instance/webpage/containers/webpage-list.component.html new file mode 100644 index 0000000000000000000000000000000000000000..e84042daac26e2eff5383147142a4a82fa4f12e0 --- /dev/null +++ b/client/src/app/admin/instance/webpage/containers/webpage-list.component.html @@ -0,0 +1,36 @@ +<div class="container-fluid"> + <nav aria-label="breadcrumb"> + <ol class="breadcrumb"> + <li class="breadcrumb-item"> + <a routerLink="/admin/instance/instance-list"> + Instances + </a> + </li> + <li class="breadcrumb-item"> + <a routerLink="/admin/instance/configure-instance/{{ instanceName | async }}"> + Configure instance {{ instanceName | async }} + </a> + </li> + <li class="breadcrumb-item active" aria-current="page">Edit instance webpages</li> + </ol> + </nav> +</div> + +<div class="container"> + <app-spinner *ngIf="(webpageFamilyListIsLoading | async) || (webpageListIsLoading | async)"></app-spinner> + + <ng-container *ngIf="(webpageFamilyListIsLoaded | async) && (webpageListIsLoaded | async)"> + <app-webpage-buttons + (addWebpageFamily)="addWebpageFamily($event)"> + </app-webpage-buttons> + + <app-webpage-family-card + *ngFor="let webpageFamily of (webpageFamilyList | async)" + [webpageFamily]="webpageFamily" + [webpageList]="webpageList | async" + (editWebpageFamily)="editWebpageFamily($event)" + (deleteWebpageFamily)="deleteWebpageFamily($event)" + (deleteWebpage)="deleteWebpage($event)"> + </app-webpage-family-card> + </ng-container> +</div> diff --git a/client/src/app/admin/instance/webpage/containers/webpage-list.component.scss b/client/src/app/admin/instance/webpage/containers/webpage-list.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..0ac62a8554a7e9c2218b358116ba288d7ec9e8d4 --- /dev/null +++ b/client/src/app/admin/instance/webpage/containers/webpage-list.component.scss @@ -0,0 +1,19 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +.card-add { + height: 200px; + background-color: #A8C96E; + transition: font-size 0.3s; +} + +.card-add:hover { + background-color: #9dc25b; + font-size: 20px; +} diff --git a/client/src/app/admin/instance/webpage/containers/webpage-list.component.ts b/client/src/app/admin/instance/webpage/containers/webpage-list.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..3b1fa3bda6ff869e4cb3ddc0e9b31fa07a170ba6 --- /dev/null +++ b/client/src/app/admin/instance/webpage/containers/webpage-list.component.ts @@ -0,0 +1,61 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Component } from '@angular/core'; + +import { Observable } from 'rxjs'; +import { Store } from '@ngrx/store'; + +import { WebpageFamily, Webpage } from 'src/app/metamodel/models'; +import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector'; +import * as webpageFamilyActions from 'src/app/metamodel/actions/webpage-family.actions'; +import * as webpageFamilySelector from 'src/app/metamodel/selectors/webpage-family.selector'; +import * as webpageActions from 'src/app/metamodel/actions/webpage.actions'; +import * as webpageSelector from 'src/app/metamodel/selectors/webpage.selector'; + +@Component({ + selector: 'app-webpage-list', + templateUrl: 'webpage-list.component.html', + styleUrls: ['webpage-list.component.scss'] +}) +export class WebpageListComponent { + public instanceName: Observable<string>; + public webpageFamilyListIsLoading: Observable<boolean>; + public webpageFamilyListIsLoaded: Observable<boolean>; + public webpageFamilyList: Observable<WebpageFamily[]>; + public webpageListIsLoading: Observable<boolean>; + public webpageListIsLoaded: Observable<boolean>; + public webpageList: Observable<Webpage[]>; + + constructor(private store: Store<{ }>) { + this.instanceName = this.store.select(instanceSelector.selectInstanceNameByRoute); + this.webpageFamilyListIsLoading = store.select(webpageFamilySelector.selectWebpageFamilyListIsLoading); + this.webpageFamilyListIsLoaded = store.select(webpageFamilySelector.selectWebpageFamilyListIsLoaded); + this.webpageFamilyList = store.select(webpageFamilySelector.selectAllWebpageFamilies); + this.webpageListIsLoading = store.select(webpageSelector.selectWebpageListIsLoading); + this.webpageListIsLoaded = store.select(webpageSelector.selectWebpageListIsLoaded); + this.webpageList = store.select(webpageSelector.selectAllWebpages); + } + + addWebpageFamily(webpageFamily: WebpageFamily) { + this.store.dispatch(webpageFamilyActions.addWebpageFamily({ webpageFamily })); + } + + editWebpageFamily(webpageFamily: WebpageFamily) { + this.store.dispatch(webpageFamilyActions.editWebpageFamily({ webpageFamily })); + } + + deleteWebpageFamily(webpageFamily: WebpageFamily) { + this.store.dispatch(webpageFamilyActions.deleteWebpageFamily({ webpageFamily })); + } + + deleteWebpage(webpage: Webpage) { + this.store.dispatch(webpageActions.deleteWebpage({ webpage })); + } +} diff --git a/client/src/app/admin/instance/webpage/webpage-routing.module.ts b/client/src/app/admin/instance/webpage/webpage-routing.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..c162b80f521725e4a5e7ce25ebbb18c687d56fed --- /dev/null +++ b/client/src/app/admin/instance/webpage/webpage-routing.module.ts @@ -0,0 +1,37 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +import { WebpageListComponent } from './containers/webpage-list.component'; +import { NewWebpageComponent } from './containers/new-webpage.component'; +import { EditWebpageComponent } from './containers/edit-webpage.component'; + +const routes: Routes = [ + { path: '', component: WebpageListComponent }, + { path: 'new-webpage', component: NewWebpageComponent }, + { path: 'edit-webpage/:id', component: EditWebpageComponent } +]; + +/** + * @class + * @classdesc Dataset routing module. + */ +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class WebpageRoutingModule { } + +export const routedComponents = [ + WebpageListComponent, + NewWebpageComponent, + EditWebpageComponent +]; diff --git a/client/src/app/instance/home/home.module.ts b/client/src/app/admin/instance/webpage/webpage.module.ts similarity index 53% rename from client/src/app/instance/home/home.module.ts rename to client/src/app/admin/instance/webpage/webpage.module.ts index 0859f9d1d45bb11bc68c64553fafe1b5961d8ef3..fa6df84f0686a88d147306cdd9fb6ee3752d8f68 100644 --- a/client/src/app/instance/home/home.module.ts +++ b/client/src/app/admin/instance/webpage/webpage.module.ts @@ -9,22 +9,30 @@ import { NgModule } from '@angular/core'; +import { EditorModule, TINYMCE_SCRIPT_SRC } from '@tinymce/tinymce-angular'; + import { SharedModule } from 'src/app/shared/shared.module'; -import { HomeRoutingModule, routedComponents } from './home-routing.module'; +import { WebpageRoutingModule, routedComponents } from './webpage-routing.module'; import { dummiesComponents } from './components'; +import { AdminSharedModule } from '../../admin-shared/admin-shared.module'; /** * @class - * @classdesc Home module. + * @classdesc Dataset module. */ @NgModule({ imports: [ SharedModule, - HomeRoutingModule + WebpageRoutingModule, + AdminSharedModule, + EditorModule ], declarations: [ routedComponents, dummiesComponents + ], + providers: [ + { provide: TINYMCE_SCRIPT_SRC, useValue: 'tinymce/tinymce.min.js' } ] }) -export class HomeModule { } +export class WebpageModule { } diff --git a/client/src/app/instance/components/index.ts b/client/src/app/instance/components/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..74d75b58fafa3e513a25c4bc99b1a732210378b8 --- /dev/null +++ b/client/src/app/instance/components/index.ts @@ -0,0 +1,18 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { InstanceNavbarComponent } from './instance-navbar.component'; +import { WebpageFamilyNavComponent } from './webpage-family-nav.component'; +import { WebpageFamilyNavMobileComponent } from './webpage-family-nav-mobile.component'; + +export const dummiesComponents = [ + InstanceNavbarComponent, + WebpageFamilyNavComponent, + WebpageFamilyNavMobileComponent +]; diff --git a/client/src/app/instance/components/instance-navbar.component.html b/client/src/app/instance/components/instance-navbar.component.html new file mode 100644 index 0000000000000000000000000000000000000000..aa363e918416c11f084d6389b9c79af751c4c6e4 --- /dev/null +++ b/client/src/app/instance/components/instance-navbar.component.html @@ -0,0 +1,147 @@ +<nav class="navbar navbar-light bg-light navbar-expand-md fixed-top border-bottom"> + <!-- Logo --> + <a [routerLink]="getInstanceBaseHref()" class="navbar-brand"> + <img *ngIf="instance.public" [src]="getLogoHref()" alt="Instance logo" /> + <img *ngIf="!instance.public" [src]="getLogoHref() | authImage | async" alt="Instance logo" /> + </a> + + <!-- Navigation --> + <div class="collapse navbar-collapse" id="navbarCollapse"> + <ul class="navbar-nav mr-auto"> + <app-webpage-family-nav + *ngFor="let webpageFamily of webpageFamilyList" + [webpageFamily]="webpageFamily" + [webpageList]="getWebpageListByFamily(webpageFamily)"> + </app-webpage-family-nav> + <li *ngIf="instance.search_by_criteria_allowed" class="nav-item pr-3"> + <a class="nav-link" routerLink="search" routerLinkActive="active"> + <span class="fas fa-search"></span> {{ instance.search_by_criteria_label }} + </a> + </li> + <li *ngIf="instance.search_multiple_allowed" class="nav-item pr-3"> + <a class="nav-link" routerLink="search-multiple" routerLinkActive="active"> + <span class="fas fa-search-plus"></span> {{ instance.search_multiple_label }} + </a> + </li> + <li *ngIf="instance.documentation_allowed" class="nav-item pr-3"> + <a class="nav-link" routerLink="documentation" routerLinkActive="active"> + <span class="fas fa-question"></span> {{ instance.documentation_label }} + </a> + </li> + </ul> + + <ul class="navbar-nav justify-content-end"> + <li class="nav-item pr-3"> + <a *ngIf="instance.back_to_portal" class="nav-link" routerLink="/portal" routerLinkActive="active"> + <span class="fa-solid fa-right-to-bracket"></span> Back to portal + </a> + </li> + <li *ngIf="!authenticationEnabled" class="nav-item pr-3"> + <a class="nav-link" routerLink="/admin" routerLinkActive="active"> + <span class="fas fa-tools"></span> Admin + </a> + </li> + </ul> + <!-- sign in / sign out --> + <button *ngIf="authenticationEnabled && !isAuthenticated" + class="btn btn-outline-success my-2 my-sm-0" + id="button-sign-in" + (click)="login.emit()"> + Sign In / Register + </button> + <span *ngIf="isAuthenticated" id="dropdown-menu" dropdown> + <button id="button-basic" dropdownToggle type="button" class="btn btn-light" aria-controls="dropdown-basic"> + <span class="fa-stack" [ngStyle]="{ color: instance.design_color }"> + <span class="fas fa-circle fa-2x"></span> + <span class="fas fa-user fa-stack-1x fa-inverse"></span> + </span> + + <span class="fas fa-chevron-down text-secondary"></span> + </button> + <ul id="basic-link-dropdown" *dropdownMenu class="dropdown-menu dropdown-menu-right dropdown-up" role="menu" aria-labelledby="basic-link"> + <li id="li-email" role="menuitem"> + <span class="dropdown-item font-italic">{{ userProfile.email }}</span> + </li> + <li class="divider dropdown-divider"></li> + <li role="menuitem"> + <a *ngIf="isAdmin()" class="dropdown-item pointer" routerLink="/admin"> + <span class="fas fa-tools"></span> Admin + </a> + <a class="dropdown-item pointer" (click)="openEditProfile.emit()"> + <span class="fas fa-id-card"></span> Edit profile + </a> + </li> + <li class="divider dropdown-divider"></li> + <li role="menuitem"> + <a class="dropdown-item text-danger pointer" (click)="logout.emit()"> + <span class="fas fa-sign-out-alt fa-fw"></span> Sign Out + </a> + </li> + </ul> + </span> + </div> + + <!-- Navigation Mobile --> + <span dropdown> + <button id="button-basic" dropdownToggle type="button" class="navbar-toggler" aria-controls="dropdown-basic"> + <span class="fas fa-bars"></span> + </button> + <ul id="basic-link-dropdown" *dropdownMenu class="dropdown-menu dropdown-menu-right dropdown-up" role="menu" + aria-labelledby="basic-link"> + <li *ngIf="isAuthenticated" role="menuitem"> + <span class="dropdown-item font-italic">{{ userProfile.email }}</span> + </li> + <li *ngIf="isAuthenticated" class="divider dropdown-divider"></li> + <app-webpage-family-nav-mobile + *ngFor="let webpageFamily of webpageFamilyList" + [webpageFamily]="webpageFamily" + [webpageList]="getWebpageListByFamily(webpageFamily)"> + </app-webpage-family-nav-mobile> + <li *ngIf="instance.search_by_criteria_allowed" role="menuitem"> + <a class="dropdown-item" routerLink="search"> + <span class="fas fa-search fa-fw"></span> {{ instance.search_by_criteria_label }} + </a> + </li> + <li *ngIf="instance.search_multiple_allowed" role="menuitem"> + <a class="dropdown-item" routerLink="search-multiple"> + <span class="fas fa-search-plus fa-fw"></span> {{ instance.search_multiple_label }} + </a> + </li> + <li *ngIf="instance.documentation_allowed" role="menuitem"> + <a class="dropdown-item" routerLink="documentation"> + <span class="fas fa-question fa-fw"></span> {{ instance.documentation_label }} + </a> + </li> + <li role="menuitem"> + <a *ngIf="instance.back_to_portal" class="dropdown-item" routerLink="/portal"> + <span class="fa-solid fa-right-to-bracket fa-fw"></span> Back to portal + </a> + </li> + <li *ngIf="isAuthenticated || !authenticationEnabled" class="divider dropdown-divider"></li> + <li *ngIf="!authenticationEnabled" role="menuitem"> + <a class="dropdown-item pointer" routerLink="/admin"> + <span class="fas fa-tools"></span> Admin + </a> + </li> + <li *ngIf="isAuthenticated" role="menuitem"> + <a *ngIf="isAdmin()" class="dropdown-item pointer" routerLink="/admin"> + <span class="fas fa-tools"></span> Admin + </a> + <a class="dropdown-item pointer" (click)="openEditProfile.emit()"> + <span class="fas fa-id-card"></span> Edit profile + </a> + </li> + <li *ngIf="authenticationEnabled" class="divider dropdown-divider"></li> + <li *ngIf="authenticationEnabled && !isAuthenticated" role="menuitem"> + <a class="dropdown-item pointer text-success" (click)="login.emit()"> + <span class="fas fa-sign-in-alt fa-fw"></span> Sign In / Register + </a> + </li> + <li *ngIf="isAuthenticated" role="menuitem"> + <a class="dropdown-item pointer text-danger" (click)="logout.emit()"> + <span class="fas fa-sign-out-alt fa-fw"></span> Sign Out + </a> + </li> + </ul> + </span> +</nav> diff --git a/client/src/app/instance/home/components/welcome.component.scss b/client/src/app/instance/components/instance-navbar.component.scss similarity index 72% rename from client/src/app/instance/home/components/welcome.component.scss rename to client/src/app/instance/components/instance-navbar.component.scss index 0c0f1a8992260f501b2473c9270dcaa856ca8654..1cc4ce8a2b777fb65944b30cbb3363caf453b52f 100644 --- a/client/src/app/instance/home/components/welcome.component.scss +++ b/client/src/app/instance/components/instance-navbar.component.scss @@ -7,11 +7,11 @@ * file that was distributed with this source code. */ -div.jumbotron p { - line-height: 35px; +.dropdown-up { + top: 80% !important; + right: 5px !important; } -.img-fluid { - height: auto; - max-width: 100%; -} \ No newline at end of file +img { + height: 60px; +} diff --git a/client/src/app/shared/components/navbar.component.ts b/client/src/app/instance/components/instance-navbar.component.ts similarity index 57% rename from client/src/app/shared/components/navbar.component.ts rename to client/src/app/instance/components/instance-navbar.component.ts index 18f9cba7087998a49e403ff6f56ec91ecf13228b..d40451e0321799228ff10fb70514e5ebff4e2996 100644 --- a/client/src/app/shared/components/navbar.component.ts +++ b/client/src/app/instance/components/instance-navbar.component.ts @@ -1,39 +1,26 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ +import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter } from '@angular/core'; -import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; - -import { Instance } from 'src/app/metamodel/models'; +import { Instance, WebpageFamily, Webpage } from 'src/app/metamodel/models'; import { UserProfile } from 'src/app/auth/user-profile.model'; import { isAdmin } from 'src/app/shared/utils'; -/** - * @class - * @classdesc Navbar component. - */ @Component({ - selector: 'app-navbar', - templateUrl: 'navbar.component.html', - styleUrls: [ 'navbar.component.scss' ], + selector: 'app-instance-navbar', + templateUrl: 'instance-navbar.component.html', + styleUrls: [ 'instance-navbar.component.scss' ], changeDetection: ChangeDetectionStrategy.OnPush }) -export class NavbarComponent { - @Input() links: {label: string, icon: string, routerLink: string}[]; +export class InstanceNavbarComponent { @Input() isAuthenticated: boolean; @Input() userProfile: UserProfile = null; @Input() userRoles: string[]; - @Input() baseHref: string; @Input() authenticationEnabled: boolean; @Input() apiUrl: string; @Input() adminRoles: string[]; - @Input() url: string; @Input() instance: Instance; + @Input() webpageFamilyList: WebpageFamily[]; + @Input() webpageList: Webpage[]; + @Input() firstWebpage: Webpage; @Output() login: EventEmitter<any> = new EventEmitter(); @Output() logout: EventEmitter<any> = new EventEmitter(); @Output() openEditProfile: EventEmitter<any> = new EventEmitter(); @@ -47,14 +34,6 @@ export class NavbarComponent { return isAdmin(this.adminRoles, this.userRoles); } - isPortalRoute() { - return this.url.includes('portal'); - } - - isAdminRoute() { - return this.url.includes('admin'); - } - /** * Returns logo href. * @@ -66,4 +45,16 @@ export class NavbarComponent { } return 'assets/cesam_anis40.png'; } + + getInstanceBaseHref() { + if (this.firstWebpage) { + return `/instance/${this.instance.name}/webpage/${this.firstWebpage.id}`; + } else { + return `/instance/${this.instance.name}`; + } + } + + getWebpageListByFamily(webpageFamily) { + return this.webpageList.filter(webpage => webpage.id_webpage_family === webpageFamily.id); + } } diff --git a/client/src/app/instance/components/webpage-family-nav-mobile.component.html b/client/src/app/instance/components/webpage-family-nav-mobile.component.html new file mode 100644 index 0000000000000000000000000000000000000000..d95db1eee897dfe7a5e768753a30122be3fa683f --- /dev/null +++ b/client/src/app/instance/components/webpage-family-nav-mobile.component.html @@ -0,0 +1,17 @@ +<li *ngIf="webpageList.length === 1" role="menuitem"> + <a class="dropdown-item" routerLink="webpage/{{ webpageList[0].id }}"> + <span *ngIf="webpageList[0].icon" [ngClass]="webpageList[0].icon"></span> {{ webpageList[0].label }} + </a> +</li> + +<ng-container *ngIf="webpageList.length > 1"> + <li class="dropdown-item font-weight-bold"> + <span *ngIf="webpageFamily.icon" [ngClass]="webpageFamily.icon"></span> {{ webpageFamily.label }} : + </li> + <li *ngFor="let webpage of webpageList" role="menuitem" class="ml-2"> + <a class="dropdown-item" routerLink="webpage/{{ webpage.id }}" fragment="nested-dropdowns"> + <span *ngIf="webpage.icon" [ngClass]="webpage.icon"></span> {{ webpage.label }} + </a> + </li> + <li class="divider dropdown-divider"></li> +</ng-container> diff --git a/client/src/app/instance/components/webpage-family-nav-mobile.component.ts b/client/src/app/instance/components/webpage-family-nav-mobile.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..e43ce2fd0786eda8889535bfe5e5d7020414d424 --- /dev/null +++ b/client/src/app/instance/components/webpage-family-nav-mobile.component.ts @@ -0,0 +1,12 @@ +import { Component, Input } from '@angular/core'; + +import { WebpageFamily, Webpage } from 'src/app/metamodel/models'; + +@Component({ + selector: 'app-webpage-family-nav-mobile', + templateUrl: 'webpage-family-nav-mobile.component.html' +}) +export class WebpageFamilyNavMobileComponent { + @Input() webpageFamily: WebpageFamily; + @Input() webpageList: Webpage[]; +} diff --git a/client/src/app/instance/components/webpage-family-nav.component.html b/client/src/app/instance/components/webpage-family-nav.component.html new file mode 100644 index 0000000000000000000000000000000000000000..d01574033a559d254ce33721545abcd59cfafa77 --- /dev/null +++ b/client/src/app/instance/components/webpage-family-nav.component.html @@ -0,0 +1,16 @@ +<li *ngIf="webpageList.length === 1" class="nav-item pr-3"> + <a class="nav-link" routerLink="webpage/{{ webpageList[0].id }}" routerLinkActive="active"> + <span *ngIf="webpageList[0].icon" [ngClass]="webpageList[0].icon"></span> {{ webpageList[0].label }} + </a> +</li> + +<li *ngIf="webpageList.length > 1" dropdown class="nav-item dropdown pr-3"> + <a class="nav-link dropdown-toggle" dropdownToggle data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"> + <span *ngIf="webpageFamily.icon" [ngClass]="webpageFamily.icon"></span> {{ webpageFamily.label }} + </a> + <div *dropdownMenu class="dropdown-menu"> + <a *ngFor="let webpage of webpageList" class="dropdown-item" routerLink="webpage/{{ webpage.id }}" routerLinkActive="active"> + <span *ngIf="webpage.icon" [ngClass]="webpage.icon"></span> {{ webpage.label }} + </a> + </div> +</li> diff --git a/client/src/app/instance/components/webpage-family-nav.component.ts b/client/src/app/instance/components/webpage-family-nav.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..c9164d0c1fb2d2fada0809df6de7df718cf4110e --- /dev/null +++ b/client/src/app/instance/components/webpage-family-nav.component.ts @@ -0,0 +1,12 @@ +import { Component, Input } from '@angular/core'; + +import { WebpageFamily, Webpage } from 'src/app/metamodel/models'; + +@Component({ + selector: 'app-webpage-family-nav', + templateUrl: 'webpage-family-nav.component.html' +}) +export class WebpageFamilyNavComponent { + @Input() webpageFamily: WebpageFamily; + @Input() webpageList: Webpage[]; +} diff --git a/client/src/app/instance/home/components/index.ts b/client/src/app/instance/home/components/index.ts deleted file mode 100644 index 8973b374e7bfb09e0ded7e6230d0170a6e266273..0000000000000000000000000000000000000000 --- a/client/src/app/instance/home/components/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { WelcomeComponent } from './welcome.component'; - -export const dummiesComponents = [ - WelcomeComponent -]; diff --git a/client/src/app/instance/home/components/welcome.component.html b/client/src/app/instance/home/components/welcome.component.html deleted file mode 100644 index f59854e39456e2bbbdb7dcaf1fcf902139b706ef..0000000000000000000000000000000000000000 --- a/client/src/app/instance/home/components/welcome.component.html +++ /dev/null @@ -1,7 +0,0 @@ -<div class="row align-items-center jumbotron"> - <div class="col-6 col-md-4 order-md-2 mx-auto text-center"> - <img *ngIf="instance.public" class="img-fluid mb-3 mb-md-0" [src]="getLogoSrc()" alt="Instance logo"> - <img *ngIf="!instance.public" class="img-fluid mb-3 mb-md-0" [src]="getLogoSrc() | authImage | async" alt="Instance logo"> - </div> - <div class="col-md-8 order-md-1 text-justify pr-md-5" [innerHtml]="instance.home_component_config.home_component_text"></div> -</div> diff --git a/client/src/app/instance/home/components/welcome.component.spec.ts b/client/src/app/instance/home/components/welcome.component.spec.ts deleted file mode 100644 index 71b0e6306b44b4b6fdbbdaee2feaf0d98e0a4265..0000000000000000000000000000000000000000 --- a/client/src/app/instance/home/components/welcome.component.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { TestBed, waitForAsync, ComponentFixture } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { WelcomeComponent } from './welcome.component'; - -describe('[Instance][Home][Component] WelcomeComponent', () => { - let component: WelcomeComponent; - let fixture: ComponentFixture<WelcomeComponent>; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [RouterTestingModule], - declarations: [WelcomeComponent] - }).compileComponents(); - fixture = TestBed.createComponent(WelcomeComponent); - component = fixture.componentInstance; - })); - - it('should create the component', () => { - expect(component).toBeDefined(); - }); - - it('#getLogoSrc() should return logo URL address', () => { - component.apiUrl = 'http://test.com'; - component.instance = { - name: 'myInstance', - label: 'My Instance', - description: 'My Instance description', - scientific_manager: 'M. Dupont', - instrument: 'Multiple', - wavelength_domain: 'Visible', - display: 10, - data_path: 'data/path', - files_path: 'files', - public: true, - portal_logo: 'logo.png', - design_color: 'green', - design_background_color: 'darker green', - design_logo: '/path/to/logo', - design_favicon: '/path/to/favicon', - home_component: 'HomeComponent', - home_component_config: { - home_component_text: 'Description', - home_component_logo: '/path/to/logo' - }, - samp_enabled: true, - back_to_portal: true, - search_by_criteria_allowed: true, - search_by_criteria_label: 'Search', - search_multiple_allowed: true, - search_multiple_label: 'Search multiple', - search_multiple_all_datasets_selected: true, - documentation_allowed: true, - documentation_label: 'Documentation', - nb_dataset_families: 1, - nb_datasets: 2 - }; - expect(component.getLogoSrc()).toBe('http://test.com/instance/myInstance/file-explorer/path/to/logo'); - }); -}); diff --git a/client/src/app/instance/home/components/welcome.component.ts b/client/src/app/instance/home/components/welcome.component.ts deleted file mode 100644 index 316ec2b11af2cc919bcd80f868cc89184e7feee4..0000000000000000000000000000000000000000 --- a/client/src/app/instance/home/components/welcome.component.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Component, Input } from '@angular/core'; - -import { Instance } from 'src/app/metamodel/models'; - -/** - * @class - * @classdesc Welcome component. - */ -@Component({ - selector: 'app-welcome', - styleUrls: ['welcome.component.scss'], - templateUrl: 'welcome.component.html' -}) -export class WelcomeComponent { - @Input() instance: Instance; - @Input() apiUrl: string; - - /** - * Returns the logo url. - * - * @return string - */ - getLogoSrc(): string { - return `${this.apiUrl}/instance/${this.instance.name}/file-explorer${this.instance.home_component_config.home_component_logo}`; - } -} diff --git a/client/src/app/instance/home/home.component.html b/client/src/app/instance/home/home.component.html deleted file mode 100644 index 96b9457fbe7b3a811f4c17a53931bf9fe3ab32ce..0000000000000000000000000000000000000000 --- a/client/src/app/instance/home/home.component.html +++ /dev/null @@ -1,3 +0,0 @@ -<div class="container"> - <app-welcome [instance]="instance | async" [apiUrl]="getApiUrl()"></app-welcome> -</div> diff --git a/client/src/app/instance/home/home.component.spec.ts b/client/src/app/instance/home/home.component.spec.ts deleted file mode 100644 index a46d838444b3979df76b2a2286a91e1a7f952a13..0000000000000000000000000000000000000000 --- a/client/src/app/instance/home/home.component.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { TestBed, waitForAsync, ComponentFixture } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { provideMockStore, MockStore } from '@ngrx/store/testing'; - -import { HomeComponent } from './home.component'; -import { AppConfigService } from 'src/app/app-config.service'; -import { Instance } from '../../metamodel/models'; - -describe('[Instance][Home] HomeComponent', () => { - @Component({ selector: '<app-welcome', template: '' }) - class WelcomeStubComponent { - @Input() instance: Instance; - @Input() apiUrl: string; - } - - let component: HomeComponent; - let fixture: ComponentFixture<HomeComponent>; - let store: MockStore; - let appConfigServiceStub = new AppConfigService(); - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [RouterTestingModule], - declarations: [ - HomeComponent, - WelcomeStubComponent - ], - providers: [ - provideMockStore({ }), - { provide: AppConfigService, useValue: appConfigServiceStub }, - ] - }).compileComponents(); - fixture = TestBed.createComponent(HomeComponent); - component = fixture.componentInstance; - store = TestBed.inject(MockStore); - })); - - it('should create the component', () => { - expect(component).toBeDefined(); - }); - - it('#getApiUrl() should return API URL address', () => { - appConfigServiceStub.apiUrl = 'http://test.com'; - expect(component.getApiUrl()).toBe('http://test.com'); - }); -}); diff --git a/client/src/app/instance/home/home.component.ts b/client/src/app/instance/home/home.component.ts deleted file mode 100644 index 48dbbd6fabd40abf96d3e72686d5caf20ae760ec..0000000000000000000000000000000000000000 --- a/client/src/app/instance/home/home.component.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Component } from '@angular/core'; - -import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs'; - -import { Instance } from 'src/app/metamodel/models'; -import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector'; -import { AppConfigService } from 'src/app/app-config.service'; - -/** - * @class - * @classdesc Home component. - */ -@Component({ - selector: 'app-home', - templateUrl: 'home.component.html' -}) -export class HomeComponent { - public instance: Observable<Instance>; - - constructor(private store: Store<{ }>, private config: AppConfigService) { - this.instance = this.store.select(instanceSelector.selectInstanceByRouteName); - } - - /** - * Returns API url. - * - * @return string - */ - getApiUrl(): string { - return this.config.apiUrl; - } -} diff --git a/client/src/app/instance/instance-routing.module.ts b/client/src/app/instance/instance-routing.module.ts index e7679bc2e7366856f410e83c92738806c23b3902..23bb9010329b3d0a78e4d364403291c40e27d181 100644 --- a/client/src/app/instance/instance-routing.module.ts +++ b/client/src/app/instance/instance-routing.module.ts @@ -16,8 +16,7 @@ import { InstanceAuthGuard } from './instance-auth.guard'; const routes: Routes = [ { path: ':iname', component: InstanceComponent, canActivate: [InstanceAuthGuard], children: [ - { path: '', redirectTo: 'home', pathMatch: 'full' }, - { path: 'home', loadChildren: () => import('./home/home.module').then(m => m.HomeModule) }, + { path: 'webpage', loadChildren: () => import('./webpage/webpage.module').then(m => m.WebpageModule) }, { path: 'search', loadChildren: () => import('./search/search.module').then(m => m.SearchModule) }, { path: 'search-multiple', loadChildren: () => import('./search-multiple/search-multiple.module').then(m => m.SearchMultipleModule) }, { path: 'documentation', loadChildren: () => import('./documentation/documentation.module').then(m => m.DocumentationModule) } diff --git a/client/src/app/instance/instance.component.html b/client/src/app/instance/instance.component.html index e31861eec51d537e3f662d6054c19336ddd0c0da..b52a32734bc9f66aadae6bafcd4763b948c39254 100644 --- a/client/src/app/instance/instance.component.html +++ b/client/src/app/instance/instance.component.html @@ -1,20 +1,24 @@ -<header> - <app-navbar - [links]="links" - [isAuthenticated]="isAuthenticated | async" - [userProfile]="userProfile | async" - [userRoles]="userRoles | async" - [baseHref]="getBaseHref()" - [authenticationEnabled]="getAuthenticationEnabled()" - [adminRoles]="getAdminRoles()" - [url]="url | async" - [apiUrl]="getApiUrl()" - [instance]="instance | async" - (login)="login()" - (logout)="logout()" - (openEditProfile)="openEditProfile()"> - </app-navbar> -</header> -<main role="main" class="container-fluid pb-4"> - <router-outlet></router-outlet> -</main> +<app-spinner *ngIf="(webpageFamilyListIsLoading | async) || (webpageListIsLoading | async)"></app-spinner> + +<ng-container *ngIf="(webpageFamilyListIsLoaded | async) && (webpageListIsLoaded | async)"> + <header> + <app-instance-navbar + [isAuthenticated]="isAuthenticated | async" + [userProfile]="userProfile | async" + [userRoles]="userRoles | async" + [authenticationEnabled]="getAuthenticationEnabled()" + [apiUrl]="getApiUrl()" + [adminRoles]="getAdminRoles()" + [instance]="instance | async" + [webpageFamilyList]="webpageFamilyList | async" + [webpageList]="webpageList | async" + [firstWebpage]="firstWebpage | async" + (login)="login()" + (logout)="logout()" + (openEditProfile)="openEditProfile()"> + </app-instance-navbar> + </header> + <main role="main" class="container-fluid pb-4"> + <router-outlet></router-outlet> + </main> +</ng-container> diff --git a/client/src/app/instance/instance.component.spec.ts b/client/src/app/instance/instance.component.spec.ts index c501001a66bf2ae9024dc7a018370f1517957cc9..fc0f236554183fa3f49a5482da6ccf752528ae8c 100644 --- a/client/src/app/instance/instance.component.spec.ts +++ b/client/src/app/instance/instance.component.spec.ts @@ -9,22 +9,27 @@ import { of } from 'rxjs'; import { InstanceComponent } from './instance.component'; import { AppConfigService } from 'src/app/app-config.service'; import * as authActions from 'src/app/auth/auth.actions'; -import { Instance } from '../metamodel/models'; +import { Instance, WebpageFamily, Webpage } from '../metamodel/models'; import { UserProfile } from '../auth/user-profile.model'; import * as datasetFamilyActions from '../metamodel/actions/dataset-family.actions'; import * as datasetActions from '../metamodel/actions/dataset.actions'; import * as datasetGroupActions from 'src/app/metamodel/actions/dataset-group.actions'; +import * as webpageFamilyActions from 'src/app/metamodel/actions/webpage-family.actions'; +import * as webpageActions from 'src/app/metamodel/actions/webpage.actions'; describe('[Instance] InstanceComponent', () => { - @Component({ selector: 'app-navbar', template: '' }) + @Component({ selector: 'app-instance-navbar', template: '' }) class NavbarStubComponent { - @Input() links: {label: string, icon: string, routerLink: string}[]; @Input() isAuthenticated: boolean; @Input() userProfile: UserProfile = null; - @Input() baseHref: string; + @Input() userRoles: string[]; @Input() authenticationEnabled: boolean; @Input() apiUrl: string; + @Input() adminRoles: string[]; @Input() instance: Instance; + @Input() webpageFamilyList: WebpageFamily[] = null; + @Input() webpageList: Webpage[] = null; + @Input() firstWebpage: Webpage = null; } let component: InstanceComponent; @@ -73,11 +78,6 @@ describe('[Instance] InstanceComponent', () => { design_background_color: 'darker green', design_logo: '/path/to/logo', design_favicon: '/path/to/favicon', - home_component: 'HomeComponent', - home_component_config: { - home_component_text: 'Description', - home_component_logo: '/path/to/logo' - }, samp_enabled: true, back_to_portal: true, search_by_criteria_allowed: true, @@ -90,32 +90,34 @@ describe('[Instance] InstanceComponent', () => { nb_dataset_families: 1, nb_datasets: 2 }; + + const webpage: Webpage = { + id: 1, + label: 'Home', + title: 'Home', + display: 10, + icon: 'fas fa-home', + content: '<p>Hello</p>', + id_webpage_family: 1 + } + component.instance = of(instance); + component.firstWebpage = of(webpage); const spy = jest.spyOn(store, 'dispatch'); - const expectedLinks = [ - { label: 'Home', icon: 'fas fa-home', routerLink: 'home' }, - { label: 'Search', icon: 'fas fa-search', routerLink: 'search' }, - { label: 'Search multiple', icon: 'fas fa-search-plus', routerLink: 'search-multiple' }, - { label: 'Documentation', icon: 'fas fa-question', routerLink: 'documentation' } - ]; component.ngOnInit(); Promise.resolve(null).then(function() { - expect(spy).toHaveBeenCalledTimes(3); + expect(spy).toHaveBeenCalledTimes(5); expect(spy).toHaveBeenCalledWith(datasetFamilyActions.loadDatasetFamilyList()); expect(spy).toHaveBeenCalledWith(datasetActions.loadDatasetList()); expect(spy).toHaveBeenCalledWith(datasetGroupActions.loadDatasetGroupList()); - expect(component.links).toEqual(expectedLinks); + expect(spy).toHaveBeenCalledWith(webpageFamilyActions.loadWebpageFamilyList()); + expect(spy).toHaveBeenCalledWith(webpageActions.loadWebpageList()); expect(component.favIcon.href).toEqual('http://localhost/undefined/instance/myInstance/file-explorer/path/to/favicon'); expect(component.title.textContent).toEqual('My Instance'); done(); }); }); - it('#getBaseHref() should return base href config key value', () => { - appConfigServiceStub.baseHref = '/my-project'; - expect(component.getBaseHref()).toBe('/my-project'); - }); - it('#authenticationEnabled() should return authentication enabled config key value', () => { appConfigServiceStub.authenticationEnabled = true; expect(component.getAuthenticationEnabled()).toBeTruthy(); diff --git a/client/src/app/instance/instance.component.ts b/client/src/app/instance/instance.component.ts index dc692bd80d951ea5908d6529174138ef802a78ad..2545b07d695c853de90160a807de8beb334f5223 100644 --- a/client/src/app/instance/instance.component.ts +++ b/client/src/app/instance/instance.component.ts @@ -9,19 +9,23 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { HttpClient } from '@angular/common/http'; +import { ActivatedRoute, Router } from '@angular/router'; import { Store } from '@ngrx/store'; import { Observable, Subscription } from 'rxjs'; -import * as fromRouter from '@ngrx/router-store'; import { UserProfile } from 'src/app/auth/user-profile.model'; -import { Instance } from 'src/app/metamodel/models'; +import { Instance, WebpageFamily, Webpage } from 'src/app/metamodel/models'; import * as authActions from 'src/app/auth/auth.actions'; import * as authSelector from 'src/app/auth/auth.selector'; import * as datasetActions from 'src/app/metamodel/actions/dataset.actions'; import * as datasetFamilyActions from 'src/app/metamodel/actions/dataset-family.actions'; import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector'; import * as datasetGroupActions from 'src/app/metamodel/actions/dataset-group.actions'; +import * as webpageFamilyActions from 'src/app/metamodel/actions/webpage-family.actions'; +import * as webpageFamilySelector from 'src/app/metamodel/selectors/webpage-family.selector'; +import * as webpageActions from 'src/app/metamodel/actions/webpage.actions'; +import * as webpageSelector from 'src/app/metamodel/selectors/webpage.selector'; import { AppConfigService } from 'src/app/app-config.service'; /** @@ -39,22 +43,38 @@ export class InstanceComponent implements OnInit, OnDestroy { public favIcon: HTMLLinkElement = document.querySelector('#favicon'); public title: HTMLLinkElement = document.querySelector('#title'); public body: HTMLBodyElement = document.querySelector('body'); - public links = [ - { label: 'Home', icon: 'fas fa-home', routerLink: 'home' } - ]; public instance: Observable<Instance>; public isAuthenticated: Observable<boolean>; public userProfile: Observable<UserProfile>; public userRoles: Observable<string[]>; - public url: Observable<string>; + public webpageFamilyListIsLoading: Observable<boolean>; + public webpageFamilyListIsLoaded: Observable<boolean>; + public webpageFamilyList: Observable<WebpageFamily[]>; + public webpageListIsLoading: Observable<boolean>; + public webpageListIsLoaded: Observable<boolean>; + public webpageList: Observable<Webpage[]>; + public firstWebpage: Observable<Webpage>; public instanceSubscription: Subscription; + public firstWebpageSubscription: Subscription; - constructor(private store: Store<{ }>, private config: AppConfigService, private http: HttpClient) { + constructor( + private store: Store<{ }>, + private config: AppConfigService, + private http: HttpClient, + private route: ActivatedRoute, + private router: Router + ) { this.instance = store.select(instanceSelector.selectInstanceByRouteName); this.isAuthenticated = store.select(authSelector.selectIsAuthenticated); this.userProfile = store.select(authSelector.selectUserProfile); this.userRoles = store.select(authSelector.selectUserRoles); - this.url = store.select(fromRouter.getSelectors().selectUrl); + this.webpageFamilyListIsLoading = store.select(webpageFamilySelector.selectWebpageFamilyListIsLoading); + this.webpageFamilyListIsLoaded = store.select(webpageFamilySelector.selectWebpageFamilyListIsLoaded); + this.webpageFamilyList = store.select(webpageFamilySelector.selectAllWebpageFamilies); + this.webpageListIsLoading = store.select(webpageSelector.selectWebpageListIsLoading); + this.webpageListIsLoaded = store.select(webpageSelector.selectWebpageListIsLoaded); + this.webpageList = store.select(webpageSelector.selectAllWebpages); + this.firstWebpage = store.select(webpageSelector.selectFirstWebpage); } ngOnInit() { @@ -63,24 +83,22 @@ export class InstanceComponent implements OnInit, OnDestroy { Promise.resolve(null).then(() => this.store.dispatch(datasetFamilyActions.loadDatasetFamilyList())); Promise.resolve(null).then(() => this.store.dispatch(datasetActions.loadDatasetList())); Promise.resolve(null).then(() => this.store.dispatch(datasetGroupActions.loadDatasetGroupList())); + Promise.resolve(null).then(() => this.store.dispatch(webpageFamilyActions.loadWebpageFamilyList())); + Promise.resolve(null).then(() => this.store.dispatch(webpageActions.loadWebpageList())); this.instanceSubscription = this.instance.subscribe(instance => { if (instance) { - if (instance.search_by_criteria_allowed) { - this.links.push({ label: instance.search_by_criteria_label, icon: 'fas fa-search', routerLink: 'search' }); - } - if (instance.search_multiple_allowed) { - this.links.push({ label: instance.search_multiple_label, icon: 'fas fa-search-plus', routerLink: 'search-multiple' }); - } - if (instance.documentation_allowed) { - this.links.push({ label: instance.documentation_label, icon: 'fas fa-question', routerLink: 'documentation' }); - } if (instance.design_favicon !== '') { this.setFaviconHref(instance); } this.title.innerHTML = instance.label; this.body.style.backgroundColor = instance.design_background_color; } - }) + }); + this.firstWebpageSubscription = this.firstWebpage.subscribe(webpage => { + if (webpage && this.router.url === '/instance/default') { + this.router.navigate(['webpage', webpage.id], { relativeTo: this.route }); + } + }); } setFaviconHref(instance: Instance) { @@ -99,15 +117,6 @@ export class InstanceComponent implements OnInit, OnDestroy { } } - /** - * Returns application base href. - * - * @return string - */ - getBaseHref(): string { - return this.config.baseHref; - } - /** * Checks if authentication is enabled. * @@ -161,5 +170,6 @@ export class InstanceComponent implements OnInit, OnDestroy { */ ngOnDestroy() { if (this.instanceSubscription) this.instanceSubscription.unsubscribe(); + if (this.firstWebpageSubscription) this.firstWebpageSubscription.unsubscribe(); } } diff --git a/client/src/app/instance/instance.module.ts b/client/src/app/instance/instance.module.ts index 33a6514300c63f72977df896246e0cf0bc1135a9..a0f28693cc50fe455be948c517f5dae9883f114f 100644 --- a/client/src/app/instance/instance.module.ts +++ b/client/src/app/instance/instance.module.ts @@ -14,6 +14,7 @@ import { EffectsModule } from '@ngrx/effects'; import { SharedModule } from 'src/app/shared/shared.module'; import { InstanceRoutingModule, routedComponents } from './instance-routing.module'; +import { dummiesComponents } from './components'; import { instanceReducer } from './instance.reducer'; import { instanceEffects } from './store/effects'; import { instanceServices } from './store/services'; @@ -30,7 +31,8 @@ import { instanceServices } from './store/services'; EffectsModule.forFeature(instanceEffects) ], declarations: [ - routedComponents + routedComponents, + dummiesComponents ], providers: [ instanceServices diff --git a/client/src/app/instance/search/components/progress-bar.component.spec.ts b/client/src/app/instance/search/components/progress-bar.component.spec.ts index 280fd988a55b71f951dba4e327872e11ce5312cb..bc4b4f02f5e650c833dffcf7a8b008069068551f 100644 --- a/client/src/app/instance/search/components/progress-bar.component.spec.ts +++ b/client/src/app/instance/search/components/progress-bar.component.spec.ts @@ -54,11 +54,6 @@ describe('[Instance][Search][Component] ProgressBarComponent', () => { design_background_color: 'darker green', design_logo: 'path/to/logo', design_favicon: 'path/to/favicon', - home_component: 'HomeComponent', - home_component_config: { - home_component_text: 'Description', - home_component_logo: 'path/to/logo' - }, samp_enabled: true, back_to_portal: true, search_by_criteria_allowed: true, @@ -93,11 +88,6 @@ describe('[Instance][Search][Component] ProgressBarComponent', () => { design_background_color: 'darker green', design_logo: 'path/to/logo', design_favicon: 'path/to/favicon', - home_component: 'HomeComponent', - home_component_config: { - home_component_text: 'Description', - home_component_logo: 'path/to/logo' - }, samp_enabled: true, back_to_portal: true, search_by_criteria_allowed: true, diff --git a/client/src/app/instance/webpage/components/index.ts b/client/src/app/instance/webpage/components/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..28c122e3ead6c199771514aeb447141d7880c7a0 --- /dev/null +++ b/client/src/app/instance/webpage/components/index.ts @@ -0,0 +1,5 @@ +import { WebpageComponent } from './webpage-content.component'; + +export const dummiesComponents = [ + WebpageComponent +]; diff --git a/client/src/app/instance/webpage/components/webpage-content.component.html b/client/src/app/instance/webpage/components/webpage-content.component.html new file mode 100644 index 0000000000000000000000000000000000000000..99c5e9a4f9b14ba3431047af7a8c746abf86c8b0 --- /dev/null +++ b/client/src/app/instance/webpage/components/webpage-content.component.html @@ -0,0 +1 @@ +<ngx-dynamic-hooks [content]="webpage.content"></ngx-dynamic-hooks> \ No newline at end of file diff --git a/client/src/app/instance/webpage/components/webpage-content.component.ts b/client/src/app/instance/webpage/components/webpage-content.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..a8bfd9bbc62020da36e27f1b93e00eb397f52045 --- /dev/null +++ b/client/src/app/instance/webpage/components/webpage-content.component.ts @@ -0,0 +1,24 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Component, Input } from '@angular/core'; + +import { Webpage } from 'src/app/metamodel/models'; + +/** + * @class + * @classdesc Webpage content component. + */ +@Component({ + selector: 'app-webpage-content', + templateUrl: 'webpage-content.component.html' +}) +export class WebpageComponent { + @Input() webpage: Webpage; +} diff --git a/client/src/app/instance/webpage/containers/webpage.component.html b/client/src/app/instance/webpage/containers/webpage.component.html new file mode 100644 index 0000000000000000000000000000000000000000..e6c9db3b20bcefbf455c1f2194083840f3bcb3c1 --- /dev/null +++ b/client/src/app/instance/webpage/containers/webpage.component.html @@ -0,0 +1,6 @@ +<div class="container"> + <app-spinner *ngIf="webpageListIsLoading | async"></app-spinner> + + <app-webpage-content *ngIf="webpageListIsLoaded | async" [webpage]="webpage | async"> + </app-webpage-content> +</div> diff --git a/client/src/app/instance/webpage/containers/webpage.component.ts b/client/src/app/instance/webpage/containers/webpage.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..b1d9d4b11572b83a4f6dd85d9420f13a67f8590e --- /dev/null +++ b/client/src/app/instance/webpage/containers/webpage.component.ts @@ -0,0 +1,37 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Component } from '@angular/core'; + +import { Observable } from 'rxjs'; +import { Store } from '@ngrx/store'; + +import { Webpage } from 'src/app/metamodel/models'; +import * as webpageSelector from 'src/app/metamodel/selectors/webpage.selector'; + +/** + * @class + * @classdesc Webpage component. + */ +@Component({ + selector: 'app-webpage', + templateUrl: 'webpage.component.html' +}) +export class WebpageComponent { + public title: HTMLLinkElement = document.querySelector('#title'); + public webpageListIsLoading: Observable<boolean>; + public webpageListIsLoaded: Observable<boolean>; + public webpage: Observable<Webpage>; + + constructor(private store: Store<{ }>) { + this.webpageListIsLoading = store.select(webpageSelector.selectWebpageListIsLoading); + this.webpageListIsLoaded = store.select(webpageSelector.selectWebpageListIsLoaded); + this.webpage = this.store.select(webpageSelector.selectWebpageByRouteId); + } +} diff --git a/client/src/app/instance/webpage/hooks/components/dynamic-router-link.component.html b/client/src/app/instance/webpage/hooks/components/dynamic-router-link.component.html new file mode 100644 index 0000000000000000000000000000000000000000..2f43ac7ec15f1e5f671dbca64512d719766158a9 --- /dev/null +++ b/client/src/app/instance/webpage/hooks/components/dynamic-router-link.component.html @@ -0,0 +1,18 @@ +<a *ngIf="isExternalLink()" + [href]="link" + [ngClass]="css" + [target]="target ? target : '_self'" +> + <ng-container *ngTemplateOutlet="contentTpl"></ng-container> +</a> + +<a *ngIf="!isExternalLink()" + [routerLink]="link" + [queryParams]="queryParams ? queryParams : {}" + [fragment]="anchorFragment ? anchorFragment : null" + [ngClass]="css" +> + <ng-container *ngTemplateOutlet="contentTpl"></ng-container> +</a> + +<ng-template #contentTpl><ng-content></ng-content></ng-template> diff --git a/client/src/app/instance/webpage/hooks/components/dynamic-router-link.component.scss b/client/src/app/instance/webpage/hooks/components/dynamic-router-link.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..1f0440ee0e66b2f7ee73998e14e352af81a61627 --- /dev/null +++ b/client/src/app/instance/webpage/hooks/components/dynamic-router-link.component.scss @@ -0,0 +1,3 @@ +:host { + display: inline; +} \ No newline at end of file diff --git a/client/src/app/instance/webpage/hooks/components/dynamic-router-link.component.ts b/client/src/app/instance/webpage/hooks/components/dynamic-router-link.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..d0e20e09d5bd91c394084aba899ed450e398f04f --- /dev/null +++ b/client/src/app/instance/webpage/hooks/components/dynamic-router-link.component.ts @@ -0,0 +1,18 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-dynamic-router-link', + templateUrl: 'dynamic-router-link.component.html', + styleUrls: [ 'dynamic-router-link.component.scss' ] +}) +export class DynamicRouterLinkComponent { + @Input() link: string; + @Input() queryParams: {[key: string]: any}; + @Input() anchorFragment: string; + @Input() css: string; + @Input() target: string; + + isExternalLink() { + return this.link.startsWith('http'); + } +} diff --git a/client/src/app/instance/webpage/hooks/components/index.ts b/client/src/app/instance/webpage/hooks/components/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..eb7f2756fb32360196bbf440fc5d4e80fc0475be --- /dev/null +++ b/client/src/app/instance/webpage/hooks/components/index.ts @@ -0,0 +1,5 @@ +import { DynamicRouterLinkComponent } from './dynamic-router-link.component'; + +export const hooksComponents = [ + DynamicRouterLinkComponent +]; diff --git a/client/src/app/instance/webpage/hooks/parsers/dynamic-router-link-parser.ts b/client/src/app/instance/webpage/hooks/parsers/dynamic-router-link-parser.ts new file mode 100644 index 0000000000000000000000000000000000000000..cc0242d8bc7e37e7c4abfb9fe4029e5c489db115 --- /dev/null +++ b/client/src/app/instance/webpage/hooks/parsers/dynamic-router-link-parser.ts @@ -0,0 +1,96 @@ +import { Injectable } from '@angular/core'; +import { HookParser, HookPosition, HookValue, HookComponentData, HookBindings, HookFinder } from 'ngx-dynamic-hooks'; + +import { DynamicRouterLinkComponent } from '../components/dynamic-router-link.component'; + +@Injectable() +export class DynamicRouterLinkParser implements HookParser { + linkOpeningTagRegex: RegExp; + linkClosingTagRegex: RegExp; + hrefAttrRegex: RegExp; + classAttrRegex: RegExp; + targetAttrRegex: RegExp; + + constructor(private hookFinder: HookFinder) { + const hrefAttr = '\\s+href\=\\"([^\\"]*?)\\"'; + const anyOtherAttr = '\\s+[a-zA-Z]+\\=\\"[^\\"]*?\\"'; + const linkOpeningTag = '\\<a(?:' + anyOtherAttr + ')*?' + hrefAttr + '(?:' + anyOtherAttr + ')*?\\>'; + + // Transform into proper regex objects and save for later + this.linkOpeningTagRegex = new RegExp(linkOpeningTag, 'gim'); + this.linkClosingTagRegex = new RegExp('<\\/a>', 'gim'); + this.hrefAttrRegex = new RegExp(hrefAttr, 'im'); + this.classAttrRegex = new RegExp('\\s+class\=\\"([^\\"]*?)\\"', 'im'); + this.targetAttrRegex = new RegExp('\\s+target\=\\"([^\\"]*?)\\"', 'im') + } + + public findHooks(content: string, context: any): Array<HookPosition> { + // With the regexes we prepared, we can simply use findEnclosingHooks() to retrieve + // the HookPositions of all internal <a>-elements from the content string + return this.hookFinder.findEnclosingHooks(content, this.linkOpeningTagRegex, this.linkClosingTagRegex); + } + + public loadComponent(hookId: number, hookValue: HookValue, context: any, childNodes: Array<Element>): HookComponentData { + // Simply return the component class here + return { + component: DynamicRouterLinkComponent + }; + } + + public getBindings(hookId: number, hookValue: HookValue, context: any): HookBindings { + // We can reuse the hrefAttrRegex here as its first capture group is the relative part of the url, + // e.g. '/jedi/windu' from 'https://www.mysite.com/jedi/windu', which is what we need + const hrefAttrMatch = hookValue.openingTag.match(this.hrefAttrRegex); + let link = hrefAttrMatch[1]; + + // The relative part of the link may still contain the query string and the + // anchor fragment, so we need to split it up accordingly + const anchorFragmentSplit = link.split('#'); + link = anchorFragmentSplit[0]; + const anchorFragment = anchorFragmentSplit.length > 1 ? anchorFragmentSplit[1] : null; + + const queryParamsSplit = link.split('?'); + link = queryParamsSplit[0]; + const queryParams = queryParamsSplit.length > 1 ? this.parseQueryString(queryParamsSplit[1]) : {}; + + // Select css part + let css = null; + const classAttrMatch = hookValue.openingTag.match(this.classAttrRegex); + if (classAttrMatch) { + css = classAttrMatch[1]; + } + + // Select target part + let target = null; + const targetAttrMatch = hookValue.openingTag.match(this.targetAttrRegex); + if (targetAttrMatch) { + target = targetAttrMatch[1]; + } + + // Give all of these to our DynamicRouterLinkComponent as inputs and we're done! + return { + inputs: { + link, + queryParams: queryParams, + anchorFragment: anchorFragment, + css, + target + } + }; + } + + /** + * A helper function that transforms a query string into a QueryParams object + * Approach by Wolfgang Kuehn @ https://stackoverflow.com/a/8649003/3099523 + * + * @param queryParamString - The queryString to parse + */ + private parseQueryString(queryParamString: string): {[key: string]: any} { + return JSON.parse('{"' + + decodeURI(queryParamString) + .replace(/"/g, '\\"') + .replace(/&/g, '","') + .replace(/=/g, '":"') + + '"}'); + } +} diff --git a/client/src/app/instance/webpage/hooks/parsers/index.ts b/client/src/app/instance/webpage/hooks/parsers/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..3dee9d17e82b7d85ef1a3566977e5212df747c17 --- /dev/null +++ b/client/src/app/instance/webpage/hooks/parsers/index.ts @@ -0,0 +1,7 @@ +import { HookParserEntry } from 'ngx-dynamic-hooks'; + +import { DynamicRouterLinkParser } from './dynamic-router-link-parser'; + +export const componentParsers: Array<HookParserEntry> = [ + DynamicRouterLinkParser +]; diff --git a/client/src/app/instance/home/home-routing.module.ts b/client/src/app/instance/webpage/webpage-routing.module.ts similarity index 70% rename from client/src/app/instance/home/home-routing.module.ts rename to client/src/app/instance/webpage/webpage-routing.module.ts index 539509e87ab7eebe64a7a34e7395cb62b198c17e..8350c2e16be056af67fef13f0b4d4defcfa30ce4 100644 --- a/client/src/app/instance/home/home-routing.module.ts +++ b/client/src/app/instance/webpage/webpage-routing.module.ts @@ -10,22 +10,22 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; -import { HomeComponent } from './home.component'; +import { WebpageComponent } from './containers/webpage.component'; const routes: Routes = [ - { path: '', component: HomeComponent } + { path: ':id', component: WebpageComponent } ]; /** * @class - * @classdesc Home routing module. + * @classdesc Webpage routing module. */ @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) -export class HomeRoutingModule { } +export class WebpageRoutingModule { } export const routedComponents = [ - HomeComponent + WebpageComponent ]; diff --git a/client/src/app/instance/webpage/webpage.module.ts b/client/src/app/instance/webpage/webpage.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..8a57c1e92b00f6dd4062a86fd7e422521c3abeea --- /dev/null +++ b/client/src/app/instance/webpage/webpage.module.ts @@ -0,0 +1,44 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { NgModule } from '@angular/core'; + +import { DynamicHooksModule } from 'ngx-dynamic-hooks'; + +import { SharedModule } from 'src/app/shared/shared.module'; +import { WebpageRoutingModule, routedComponents } from './webpage-routing.module'; +import { dummiesComponents } from './components'; +import { componentParsers } from './hooks/parsers'; +import { hooksComponents } from './hooks/components'; + +/** + * @class + * @classdesc Webpage module. + */ +@NgModule({ + imports: [ + SharedModule, + WebpageRoutingModule, + DynamicHooksModule.forRoot({ + globalParsers: componentParsers + }), + ], + declarations: [ + routedComponents, + dummiesComponents, + hooksComponents + ], + providers: [ + componentParsers + ], + entryComponents: [ + hooksComponents + ] +}) +export class WebpageModule { } diff --git a/client/src/app/metamodel/actions/webpage-family.actions.ts b/client/src/app/metamodel/actions/webpage-family.actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..796e33c5c0b9547567c8f3dcddc93bc866e6308b --- /dev/null +++ b/client/src/app/metamodel/actions/webpage-family.actions.ts @@ -0,0 +1,25 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createAction, props } from '@ngrx/store'; + +import { WebpageFamily } from '../models'; + +export const loadWebpageFamilyList = createAction('[Metamodel] Load Webpage Family List'); +export const loadWebpageFamilyListSuccess = createAction('[Metamodel] Load Webpage Family List Success', props<{ webpageFamilies: WebpageFamily[] }>()); +export const loadWebpageFamilyListFail = createAction('[Metamodel] Load Webpage Family List Fail'); +export const addWebpageFamily = createAction('[Metamodel] Add Webpage Family', props<{ webpageFamily: WebpageFamily }>()); +export const addWebpageFamilySuccess = createAction('[Metamodel] Add Webpage Family Success', props<{ webpageFamily: WebpageFamily }>()); +export const addWebpageFamilyFail = createAction('[Metamodel] Add Webpage Family Fail'); +export const editWebpageFamily = createAction('[Metamodel] Edit Webpage Family', props<{ webpageFamily: WebpageFamily }>()); +export const editWebpageFamilySuccess = createAction('[Metamodel] Edit Webpage Family Success', props<{ webpageFamily: WebpageFamily }>()); +export const editWebpageFamilyFail = createAction('[Metamodel] Edit Webpage Family Fail'); +export const deleteWebpageFamily = createAction('[Metamodel] Delete Webpage Family', props<{ webpageFamily: WebpageFamily }>()); +export const deleteWebpageFamilySuccess = createAction('[Metamodel] Delete Webpage Family Success', props<{ webpageFamily: WebpageFamily }>()); +export const deleteWebpageFamilyFail = createAction('[Metamodel] Delete Webpage Family Fail'); diff --git a/client/src/app/metamodel/actions/webpage.actions.ts b/client/src/app/metamodel/actions/webpage.actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..9e068d01b1e625fb99ae8838d564d75aeabed95e --- /dev/null +++ b/client/src/app/metamodel/actions/webpage.actions.ts @@ -0,0 +1,25 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createAction, props } from '@ngrx/store'; + +import { Webpage } from '../models'; + +export const loadWebpageList = createAction('[Metamodel] Load Webpage List'); +export const loadWebpageListSuccess = createAction('[Metamodel] Load Webpage List Success', props<{ webpages: Webpage[] }>()); +export const loadWebpageListFail = createAction('[Metamodel] Load Webpage List Fail'); +export const addWebpage = createAction('[Metamodel] Add Webpage', props<{ webpage: Webpage }>()); +export const addWebpageSuccess = createAction('[Metamodel] Add Webpage Success', props<{ webpage: Webpage }>()); +export const addWebpageFail = createAction('[Metamodel] Add Webpage Fail'); +export const editWebpage = createAction('[Metamodel] Edit Webpage', props<{ webpage: Webpage }>()); +export const editWebpageSuccess = createAction('[Metamodel] Edit Webpage Success', props<{ webpage: Webpage }>()); +export const editWebpageFail = createAction('[Metamodel] Edit Webpage Fail'); +export const deleteWebpage = createAction('[Metamodel] Delete Webpage', props<{ webpage: Webpage }>()); +export const deleteWebpageSuccess = createAction('[Metamodel] Delete Webpage Success', props<{ webpage: Webpage }>()); +export const deleteWebpageFail = createAction('[Metamodel] Delete Webpage Fail'); diff --git a/client/src/app/metamodel/effects/index.ts b/client/src/app/metamodel/effects/index.ts index 111bd9f732e401b4dc337afedbc0a66adfb68728..9f2e2fc1b4e1f176e4ee57b683a368506214a3ac 100644 --- a/client/src/app/metamodel/effects/index.ts +++ b/client/src/app/metamodel/effects/index.ts @@ -20,6 +20,8 @@ import { OutputFamilyEffects } from './output-family.effects'; import { ImageEffects } from './image.effects'; import { FileEffects } from './file.effects'; import { ConeSearchConfigEffects } from './cone-search-config.effects' +import { WebpageFamilyEffects } from './webpage-family.effects'; +import { WebpageEffects } from './webpage.effects'; export const metamodelEffects = [ DatabaseEffects, @@ -34,5 +36,7 @@ export const metamodelEffects = [ OutputFamilyEffects, ImageEffects, FileEffects, - ConeSearchConfigEffects + ConeSearchConfigEffects, + WebpageFamilyEffects, + WebpageEffects ]; diff --git a/client/src/app/metamodel/effects/webpage-family.effects.ts b/client/src/app/metamodel/effects/webpage-family.effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..75732948b2e296c11fdfbe03fb52f6e507ebf86b --- /dev/null +++ b/client/src/app/metamodel/effects/webpage-family.effects.ts @@ -0,0 +1,162 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Injectable } from '@angular/core'; + +import { Actions, createEffect, ofType, concatLatestFrom } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { of } from 'rxjs'; +import { map, tap, mergeMap, catchError } from 'rxjs/operators'; +import { ToastrService } from 'ngx-toastr'; + +import * as webpageFamilyActions from '../actions/webpage-family.actions'; +import { WebpageFamilyService } from '../services/webpage-family.service'; +import * as instanceSelector from '../selectors/instance.selector'; + +/** + * @class + * @classdesc Webpage family effects. + */ +@Injectable() +export class WebpageFamilyEffects { + /** + * Calls action to retrieve webpage family list. + */ + loadWebpageFamilies$ = createEffect((): any => + this.actions$.pipe( + ofType(webpageFamilyActions.loadWebpageFamilyList), + concatLatestFrom(() => this.store.select(instanceSelector.selectInstanceNameByRoute)), + mergeMap(([, instanceName]) => this.webpageFamilyService.retrieveWebpageFamilyList(instanceName) + .pipe( + map(webpageFamilies => webpageFamilyActions.loadWebpageFamilyListSuccess({ webpageFamilies })), + catchError(() => of(webpageFamilyActions.loadWebpageFamilyListFail())) + ) + ) + ) + ); + + /** + * Calls action to add a webpage family. + */ + addWebpageFamily$ = createEffect((): any => + this.actions$.pipe( + ofType(webpageFamilyActions.addWebpageFamily), + concatLatestFrom(() => this.store.select(instanceSelector.selectInstanceNameByRoute)), + mergeMap(([action, instanceName]) => this.webpageFamilyService.addWebpageFamily(instanceName, action.webpageFamily) + .pipe( + map(webpageFamily => webpageFamilyActions.addWebpageFamilySuccess({ webpageFamily })), + catchError(() => of(webpageFamilyActions.addWebpageFamilyFail())) + ) + ) + ) + ); + + /** + * Displays add webpage family success notification. + */ + addWebpageFamilySuccess$ = createEffect(() => + this.actions$.pipe( + ofType(webpageFamilyActions.addWebpageFamilySuccess), + concatLatestFrom(() => this.store.select(instanceSelector.selectInstanceNameByRoute)), + tap(([, instanceName]) => { + this.toastr.success('Webpage family successfully added', 'The new webpage family was added into the database') + }) + ), { dispatch: false } + ); + + /** + * Displays add webpage family fail notification. + */ + addWebpageFamilyFail$ = createEffect(() => + this.actions$.pipe( + ofType(webpageFamilyActions.addWebpageFamilyFail), + tap(() => this.toastr.error('Failure to add webpage family', 'The new webpage family could not be added into the database')) + ), { dispatch: false } + ); + + /** + * Calls action to modify a webpage family. + */ + editWebpageFamily$ = createEffect((): any => + this.actions$.pipe( + ofType(webpageFamilyActions.editWebpageFamily), + mergeMap(action => this.webpageFamilyService.editWebpageFamily(action.webpageFamily) + .pipe( + map(webpageFamily => webpageFamilyActions.editWebpageFamilySuccess({ webpageFamily })), + catchError(() => of(webpageFamilyActions.editWebpageFamilyFail())) + ) + ) + ) + ); + + /** + * Displays edit webpage family success notification. + */ + editWebpageFamilySuccess$ = createEffect(() => + this.actions$.pipe( + ofType(webpageFamilyActions.editWebpageFamilySuccess), + concatLatestFrom(() => this.store.select(instanceSelector.selectInstanceNameByRoute)), + tap(([action, instanceName]) => { + this.toastr.success('Webpage family successfully edited', 'The existing webpage family has been edited into the database') + }) + ), { dispatch: false } + ); + + /** + * Displays edit webpage family error notification. + */ + editWebpageFamilyFail$ = createEffect(() => + this.actions$.pipe( + ofType(webpageFamilyActions.editWebpageFamilyFail), + tap(() => this.toastr.error('Failure to edit webpage family', 'The existing webpage family could not be edited into the database')) + ), { dispatch: false } + ); + + /** + * Calls action to remove a webpage family. + */ + deleteWebpageFamily$ = createEffect((): any => + this.actions$.pipe( + ofType(webpageFamilyActions.deleteWebpageFamily), + mergeMap(action => this.webpageFamilyService.deleteWebpageFamily(action.webpageFamily.id) + .pipe( + map(() => webpageFamilyActions.deleteWebpageFamilySuccess({ webpageFamily: action.webpageFamily })), + catchError(() => of(webpageFamilyActions.deleteWebpageFamilyFail())) + ) + ) + ) + ); + + /** + * Displays delete webpage family success notification. + */ + deleteWebpageFamilySuccess$ = createEffect(() => + this.actions$.pipe( + ofType(webpageFamilyActions.deleteWebpageFamilySuccess), + tap(() => this.toastr.success('Webpage family successfully deleted', 'The existing webpage family has been deleted')) + ), { dispatch: false } + ); + + /** + * Displays delete webpage family error notification. + */ + deleteWebpageFamilyFail$ = createEffect(() => + this.actions$.pipe( + ofType(webpageFamilyActions.deleteWebpageFamilyFail), + tap(() => this.toastr.error('Failure to delete webpage family', 'The existing webpage family could not be deleted from the database')) + ), { dispatch: false } + ); + + constructor( + private actions$: Actions, + private webpageFamilyService: WebpageFamilyService, + private toastr: ToastrService, + private store: Store<{ }> + ) {} +} diff --git a/client/src/app/metamodel/effects/webpage.effects.ts b/client/src/app/metamodel/effects/webpage.effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..d45f9fbb29e9e3c3dd6d878aaa34f96e12054589 --- /dev/null +++ b/client/src/app/metamodel/effects/webpage.effects.ts @@ -0,0 +1,165 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; + +import { Actions, createEffect, ofType, concatLatestFrom } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { of } from 'rxjs'; +import { map, tap, mergeMap, catchError } from 'rxjs/operators'; +import { ToastrService } from 'ngx-toastr'; + +import * as webpageActions from '../actions/webpage.actions'; +import { WebpageService } from '../services/webpage.service'; +import * as instanceSelector from '../selectors/instance.selector'; + +/** + * @class + * @classdesc Webpage effects. + */ +@Injectable() +export class WebpageEffects { + /** + * Calls action to retrieve webpage list. + */ + loadWebpages$ = createEffect((): any => + this.actions$.pipe( + ofType(webpageActions.loadWebpageList), + concatLatestFrom(() => this.store.select(instanceSelector.selectInstanceNameByRoute)), + mergeMap(([, instanceName]) => this.webpageService.retrieveWebpageList(instanceName) + .pipe( + map(webpages => webpageActions.loadWebpageListSuccess({ webpages })), + catchError(() => of(webpageActions.loadWebpageListFail())) + ) + ) + ) + ); + + /** + * Calls action to add a webpage. + */ + addWebpage$ = createEffect((): any => + this.actions$.pipe( + ofType(webpageActions.addWebpage), + mergeMap(action => this.webpageService.addWebpage(action.webpage) + .pipe( + map(webpage => webpageActions.addWebpageSuccess({ webpage })), + catchError(() => of(webpageActions.addWebpageFail())) + ) + ) + ) + ); + + /** + * Displays add webpage success notification. + */ + addWebpageSuccess$ = createEffect(() => + this.actions$.pipe( + ofType(webpageActions.addWebpageSuccess), + concatLatestFrom(() => this.store.select(instanceSelector.selectInstanceNameByRoute)), + tap(([, instanceName]) => { + this.router.navigateByUrl(`/admin/instance/configure-instance/${instanceName}/webpage`); + this.toastr.success('Webpage successfully added', 'The new webpage was added into the database'); + }) + ), { dispatch: false } + ); + + /** + * Displays add webpage fail notification. + */ + addWebpageFail$ = createEffect(() => + this.actions$.pipe( + ofType(webpageActions.addWebpageFail), + tap(() => this.toastr.error('Failure to add webpage', 'The new webpage could not be added into the database')) + ), { dispatch: false } + ); + + /** + * Calls action to modify a webpage. + */ + editWebpage$ = createEffect((): any => + this.actions$.pipe( + ofType(webpageActions.editWebpage), + mergeMap(action => this.webpageService.editWebpage(action.webpage) + .pipe( + map(webpage => webpageActions.editWebpageSuccess({ webpage })), + catchError(() => of(webpageActions.editWebpageFail())) + ) + ) + ) + ); + + /** + * Displays edit webpage success notification. + */ + editWebpageSuccess$ = createEffect(() => + this.actions$.pipe( + ofType(webpageActions.editWebpageSuccess), + concatLatestFrom(() => this.store.select(instanceSelector.selectInstanceNameByRoute)), + tap(([, instanceName]) => { + this.router.navigateByUrl(`/admin/instance/configure-instance/${instanceName}/webpage`); + this.toastr.success('Webpage successfully edited', 'The existing webpage has been edited into the database') + }) + ), { dispatch: false } + ); + + /** + * Displays edit webpage error notification. + */ + editWebpageFail$ = createEffect(() => + this.actions$.pipe( + ofType(webpageActions.editWebpageFail), + tap(() => this.toastr.error('Failure to edit webpage', 'The existing webpage could not be edited into the database')) + ), { dispatch: false } + ); + + /** + * Calls action to remove a webpage. + */ + deleteWebpage$ = createEffect((): any => + this.actions$.pipe( + ofType(webpageActions.deleteWebpage), + mergeMap(action => this.webpageService.deleteWebpage(action.webpage.id) + .pipe( + map(() => webpageActions.deleteWebpageSuccess({ webpage: action.webpage })), + catchError(() => of(webpageActions.deleteWebpageFail())) + ) + ) + ) + ); + + /** + * Displays delete webpage success notification. + */ + deleteWebpageSuccess$ = createEffect(() => + this.actions$.pipe( + ofType(webpageActions.deleteWebpageSuccess), + tap(() => this.toastr.success('Webpage successfully deleted', 'The existing webpage has been deleted')) + ), { dispatch: false } + ); + + /** + * Displays delete webpage error notification. + */ + deleteWebpageFail$ = createEffect(() => + this.actions$.pipe( + ofType(webpageActions.deleteWebpageFail), + tap(() => this.toastr.error('Failure to delete webpage', 'The existing webpage could not be deleted from the database')) + ), { dispatch: false } + ); + + constructor( + private actions$: Actions, + private webpageService: WebpageService, + private router: Router, + private toastr: ToastrService, + private store: Store<{ }> + ) {} +} diff --git a/client/src/app/metamodel/metamodel.reducer.ts b/client/src/app/metamodel/metamodel.reducer.ts index 2cccdfc3787cfaac8e61e759cd38e77e93f7daa0..017bb208bf52fad13a7d7d9ffef312fd6bdd2e3b 100644 --- a/client/src/app/metamodel/metamodel.reducer.ts +++ b/client/src/app/metamodel/metamodel.reducer.ts @@ -23,6 +23,8 @@ import * as outputFamily from './reducers/output-family.reducer'; import * as image from './reducers/image.reducer'; import * as file from './reducers/file.reducer'; import * as coneSearchConfig from './reducers/cone-search-config.reducer'; +import * as webpageFamily from './reducers/webpage-family.reducer'; +import * as webpage from './reducers/webpage.reducer'; /** * Interface for metamodel state. @@ -43,6 +45,8 @@ export interface State { image: image.State; file: file.State; coneSearchConfig: coneSearchConfig.State; + webpageFamily: webpageFamily.State; + webpage: webpage.State; } const reducers = { @@ -58,7 +62,9 @@ const reducers = { outputFamily: outputFamily.outputFamilyReducer, image: image.imageReducer, file: file.fileReducer, - coneSearchConfig: coneSearchConfig.coneSearchConfigReducer + coneSearchConfig: coneSearchConfig.coneSearchConfigReducer, + webpageFamily: webpageFamily.webpageFamilyReducer, + webpage: webpage.webpageReducer }; export const metamodelReducer = combineReducers(reducers); diff --git a/client/src/app/metamodel/models/index.ts b/client/src/app/metamodel/models/index.ts index 82f01695472cea1a22424bf1a7465fdcd6f168b5..976dea69f42d52915527e4df612fd2a076f406e0 100644 --- a/client/src/app/metamodel/models/index.ts +++ b/client/src/app/metamodel/models/index.ts @@ -22,4 +22,6 @@ export * from './image.model'; export * from './renderers'; export * from './detail-renderers'; export * from './cone-search-config.model'; -export * from './file.model'; \ No newline at end of file +export * from './file.model'; +export * from './webpage.model'; +export * from './webpage-family'; diff --git a/client/src/app/metamodel/models/instance.model.ts b/client/src/app/metamodel/models/instance.model.ts index 6294572235d3f6826cf09f9fbd4b28ed04e3fd91..9a467958d036e56fa5d07560da2a155bd36546b9 100644 --- a/client/src/app/metamodel/models/instance.model.ts +++ b/client/src/app/metamodel/models/instance.model.ts @@ -28,11 +28,6 @@ export interface Instance { design_background_color: string; design_logo: string; design_favicon: string; - home_component: string; - home_component_config: { - home_component_text: string; - home_component_logo: string; - }; samp_enabled: boolean; back_to_portal: boolean; search_by_criteria_allowed: boolean; diff --git a/client/src/app/metamodel/models/webpage-family.ts b/client/src/app/metamodel/models/webpage-family.ts new file mode 100644 index 0000000000000000000000000000000000000000..0c85583ee497cc961bd6fb59647839851a39555e --- /dev/null +++ b/client/src/app/metamodel/models/webpage-family.ts @@ -0,0 +1,20 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Interface for webpage family. + * + * @interface WebpageFamily + */ + export interface WebpageFamily { + id: number; + label: string; + icon: string; + display: number; +} diff --git a/client/src/app/metamodel/models/webpage.model.ts b/client/src/app/metamodel/models/webpage.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..7f37e6d5ad87de24f8870fd6c1f10e08db729545 --- /dev/null +++ b/client/src/app/metamodel/models/webpage.model.ts @@ -0,0 +1,23 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Interface for web page. + * + * @interface Webpage + */ +export interface Webpage { + id: number; + label: string; + icon: string; + display: number; + title: string; + content: string; + id_webpage_family: number; +} diff --git a/client/src/app/metamodel/reducers/webpage-family.reducer.ts b/client/src/app/metamodel/reducers/webpage-family.reducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..8f4ba68c5cb4a17601d3e4019c129ba2c21c848a --- /dev/null +++ b/client/src/app/metamodel/reducers/webpage-family.reducer.ts @@ -0,0 +1,84 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createReducer, on } from '@ngrx/store'; +import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; + +import { WebpageFamily } from '../models'; +import * as webpageFamilyActions from '../actions/webpage-family.actions'; + +/** + * Interface for webpage family state. + * + * @interface State + */ +export interface State extends EntityState<WebpageFamily> { + webpageFamilyListIsLoading: boolean; + webpageFamilyListIsLoaded: boolean; +} + +export const adapter: EntityAdapter<WebpageFamily> = createEntityAdapter<WebpageFamily>({ + selectId: (webpageFamily: WebpageFamily) => webpageFamily.id, + sortComparer: (a: WebpageFamily, b: WebpageFamily) => a.display - b.display +}); + +export const initialState: State = adapter.getInitialState({ + webpageFamilyListIsLoading: false, + webpageFamilyListIsLoaded: false +}); + +export const webpageFamilyReducer = createReducer( + initialState, + on(webpageFamilyActions.loadWebpageFamilyList, (state) => { + return { + ...state, + webpageFamilyListIsLoading: true + } + }), + on(webpageFamilyActions.loadWebpageFamilyListSuccess, (state, { webpageFamilies }) => { + return adapter.setAll( + webpageFamilies, + { + ...state, + webpageFamilyListIsLoading: false, + webpageFamilyListIsLoaded: true + } + ); + }), + on(webpageFamilyActions.loadWebpageFamilyListFail, (state) => { + return { + ...state, + webpageFamilyListIsLoading: false + } + }), + on(webpageFamilyActions.addWebpageFamilySuccess, (state, { webpageFamily }) => { + return adapter.addOne(webpageFamily, state) + }), + on(webpageFamilyActions.editWebpageFamilySuccess, (state, { webpageFamily }) => { + return adapter.setOne(webpageFamily, state) + }), + on(webpageFamilyActions.deleteWebpageFamilySuccess, (state, { webpageFamily }) => { + return adapter.removeOne(webpageFamily.id, state) + }) +); + +const { + selectIds, + selectEntities, + selectAll, + selectTotal, +} = adapter.getSelectors(); + +export const selectWebpageFamilyIds = selectIds; +export const selectWebpageFamilyEntities = selectEntities; +export const selectAllWebpageFamilies = selectAll; +export const selectWebpageFamilyTotal = selectTotal; + +export const selectWebpageFamilyListIsLoading = (state: State) => state.webpageFamilyListIsLoading; +export const selectWebpageFamilyListIsLoaded = (state: State) => state.webpageFamilyListIsLoaded; diff --git a/client/src/app/metamodel/reducers/webpage.reducer.ts b/client/src/app/metamodel/reducers/webpage.reducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..cd0a982086bef28ee5cfd7134ff482f5666cd4c2 --- /dev/null +++ b/client/src/app/metamodel/reducers/webpage.reducer.ts @@ -0,0 +1,84 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createReducer, on } from '@ngrx/store'; +import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; + +import { Webpage } from '../models'; +import * as webpageActions from '../actions/webpage.actions'; + +/** + * Interface for webpage state. + * + * @interface State + */ +export interface State extends EntityState<Webpage> { + webpageListIsLoading: boolean; + webpageListIsLoaded: boolean; +} + +export const adapter: EntityAdapter<Webpage> = createEntityAdapter<Webpage>({ + selectId: (webpage: Webpage) => webpage.id, + sortComparer: (a: Webpage, b: Webpage) => a.display - b.display +}); + +export const initialState: State = adapter.getInitialState({ + webpageListIsLoading: false, + webpageListIsLoaded: false +}); + +export const webpageReducer = createReducer( + initialState, + on(webpageActions.loadWebpageList, (state) => { + return { + ...state, + webpageListIsLoading: true + } + }), + on(webpageActions.loadWebpageListSuccess, (state, { webpages }) => { + return adapter.setAll( + webpages, + { + ...state, + webpageListIsLoading: false, + webpageListIsLoaded: true + } + ); + }), + on(webpageActions.loadWebpageListFail, (state) => { + return { + ...state, + webpageListIsLoading: false + } + }), + on(webpageActions.addWebpageSuccess, (state, { webpage }) => { + return adapter.addOne(webpage, state) + }), + on(webpageActions.editWebpageSuccess, (state, { webpage }) => { + return adapter.setOne(webpage, state) + }), + on(webpageActions.deleteWebpageSuccess, (state, { webpage }) => { + return adapter.removeOne(webpage.id, state) + }) +); + +const { + selectIds, + selectEntities, + selectAll, + selectTotal, +} = adapter.getSelectors(); + +export const selectWebpageIds = selectIds; +export const selectWebpageEntities = selectEntities; +export const selectAllWebpages = selectAll; +export const selectWebpageTotal = selectTotal; + +export const selectWebpageListIsLoading = (state: State) => state.webpageListIsLoading; +export const selectWebpageListIsLoaded = (state: State) => state.webpageListIsLoaded; diff --git a/client/src/app/metamodel/selectors/webpage-family.selector.ts b/client/src/app/metamodel/selectors/webpage-family.selector.ts new file mode 100644 index 0000000000000000000000000000000000000000..0c425422511008e57824062d9cf9684232e73b54 --- /dev/null +++ b/client/src/app/metamodel/selectors/webpage-family.selector.ts @@ -0,0 +1,48 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createSelector } from '@ngrx/store'; + +import * as reducer from '../metamodel.reducer'; +import * as fromWebpageFamily from '../reducers/webpage-family.reducer'; + +export const selectWebpageFamilyState = createSelector( + reducer.getMetamodelState, + (state: reducer.State) => state.webpageFamily +); + +export const selectWebpageFamilyIds = createSelector( + selectWebpageFamilyState, + fromWebpageFamily.selectWebpageFamilyIds +); + +export const selectWebpageFamilyEntities = createSelector( + selectWebpageFamilyState, + fromWebpageFamily.selectWebpageFamilyEntities +); + +export const selectAllWebpageFamilies = createSelector( + selectWebpageFamilyState, + fromWebpageFamily.selectAllWebpageFamilies +); + +export const selectWebpageFamilyTotal = createSelector( + selectWebpageFamilyState, + fromWebpageFamily.selectWebpageFamilyTotal +); + +export const selectWebpageFamilyListIsLoading = createSelector( + selectWebpageFamilyState, + fromWebpageFamily.selectWebpageFamilyListIsLoading +); + +export const selectWebpageFamilyListIsLoaded = createSelector( + selectWebpageFamilyState, + fromWebpageFamily.selectWebpageFamilyListIsLoaded +); diff --git a/client/src/app/metamodel/selectors/webpage.selector.ts b/client/src/app/metamodel/selectors/webpage.selector.ts new file mode 100644 index 0000000000000000000000000000000000000000..20a5be4d3a8417f5e205cf4c0f1323ffcb612899 --- /dev/null +++ b/client/src/app/metamodel/selectors/webpage.selector.ts @@ -0,0 +1,64 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createSelector } from '@ngrx/store'; + +import * as reducer from '../metamodel.reducer'; +import * as fromWebpage from '../reducers/webpage.reducer'; +import * as webpageFamilySelector from './webpage-family.selector'; + +export const selectWebpageState = createSelector( + reducer.getMetamodelState, + (state: reducer.State) => state.webpage +); + +export const selectWebpageIds = createSelector( + selectWebpageState, + fromWebpage.selectWebpageIds +); + +export const selectWebpageEntities = createSelector( + selectWebpageState, + fromWebpage.selectWebpageEntities +); + +export const selectAllWebpages = createSelector( + selectWebpageState, + fromWebpage.selectAllWebpages +); + +export const selectWebpageTotal = createSelector( + selectWebpageState, + fromWebpage.selectWebpageTotal +); + +export const selectWebpageListIsLoading = createSelector( + selectWebpageState, + fromWebpage.selectWebpageListIsLoading +); + +export const selectWebpageListIsLoaded = createSelector( + selectWebpageState, + fromWebpage.selectWebpageListIsLoaded +); + +export const selectWebpageByRouteId = createSelector( + selectWebpageEntities, + reducer.selectRouterState, + (entities, router) => entities[router.state.params.id] +); + +export const selectFirstWebpage = createSelector( + webpageFamilySelector.selectAllWebpageFamilies, + selectAllWebpages, + (webpageFamilyList, webpageList) => { + const firstWebpageFamily = webpageFamilyList[0]; + return webpageList.filter(webpage => webpage.id_webpage_family === firstWebpageFamily.id)[0]; + } +); \ No newline at end of file diff --git a/client/src/app/metamodel/services/index.ts b/client/src/app/metamodel/services/index.ts index c24336c01c0642c22818a75ee76c8be5670a9f0e..1c670871ca257a1f0da700f2ce2f45baa6e8fcd7 100644 --- a/client/src/app/metamodel/services/index.ts +++ b/client/src/app/metamodel/services/index.ts @@ -20,6 +20,8 @@ import { OutputFamilyService } from './output-family.service'; import { ImageService } from './image.service'; import { FileService } from './file.service'; import { ConeSearchConfigService } from './cone-search-config.service'; +import { WebpageFamilyService } from './webpage-family.service'; +import { WebpageService } from './webpage.service'; export const metamodelServices = [ DatabaseService, @@ -34,5 +36,7 @@ export const metamodelServices = [ OutputFamilyService, ImageService, FileService, - ConeSearchConfigService + ConeSearchConfigService, + WebpageFamilyService, + WebpageService ]; diff --git a/client/src/app/metamodel/services/webpage-family.service.ts b/client/src/app/metamodel/services/webpage-family.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..510ac66167f58a0345b8d155d394ac9aa96da7d0 --- /dev/null +++ b/client/src/app/metamodel/services/webpage-family.service.ts @@ -0,0 +1,70 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +import { Observable } from 'rxjs'; + +import { WebpageFamily } from '../models'; +import { AppConfigService } from 'src/app/app-config.service'; + +/** + * @class + * @classdesc Webpage family service. + */ +@Injectable() +export class WebpageFamilyService { + constructor(private http: HttpClient, private config: AppConfigService) { } + + /** + * Retrieves webpage families for the given instance. + * + * @param {string} instanceName - The instance. + * + * @return Observable<WebpageFamily> + */ + retrieveWebpageFamilyList(instanceName: string): Observable<WebpageFamily[]> { + return this.http.get<WebpageFamily[]>(`${this.config.apiUrl}/instance/${instanceName}/webpage-family`); + } + + /** + * Adds a new webpage family for the given instance. + * + * @param {string} instanceName - The instance. + * @param {WebpageFamily} newWebpageFamily - The webpage family. + * + * @return Observable<WebpageFamily> + */ + addWebpageFamily(instanceName: string, newWebpageFamily: WebpageFamily): Observable<WebpageFamily> { + return this.http.post<WebpageFamily>(`${this.config.apiUrl}/instance/${instanceName}/webpage-family`, newWebpageFamily); + } + + /** + * Modifies a webpage family. + * + * @param {WebpageFamily} webpageFamily - The webpage family. + * + * @return Observable<WebpageFamily> + */ + editWebpageFamily(webpageFamily: WebpageFamily): Observable<WebpageFamily> { + return this.http.put<WebpageFamily>(`${this.config.apiUrl}/webpage-family/${webpageFamily.id}`, webpageFamily); + } + + /** + * Removes a webpage family. + * + * @param {number} webpageFamilyId - The webpage family ID. + * + * @return Observable<object> + */ + deleteWebpageFamily(webpageFamilyId: number): Observable<object> { + return this.http.delete(`${this.config.apiUrl}/webpage-family/${webpageFamilyId}`); + } +} diff --git a/client/src/app/metamodel/services/webpage.service.ts b/client/src/app/metamodel/services/webpage.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac38b3589fe361a462e4304e0727f3bd3fd5f67e --- /dev/null +++ b/client/src/app/metamodel/services/webpage.service.ts @@ -0,0 +1,69 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +import { Observable } from 'rxjs'; + +import { Webpage } from '../models'; +import { AppConfigService } from 'src/app/app-config.service'; + +/** + * @class + * @classdesc Webpage service. + */ +@Injectable() +export class WebpageService { + constructor(private http: HttpClient, private config: AppConfigService) { } + + /** + * Retrieves webpages for the given instance. + * + * @param {string} instanceName - The instance. + * + * @return Observable<Webpage[]> + */ + retrieveWebpageList(instanceName: string): Observable<Webpage[]> { + return this.http.get<Webpage[]>(`${this.config.apiUrl}/instance/${instanceName}/webpage`); + } + + /** + * Adds a new webpage for the given instance. + * + * @param {Webpage} newWebpage - The webpage. + * + * @return Observable<Webpage> + */ + addWebpage(newWebpage: Webpage): Observable<Webpage> { + return this.http.post<Webpage>(`${this.config.apiUrl}/webpage-family/${newWebpage.id_webpage_family}/webpage`, newWebpage); + } + + /** + * Modifies a webpage. + * + * @param {Webpage} webpage - The Webpage. + * + * @return Observable<Webpage> + */ + editWebpage(webpage: Webpage): Observable<Webpage> { + return this.http.put<Webpage>(`${this.config.apiUrl}/webpage/${webpage.id}`, webpage); + } + + /** + * Removes a webpage. + * + * @param {number} webpageId - The webpage ID. + * + * @return Observable<object> + */ + deleteWebpage(webpageId: number): Observable<object> { + return this.http.delete(`${this.config.apiUrl}/webpage/${webpageId}`); + } +} diff --git a/client/src/app/portal/components/index.ts b/client/src/app/portal/components/index.ts index 20812c31b8e9f4c4c6456920902086940296a386..685b2995ee7cafc764e12b01b5f2d5becd7bd4fd 100644 --- a/client/src/app/portal/components/index.ts +++ b/client/src/app/portal/components/index.ts @@ -8,7 +8,9 @@ */ import { InstanceCardComponent } from './instance-card.component'; +import { PortalNavbarComponent } from './portal-navbar.component'; export const dummiesComponents = [ - InstanceCardComponent + InstanceCardComponent, + PortalNavbarComponent ]; diff --git a/client/src/app/portal/components/portal-navbar.component.html b/client/src/app/portal/components/portal-navbar.component.html new file mode 100644 index 0000000000000000000000000000000000000000..b253077a43932bf1eeb8215b9b3f9b0f89bf436b --- /dev/null +++ b/client/src/app/portal/components/portal-navbar.component.html @@ -0,0 +1,93 @@ +<nav class="navbar navbar-light bg-light navbar-expand-md fixed-top border-bottom"> + <!-- Logo --> + <a routerLink="/" class="navbar-brand"> + <img src="assets/cesam_anis40.png" alt="ANIS logo" /> + </a> + + <!-- Navigation --> + <div class="collapse navbar-collapse" id="navbarCollapse"> + <ul class="navbar-nav mr-auto"></ul> + <ul class="navbar-nav justify-content-end"> + <li *ngIf="!authenticationEnabled" class="nav-item pr-3"> + <a class="nav-link" routerLink="/admin" routerLinkActive="active"> + <span class="fas fa-tools"></span> Admin + </a> + </li> + </ul> + <!-- sign in / sign out --> + <button *ngIf="authenticationEnabled && !isAuthenticated" + class="btn btn-outline-success my-2 my-sm-0" + id="button-sign-in" + (click)="login.emit()"> + Sign In / Register + </button> + <span *ngIf="isAuthenticated" id="dropdown-menu" dropdown> + <button id="button-basic" dropdownToggle type="button" class="btn btn-light" aria-controls="dropdown-basic"> + <span class="fa-stack" [ngStyle]="{ color: '#7AC29A' }"> + <span class="fas fa-circle fa-2x"></span> + <span class="fas fa-user fa-stack-1x fa-inverse"></span> + </span> + + <span class="fas fa-chevron-down text-secondary"></span> + </button> + <ul id="basic-link-dropdown" *dropdownMenu class="dropdown-menu dropdown-menu-right dropdown-up" role="menu" aria-labelledby="basic-link"> + <li id="li-email" role="menuitem"> + <span class="dropdown-item font-italic">{{ userProfile.email }}</span> + </li> + <li class="divider dropdown-divider"></li> + <li role="menuitem"> + <a *ngIf="isAdmin()" class="dropdown-item pointer" routerLink="/admin"> + <span class="fas fa-tools"></span> Admin + </a> + <a class="dropdown-item pointer" (click)="openEditProfile.emit()"> + <span class="fas fa-id-card"></span> Edit profile + </a> + </li> + <li class="divider dropdown-divider"></li> + <li role="menuitem"> + <a class="dropdown-item text-danger pointer" (click)="logout.emit()"> + <span class="fas fa-sign-out-alt fa-fw"></span> Sign Out + </a> + </li> + </ul> + </span> + </div> + + <!-- Navigation Mobile --> + <span dropdown> + <button id="button-basic" dropdownToggle type="button" class="navbar-toggler" aria-controls="dropdown-basic"> + <span class="fas fa-bars"></span> + </button> + <ul id="basic-link-dropdown" *dropdownMenu class="dropdown-menu dropdown-menu-right dropdown-up" role="menu" + aria-labelledby="basic-link"> + <li *ngIf="isAuthenticated" role="menuitem"> + <span class="dropdown-item font-italic">{{ userProfile.email }}</span> + </li> + <li *ngIf="isAuthenticated" class="divider dropdown-divider"></li> + <li *ngIf="!authenticationEnabled" role="menuitem"> + <a class="dropdown-item pointer" routerLink="/admin"> + <span class="fas fa-tools"></span> Admin + </a> + </li> + <li *ngIf="isAuthenticated" role="menuitem"> + <a *ngIf="isAdmin()" class="dropdown-item pointer" routerLink="/admin"> + <span class="fas fa-tools"></span> Admin + </a> + <a class="dropdown-item pointer" (click)="openEditProfile.emit()"> + <span class="fas fa-id-card"></span> Edit profile + </a> + </li> + <li *ngIf="isAuthenticated" class="divider dropdown-divider"></li> + <li *ngIf="authenticationEnabled && !isAuthenticated" role="menuitem"> + <a class="dropdown-item pointer text-success" (click)="login.emit()"> + <span class="fas fa-sign-in-alt fa-fw"></span> Sign In / Register + </a> + </li> + <li *ngIf="isAuthenticated" role="menuitem"> + <a class="dropdown-item pointer text-danger" (click)="logout.emit()"> + <span class="fas fa-sign-out-alt fa-fw"></span> Sign Out + </a> + </li> + </ul> + </span> +</nav> diff --git a/client/src/app/portal/components/portal-navbar.component.scss b/client/src/app/portal/components/portal-navbar.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..1cc4ce8a2b777fb65944b30cbb3363caf453b52f --- /dev/null +++ b/client/src/app/portal/components/portal-navbar.component.scss @@ -0,0 +1,17 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +.dropdown-up { + top: 80% !important; + right: 5px !important; +} + +img { + height: 60px; +} diff --git a/client/src/app/portal/components/portal-navbar.component.ts b/client/src/app/portal/components/portal-navbar.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..b8d0ab4b1719b5cf797d1c9bd2df523637883815 --- /dev/null +++ b/client/src/app/portal/components/portal-navbar.component.ts @@ -0,0 +1,30 @@ +import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter } from '@angular/core'; + +import { UserProfile } from 'src/app/auth/user-profile.model'; +import { isAdmin } from 'src/app/shared/utils'; + +@Component({ + selector: 'app-portal-navbar', + templateUrl: 'portal-navbar.component.html', + styleUrls: [ 'portal-navbar.component.scss' ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class PortalNavbarComponent { + @Input() isAuthenticated: boolean; + @Input() userProfile: UserProfile = null; + @Input() userRoles: string[]; + @Input() authenticationEnabled: boolean; + @Input() adminRoles: string[]; + @Output() login: EventEmitter<any> = new EventEmitter(); + @Output() logout: EventEmitter<any> = new EventEmitter(); + @Output() openEditProfile: EventEmitter<any> = new EventEmitter(); + + /** + * Returns true if user is admin + * + * @returns boolean + */ + isAdmin() { + return isAdmin(this.adminRoles, this.userRoles); + } +} diff --git a/client/src/app/portal/containers/portal-home.component.html b/client/src/app/portal/containers/portal-home.component.html index 29c5b00fe27cdae382d870f9afc4b0861280b62d..ba726a547cb4689f290c2be833c770c860bb8301 100644 --- a/client/src/app/portal/containers/portal-home.component.html +++ b/client/src/app/portal/containers/portal-home.component.html @@ -1,17 +1,14 @@ <header> - <app-navbar - [links]="links" + <app-portal-navbar [isAuthenticated]="isAuthenticated | async" [userProfile]="userProfile | async" [userRoles]="userRoles | async" - [baseHref]="getBaseHref()" [authenticationEnabled]="getAuthenticationEnabled()" [adminRoles]="getAdminRoles()" - [url]="url | async" (login)="login()" (logout)="logout()" (openEditProfile)="openEditProfile()"> - </app-navbar> + </app-portal-navbar> </header> <main role="main" class="container-fluid pb-4"> <div class="container"> diff --git a/client/src/app/portal/containers/portal-home.component.spec.ts b/client/src/app/portal/containers/portal-home.component.spec.ts index 66dd2b6217b1db9aba6b6c3e28eb37e6b7fb0f04..0c9d4f20e9cfe76e2e21da30d9a6ac10bb800087 100644 --- a/client/src/app/portal/containers/portal-home.component.spec.ts +++ b/client/src/app/portal/containers/portal-home.component.spec.ts @@ -53,11 +53,6 @@ describe('[Instance][Portal][Container] PortalHomeComponent', () => { expect(component).toBeDefined(); }); - it('#getBaseHref() should return base href config key value', () => { - appConfigServiceStub.baseHref = '/my-project'; - expect(component.getBaseHref()).toBe('/my-project'); - }); - it('#authenticationEnabled() should return authentication enabled config key value', () => { appConfigServiceStub.authenticationEnabled = true; expect(component.getAuthenticationEnabled()).toBeTruthy(); diff --git a/client/src/app/portal/containers/portal-home.component.ts b/client/src/app/portal/containers/portal-home.component.ts index ceb30652b6bd4251dfe7971389d5f02bb1ca515c..56ea525430382c9f1b143d912dcc3951e5518334 100644 --- a/client/src/app/portal/containers/portal-home.component.ts +++ b/client/src/app/portal/containers/portal-home.component.ts @@ -11,7 +11,6 @@ import { Component, OnInit } from '@angular/core'; import { Observable, Subscription } from 'rxjs'; import { Store } from '@ngrx/store'; -import * as fromRouter from '@ngrx/router-store'; import { UserProfile } from 'src/app/auth/user-profile.model'; import { Instance, InstanceGroup } from 'src/app/metamodel/models'; @@ -36,13 +35,11 @@ export class PortalHomeComponent implements OnInit { public favIcon: HTMLLinkElement = document.querySelector('#favicon'); public title: HTMLLinkElement = document.querySelector('#title'); public body: HTMLBodyElement = document.querySelector('body'); - public links = []; public isAuthenticated: Observable<boolean>; public userProfile: Observable<UserProfile>; public userRoles: Observable<string[]>; public instanceList: Observable<Instance[]>; public instanceGroupList: Observable<InstanceGroup[]>; - public url: Observable<string>; public userRolesSubscription: Subscription; constructor(private store: Store<{ }>, private config: AppConfigService) { @@ -51,7 +48,6 @@ export class PortalHomeComponent implements OnInit { this.userRoles = store.select(authSelector.selectUserRoles); this.instanceList = store.select(instanceSelector.selectAllInstances); this.instanceGroupList = store.select(instanceGroupSelector.selectAllInstanceGroups); - this.url = store.select(fromRouter.getSelectors().selectUrl); } ngOnInit() { @@ -60,15 +56,6 @@ export class PortalHomeComponent implements OnInit { this.body.style.backgroundColor = 'white'; } - /** - * Returns application base href. - * - * @return string - */ - getBaseHref(): string { - return this.config.baseHref; - } - /** * Checks if authentication is enabled. * diff --git a/client/src/app/shared/components/index.ts b/client/src/app/shared/components/index.ts index 87259b3c83248b10b2b7058ac79b71127a11b7ca..18a3063846b295a531f6311a1637ce0c83265947 100644 --- a/client/src/app/shared/components/index.ts +++ b/client/src/app/shared/components/index.ts @@ -8,9 +8,7 @@ */ import { SpinnerComponent } from "./spinner.component"; -import { NavbarComponent } from './navbar.component'; export const sharedComponents = [ - SpinnerComponent, - NavbarComponent + SpinnerComponent ]; diff --git a/client/src/app/shared/components/navbar.component.spec.ts b/client/src/app/shared/components/navbar.component.spec.ts deleted file mode 100644 index c7c9debb79f73e155085b3d568c820ef6951cdc9..0000000000000000000000000000000000000000 --- a/client/src/app/shared/components/navbar.component.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { TestBed, ComponentFixture } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { NavbarComponent } from './navbar.component'; -import { INSTANCE } from '../../../test-data'; - -describe('[Shared][Component] NavbarComponent', () => { - let component: NavbarComponent; - let fixture: ComponentFixture<NavbarComponent>; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [NavbarComponent], - imports: [RouterTestingModule] - - }).compileComponents(); - fixture = TestBed.createComponent(NavbarComponent); - component = fixture.componentInstance; - }); - - it('should create the component', () => { - expect(component).toBeDefined(); - }); - - it('should return logo href', () => { - component.apiUrl = 'http://test.com'; - component.instance = INSTANCE; - expect(component.getLogoHref()).toEqual('http://test.com/instance/myInstance/file-explorer/path/to/logo'); - component.instance.design_logo = ''; - expect(component.getLogoHref()).toEqual('assets/cesam_anis40.png'); - }); -}); diff --git a/client/src/app/shared/pipes/index.ts b/client/src/app/shared/pipes/index.ts index 6f5205846b9ae9917c7d1b1ec71919e4d4b5290d..4c74ce3d070a9d0bc96dd779715b3a4af8315ef5 100644 --- a/client/src/app/shared/pipes/index.ts +++ b/client/src/app/shared/pipes/index.ts @@ -13,6 +13,7 @@ import { OutputFamilyByIdPipe } from './output-family-by-id.pipe'; import { DatasetByNamePipe } from './dataset-by-name.pipe'; import { InstanceByNamePipe } from './instance-by-name.pipe'; import { AuthImagePipe } from './auth-image.pipe'; +import { WebpageListByFamilyPipe } from './webpage-list-by-family.pipe'; export const sharedPipes = [ DatasetListByFamilyPipe, @@ -20,5 +21,6 @@ export const sharedPipes = [ OutputFamilyByIdPipe, DatasetByNamePipe, InstanceByNamePipe, - AuthImagePipe + AuthImagePipe, + WebpageListByFamilyPipe ]; diff --git a/client/src/app/shared/pipes/webpage-list-by-family.pipe.ts b/client/src/app/shared/pipes/webpage-list-by-family.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..e93b931bb4169c5d0215ad4c980334212a6f9ceb --- /dev/null +++ b/client/src/app/shared/pipes/webpage-list-by-family.pipe.ts @@ -0,0 +1,27 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Pipe, PipeTransform } from '@angular/core'; + +import { Webpage } from 'src/app/metamodel/models'; + +/** + * @class + * @classdesc Returns webpages corresponding to the given webpage family ID. + * + * @example + * // returns webpages that matching with the webpage family ID among the webpage list + * {{ webpageList | webpageListByFamily:1 }} + */ +@Pipe({ name: 'webpageListByFamily' }) +export class WebpageListByFamilyPipe implements PipeTransform { + transform(webpageList: Webpage[], idWebpageFamily: number): Webpage[] { + return webpageList.filter(webpage => webpage.id_webpage_family === idWebpageFamily); + } +} diff --git a/client/src/styles.scss b/client/src/styles.scss index 2f276ed780c804c7a33845a14cafaff9d99f1680..d706a62faffc0855002d27a111992b3351d8ab67 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -79,11 +79,11 @@ main { } /* Angular forms */ -input.ng-valid, select.ng-valid, .ng-select.ng-valid div.ng-select-container, textarea.ng-valid { +input.ng-valid, select.ng-valid, .ng-select.ng-valid div.ng-select-container, textarea.ng-valid, editor.ng-valid { border-left: 5px solid #42A948; /* green */ } -input.ng-invalid, select.ng-invalid, .ng-select.ng-invalid div.ng-select-container, textarea.ng-invalid { +input.ng-invalid, select.ng-invalid, .ng-select.ng-invalid div.ng-select-container, textarea.ng-invalid, editor.ng-invalid { border-left: 5px solid #a94442; /* red */ } diff --git a/client/src/test-data.ts b/client/src/test-data.ts index a7f29cf4c967d6cb400e2c876f96558c6f2eb255..40464d8233914649c8fddb66f59687f6158ef2cd 100644 --- a/client/src/test-data.ts +++ b/client/src/test-data.ts @@ -60,11 +60,6 @@ export const INSTANCE_LIST: Instance[] = [ design_background_color: 'darker green', design_logo: 'path/to/logo', design_favicon: 'path/to/favicon', - home_component: 'HomeComponent', - home_component_config: { - home_component_text: 'Description', - home_component_logo: 'path/to/logo' - }, samp_enabled: true, back_to_portal: true, search_by_criteria_allowed: false, @@ -93,11 +88,6 @@ export const INSTANCE_LIST: Instance[] = [ design_background_color: 'darker green', design_logo: 'path/to/logo', design_favicon: 'path/to/favicon', - home_component: 'HomeComponent', - home_component_config: { - home_component_text: 'Description', - home_component_logo: 'path/to/logo' - }, samp_enabled: true, back_to_portal: true, search_by_criteria_allowed: false, @@ -128,11 +118,6 @@ export const INSTANCE: Instance = { design_background_color: 'darker green', design_logo: '/path/to/logo', design_favicon: '/path/to/favicon', - home_component: 'HomeComponent', - home_component_config: { - home_component_text: 'Description', - home_component_logo: '/path/to/logo' - }, samp_enabled: true, back_to_portal: true, search_by_criteria_allowed: false, diff --git a/client/yarn.lock b/client/yarn.lock index 6418171403d03c5fd92b58ce44d9bfc89db43592..4962349063a3ce88930ccb6ab09f2edd10e40ac9 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1928,6 +1928,14 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@tinymce/tinymce-angular@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@tinymce/tinymce-angular/-/tinymce-angular-6.0.1.tgz#8573bef54be8533c29e938c8c96cb2057e08b7a6" + integrity sha512-USuwQwcBmvl1fN9n1FsUqM8ZOQjJLe2VleQRENU9R46ZgrB6Ic5thyUV2RPHUrNgN99QJ+4HyE465qzgv+M7Mw== + dependencies: + tinymce "^6.0.0 || ^5.5.0" + tslib "^2.3.0" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -6461,6 +6469,13 @@ ngx-bootstrap@^8.0.0: dependencies: tslib "^2.0.0" +ngx-dynamic-hooks@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/ngx-dynamic-hooks/-/ngx-dynamic-hooks-2.0.3.tgz#1a5559e7df82dfa08484409fb86c06f090b2fedf" + integrity sha512-9JNcke0tnUrM4HIWLXqPhw94R5Q61d3F0OdCWh3PNRSf6zsyeIO3Si0HHkUnqRx0mnSX9lxuqdETftcpDfM7nQ== + dependencies: + tslib "^2.0.0" + ngx-json-viewer@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/ngx-json-viewer/-/ngx-json-viewer-3.0.2.tgz#91e72fe41f80756181aa0d36b4bfaeac5df5b1b1" @@ -8182,6 +8197,11 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== +"tinymce@^6.0.0 || ^5.5.0", tinymce@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-6.0.3.tgz#993db09afa473a764ad8b594cdaf744b2c7e2e74" + integrity sha512-4cu80kWF7nRGhviE10poZtjTkl3jNL+lycilCMfdm3KU5V7FtiQQrKbEo6GInXT05RY78Ha/NFP0gOBELcSpfg== + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" diff --git a/conf-dev/create-db.sh b/conf-dev/create-db.sh index 905a1ca64357249e0bd607c11aea1f94cf6f7040..85345caa90455d1001ccb6a8443c2682d4593b2b 100644 --- a/conf-dev/create-db.sh +++ b/conf-dev/create-db.sh @@ -8,7 +8,7 @@ set -e curl -d '{"label":"Test","dbname":"anis_test","dbtype":"pdo_pgsql","dbhost":"db","dbport":5432,"dblogin":"anis","dbpassword":"anis"}' --header 'Content-Type: application/json' -X POST http://localhost/database # Add default instance -curl -d '{"name":"default","label":"Default instance","description":"Instance for the test","scientific_manager":"M. Durand","instrument":"Multiple","wavelength_domain":"Visible imaging / Spectroscopy","display":10,"data_path":"\/DEFAULT","files_path":"\/INSTANCE_FILES","public":true,"portal_logo":"","design_color":"#7AC29A","design_background_color":"#ffffff","design_logo":"/logo.png","design_favicon":"/favicon.ico","home_component":"WelcomeComponent","home_component_config":{"home_component_text":"AstroNomical Information System","home_component_logo":"/home_component_logo.png"},"samp_enabled":true,"back_to_portal":true,"search_by_criteria_allowed":true,"search_by_criteria_label":"Search","search_multiple_allowed":false,"search_multiple_label":"Search multiple","search_multiple_all_datasets_selected":false,"documentation_allowed":false,"documentation_label":"Documentation"}' --header 'Content-Type: application/json' -X POST http://localhost/instance +curl -d '{"name":"default","label":"Default instance","description":"Instance for the test","scientific_manager":"M. Durand","instrument":"Multiple","wavelength_domain":"Visible imaging / Spectroscopy","display":10,"data_path":"\/DEFAULT","files_path":"\/INSTANCE_FILES","public":true,"portal_logo":"","design_color":"#7AC29A","design_background_color":"#ffffff","design_logo":"/logo.png","design_favicon":"/favicon.ico","samp_enabled":true,"back_to_portal":true,"search_by_criteria_allowed":true,"search_by_criteria_label":"Search","search_multiple_allowed":false,"search_multiple_label":"Search multiple","search_multiple_all_datasets_selected":false,"documentation_allowed":false,"documentation_label":"Documentation"}' --header 'Content-Type: application/json' -X POST http://localhost/instance # Add dataset families curl -d '{"label":"Default dataset family","display":10,"opened":true}' --header 'Content-Type: application/json' -X POST http://localhost/instance/default/dataset-family @@ -109,3 +109,7 @@ curl -d '{"id":12,"name":"burst_id","label":"burst_id","form_label":"Burst ID"," curl -d '{"id":13,"name":"pipeline_version","label":"pipeline_version","form_label":"Pipeline version","description":null,"primary_key":false,"output_display":130,"criteria_display":130,"search_type":null,"type":"float","operator":null,"dynamic_operator":true,"min":null,"max":null,"placeholder_min":null,"placeholder_max":null,"renderer":null,"renderer_config":null,"selected":false,"order_by":true,"archive":false,"detail":false,"display_detail":130,"renderer_detail":null,"renderer_detail_config":null,"options":null,"vo_utype":null,"vo_ucd":null,"vo_unit":null,"vo_description":null,"vo_datatype":null,"vo_size":null,"id_criteria_family":null,"id_output_category":7}' --header 'Content-Type: application/json' -X POST http://localhost/dataset/products/attribute curl -d '{"id":14,"name":"schema_version","label":"schema_version","form_label":"Schema version","description":null,"primary_key":false,"output_display":140,"criteria_display":140,"search_type":null,"type":"float","operator":null,"dynamic_operator":true,"min":null,"max":null,"placeholder_min":null,"placeholder_max":null,"renderer":null,"renderer_config":null,"selected":false,"order_by":true,"archive":false,"detail":false,"display_detail":140,"renderer_detail":null,"renderer_detail_config":null,"options":null,"vo_utype":null,"vo_ucd":null,"vo_unit":null,"vo_description":null,"vo_datatype":null,"vo_size":null,"id_criteria_family":null,"id_output_category":7}' --header 'Content-Type: application/json' -X POST http://localhost/dataset/products/attribute curl -d '{"id":15,"name":"src_id","label":"src_id","form_label":"SRC ID","description":null,"primary_key":false,"output_display":150,"criteria_display":150,"search_type":null,"type":"integer","operator":null,"dynamic_operator":true,"min":null,"max":null,"placeholder_min":null,"placeholder_max":null,"renderer":null,"renderer_config":null,"selected":false,"order_by":true,"archive":false,"detail":false,"display_detail":150,"renderer_detail":null,"renderer_detail_config":null,"options":null,"vo_utype":null,"vo_ucd":null,"vo_unit":null,"vo_description":null,"vo_datatype":null,"vo_size":null,"id_criteria_family":null,"id_output_category":7}' --header 'Content-Type: application/json' -X POST http://localhost/dataset/products/attribute + +# Add webpages +curl -d '{"label":"Default","icon":null,"display":10}' --header 'Content-Type: application/json' -X POST http://localhost/instance/default/webpage-family +curl -d '{"label":"Home","icon":"fas fa-home","display":10,"title":"Home","content":"<div class=\"row align-items-center jumbotron\"><div class=\"col-6 col-md-4 order-md-2 mx-auto text-center\"><img class=\"img-fluid mb-3 mb-md-0\" src=\"http://localhost:8080/instance/default/file-explorer/home_component_logo.png\" alt=\"Instance logo\"></div><div class=\"col-md-8 order-md-1 text-justify pr-md-5\"><h2 class=\"mb-3\">Welcome to the ANIS default instance</h2><p class=\"lead\">This service provides several sub-services to interact with the database.<br>Here is a brief presentation of these sub-services:</p><ul class=\"lead\"><li><a href=\"https://drf-gitlab.cea.fr/svom/sdb/api-import/-/wikis/home\">/import/</a> => This sub-service allows you to import data L0, L1 or SR3/SR4</li><li>/export-rest => => This sub-service allows you to request and export data from the database in json format with a specific URL. To build this URL you could <a class=\"btn btn-warning\" href=\"../../search\">Go to search form</a></li></ul></div></div>"}' --header 'Content-Type: application/json' -X POST http://localhost/webpage-family/1/webpage diff --git a/server/app/dependencies.php b/server/app/dependencies.php index 994388f37e4bc123ab59b62f8994d076a30dc551..102f8df534db10df4d9be0832cc6825ba9449115 100644 --- a/server/app/dependencies.php +++ b/server/app/dependencies.php @@ -145,6 +145,26 @@ $container->set('App\Action\DatasetAction', function (ContainerInterface $c) { return new App\Action\DatasetAction($c->get('em')); }); +$container->set('App\Action\WebpageFamilyListAction', function (ContainerInterface $c) { + return new App\Action\WebpageFamilyListAction($c->get('em')); +}); + +$container->set('App\Action\WebpageFamilyAction', function (ContainerInterface $c) { + return new App\Action\WebpageFamilyAction($c->get('em')); +}); + +$container->set('App\Action\WebpageListAction', function (ContainerInterface $c) { + return new App\Action\WebpageListAction($c->get('em')); +}); + +$container->set('App\Action\WebpageAction', function (ContainerInterface $c) { + return new App\Action\WebpageAction($c->get('em')); +}); + +$container->set('App\Action\WebpageListByInstanceAction', function (ContainerInterface $c) { + return new App\Action\WebpageListByInstanceAction($c->get('em')); +}); + $container->set('App\Action\CriteriaFamilyListAction', function (ContainerInterface $c) { return new App\Action\CriteriaFamilyListAction($c->get('em')); }); diff --git a/server/app/routes.php b/server/app/routes.php index 2b6715908160e51c1ca4dfbd9c8d8e8ac0b44464..6ad263ef5e6d50fe512d28a1bc4a4b2d12eb97db 100644 --- a/server/app/routes.php +++ b/server/app/routes.php @@ -40,11 +40,16 @@ $app->group('', function (RouteCollectorProxy $group) { $group->map([OPTIONS, GET], '/instance/{name}/file-explorer[{fpath:.*}]', App\Action\InstanceFileExplorerAction::class); $group->map([OPTIONS, GET, POST], '/instance/{name}/dataset-group', App\Action\DatasetGroupListAction::class); $group->map([OPTIONS, GET, POST], '/instance/{name}/dataset-family', App\Action\DatasetFamilyListAction::class); + $group->map([OPTIONS, GET, POST], '/instance/{name}/webpage-family', App\Action\WebpageFamilyListAction::class); $group->map([OPTIONS, GET], '/instance/{name}/dataset', App\Action\DatasetListByInstanceAction::class); + $group->map([OPTIONS, GET], '/instance/{name}/webpage', App\Action\WebpageListByInstanceAction::class); $group->map([OPTIONS, GET, PUT, DELETE], '/dataset-group/{id}', App\Action\DatasetGroupAction::class); $group->map([OPTIONS, GET, PUT, DELETE], '/dataset-family/{id}', App\Action\DatasetFamilyAction::class); + $group->map([OPTIONS, GET, PUT, DELETE], '/webpage-family/{id}', App\Action\WebpageFamilyAction::class); $group->map([OPTIONS, GET, POST], '/dataset-family/{id}/dataset', App\Action\DatasetListAction::class); + $group->map([OPTIONS, GET, POST], '/webpage-family/{id}/webpage', App\Action\WebpageListAction::class); $group->map([OPTIONS, GET, PUT, DELETE], '/dataset/{name}', App\Action\DatasetAction::class); + $group->map([OPTIONS, GET, PUT, DELETE], '/webpage/{id}', App\Action\WebpageAction::class); $group->map([OPTIONS, GET], '/dataset/{name}/file-explorer[{fpath:.*}]', App\Action\DatasetFileExplorerAction::class); $group->map([OPTIONS, GET, POST], '/dataset/{name}/criteria-family', App\Action\CriteriaFamilyListAction::class); $group->map([OPTIONS, GET, PUT, DELETE], '/criteria-family/{id}', App\Action\CriteriaFamilyAction::class); diff --git a/server/doctrine-proxy/__CG__AppEntityInstance.php b/server/doctrine-proxy/__CG__AppEntityInstance.php index bd2aab261cf8de6bbff87ff7ca87acc15770cc14..a1f2d42bcc6ff779a110dc3fca2b2f4f4875bafb 100644 --- a/server/doctrine-proxy/__CG__AppEntityInstance.php +++ b/server/doctrine-proxy/__CG__AppEntityInstance.php @@ -67,10 +67,10 @@ class Instance extends \App\Entity\Instance implements \Doctrine\ORM\Proxy\Proxy public function __sleep() { if ($this->__isInitialized__) { - return ['__isInitialized__', 'name', 'label', 'description', 'scientificManager', 'instrument', 'wavelengthDomain', 'display', 'dataPath', 'filesPath', 'public', 'portalLogo', 'designColor', 'designBackgroundColor', 'designLogo', 'designFavicon', 'homeComponent', 'homeComponentConfig', 'sampEnabled', 'backToPortal', 'searchByCriteriaAllowed', 'searchByCriteriaLabel', 'searchMultipleAllowed', 'searchMultipleLabel', 'searchMultipleAllDatasetsSelected', 'documentationAllowed', 'documentationLabel', 'datasetFamilies']; + return ['__isInitialized__', 'name', 'label', 'description', 'scientificManager', 'instrument', 'wavelengthDomain', 'display', 'dataPath', 'filesPath', 'public', 'portalLogo', 'designColor', 'designBackgroundColor', 'designLogo', 'designFavicon', 'sampEnabled', 'backToPortal', 'searchByCriteriaAllowed', 'searchByCriteriaLabel', 'searchMultipleAllowed', 'searchMultipleLabel', 'searchMultipleAllDatasetsSelected', 'documentationAllowed', 'documentationLabel', 'datasetFamilies']; } - return ['__isInitialized__', 'name', 'label', 'description', 'scientificManager', 'instrument', 'wavelengthDomain', 'display', 'dataPath', 'filesPath', 'public', 'portalLogo', 'designColor', 'designBackgroundColor', 'designLogo', 'designFavicon', 'homeComponent', 'homeComponentConfig', 'sampEnabled', 'backToPortal', 'searchByCriteriaAllowed', 'searchByCriteriaLabel', 'searchMultipleAllowed', 'searchMultipleLabel', 'searchMultipleAllDatasetsSelected', 'documentationAllowed', 'documentationLabel', 'datasetFamilies']; + return ['__isInitialized__', 'name', 'label', 'description', 'scientificManager', 'instrument', 'wavelengthDomain', 'display', 'dataPath', 'filesPath', 'public', 'portalLogo', 'designColor', 'designBackgroundColor', 'designLogo', 'designFavicon', 'sampEnabled', 'backToPortal', 'searchByCriteriaAllowed', 'searchByCriteriaLabel', 'searchMultipleAllowed', 'searchMultipleLabel', 'searchMultipleAllDatasetsSelected', 'documentationAllowed', 'documentationLabel', 'datasetFamilies']; } /** @@ -500,50 +500,6 @@ class Instance extends \App\Entity\Instance implements \Doctrine\ORM\Proxy\Proxy return parent::setDesignFavicon($designFavicon); } - /** - * {@inheritDoc} - */ - public function getHomeComponent() - { - - $this->__initializer__ && $this->__initializer__->__invoke($this, 'getHomeComponent', []); - - return parent::getHomeComponent(); - } - - /** - * {@inheritDoc} - */ - public function setHomeComponent($homeComponent) - { - - $this->__initializer__ && $this->__initializer__->__invoke($this, 'setHomeComponent', [$homeComponent]); - - return parent::setHomeComponent($homeComponent); - } - - /** - * {@inheritDoc} - */ - public function getHomeComponentConfig() - { - - $this->__initializer__ && $this->__initializer__->__invoke($this, 'getHomeComponentConfig', []); - - return parent::getHomeComponentConfig(); - } - - /** - * {@inheritDoc} - */ - public function setHomeComponentConfig($homeComponentConfig) - { - - $this->__initializer__ && $this->__initializer__->__invoke($this, 'setHomeComponentConfig', [$homeComponentConfig]); - - return parent::setHomeComponentConfig($homeComponentConfig); - } - /** * {@inheritDoc} */ diff --git a/server/doctrine-proxy/__CG__AppEntityWebpage.php b/server/doctrine-proxy/__CG__AppEntityWebpage.php new file mode 100644 index 0000000000000000000000000000000000000000..45116fc175f6bdcf3675b96510f029606488d871 --- /dev/null +++ b/server/doctrine-proxy/__CG__AppEntityWebpage.php @@ -0,0 +1,338 @@ +<?php + +namespace DoctrineProxies\__CG__\App\Entity; + + +/** + * DO NOT EDIT THIS FILE - IT WAS CREATED BY DOCTRINE'S PROXY GENERATOR + */ +class Webpage extends \App\Entity\Webpage implements \Doctrine\ORM\Proxy\Proxy +{ + /** + * @var \Closure the callback responsible for loading properties in the proxy object. This callback is called with + * three parameters, being respectively the proxy object to be initialized, the method that triggered the + * initialization process and an array of ordered parameters that were passed to that method. + * + * @see \Doctrine\Common\Proxy\Proxy::__setInitializer + */ + public $__initializer__; + + /** + * @var \Closure the callback responsible of loading properties that need to be copied in the cloned object + * + * @see \Doctrine\Common\Proxy\Proxy::__setCloner + */ + public $__cloner__; + + /** + * @var boolean flag indicating if this object was already initialized + * + * @see \Doctrine\Persistence\Proxy::__isInitialized + */ + public $__isInitialized__ = false; + + /** + * @var array<string, null> properties to be lazy loaded, indexed by property name + */ + public static $lazyPropertiesNames = array ( +); + + /** + * @var array<string, mixed> default values of properties to be lazy loaded, with keys being the property names + * + * @see \Doctrine\Common\Proxy\Proxy::__getLazyProperties + */ + public static $lazyPropertiesDefaults = array ( +); + + + + public function __construct(?\Closure $initializer = null, ?\Closure $cloner = null) + { + + $this->__initializer__ = $initializer; + $this->__cloner__ = $cloner; + } + + + + + + + + /** + * + * @return array + */ + public function __sleep() + { + if ($this->__isInitialized__) { + return ['__isInitialized__', 'id', 'label', 'icon', 'display', 'title', 'content', 'webpageFamily']; + } + + return ['__isInitialized__', 'id', 'label', 'icon', 'display', 'title', 'content', 'webpageFamily']; + } + + /** + * + */ + public function __wakeup() + { + if ( ! $this->__isInitialized__) { + $this->__initializer__ = function (Webpage $proxy) { + $proxy->__setInitializer(null); + $proxy->__setCloner(null); + + $existingProperties = get_object_vars($proxy); + + foreach ($proxy::$lazyPropertiesDefaults as $property => $defaultValue) { + if ( ! array_key_exists($property, $existingProperties)) { + $proxy->$property = $defaultValue; + } + } + }; + + } + } + + /** + * + */ + public function __clone() + { + $this->__cloner__ && $this->__cloner__->__invoke($this, '__clone', []); + } + + /** + * Forces initialization of the proxy + */ + public function __load() + { + $this->__initializer__ && $this->__initializer__->__invoke($this, '__load', []); + } + + /** + * {@inheritDoc} + * @internal generated method: use only when explicitly handling proxy specific loading logic + */ + public function __isInitialized() + { + return $this->__isInitialized__; + } + + /** + * {@inheritDoc} + * @internal generated method: use only when explicitly handling proxy specific loading logic + */ + public function __setInitialized($initialized) + { + $this->__isInitialized__ = $initialized; + } + + /** + * {@inheritDoc} + * @internal generated method: use only when explicitly handling proxy specific loading logic + */ + public function __setInitializer(\Closure $initializer = null) + { + $this->__initializer__ = $initializer; + } + + /** + * {@inheritDoc} + * @internal generated method: use only when explicitly handling proxy specific loading logic + */ + public function __getInitializer() + { + return $this->__initializer__; + } + + /** + * {@inheritDoc} + * @internal generated method: use only when explicitly handling proxy specific loading logic + */ + public function __setCloner(\Closure $cloner = null) + { + $this->__cloner__ = $cloner; + } + + /** + * {@inheritDoc} + * @internal generated method: use only when explicitly handling proxy specific cloning logic + */ + public function __getCloner() + { + return $this->__cloner__; + } + + /** + * {@inheritDoc} + * @internal generated method: use only when explicitly handling proxy specific loading logic + * @deprecated no longer in use - generated code now relies on internal components rather than generated public API + * @static + */ + public function __getLazyProperties() + { + return self::$lazyPropertiesDefaults; + } + + + /** + * {@inheritDoc} + */ + public function getId() + { + if ($this->__isInitialized__ === false) { + return (int) parent::getId(); + } + + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'getId', []); + + return parent::getId(); + } + + /** + * {@inheritDoc} + */ + public function getLabel() + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'getLabel', []); + + return parent::getLabel(); + } + + /** + * {@inheritDoc} + */ + public function setLabel($label) + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'setLabel', [$label]); + + return parent::setLabel($label); + } + + /** + * {@inheritDoc} + */ + public function getIcon() + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'getIcon', []); + + return parent::getIcon(); + } + + /** + * {@inheritDoc} + */ + public function setIcon($icon) + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'setIcon', [$icon]); + + return parent::setIcon($icon); + } + + /** + * {@inheritDoc} + */ + public function getDisplay() + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'getDisplay', []); + + return parent::getDisplay(); + } + + /** + * {@inheritDoc} + */ + public function setDisplay($display) + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'setDisplay', [$display]); + + return parent::setDisplay($display); + } + + /** + * {@inheritDoc} + */ + public function getTitle() + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'getTitle', []); + + return parent::getTitle(); + } + + /** + * {@inheritDoc} + */ + public function setTitle($title) + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'setTitle', [$title]); + + return parent::setTitle($title); + } + + /** + * {@inheritDoc} + */ + public function getContent() + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'getContent', []); + + return parent::getContent(); + } + + /** + * {@inheritDoc} + */ + public function setContent($content) + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'setContent', [$content]); + + return parent::setContent($content); + } + + /** + * {@inheritDoc} + */ + public function getWebpageFamily() + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'getWebpageFamily', []); + + return parent::getWebpageFamily(); + } + + /** + * {@inheritDoc} + */ + public function setWebpageFamily($webpageFamily) + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'setWebpageFamily', [$webpageFamily]); + + return parent::setWebpageFamily($webpageFamily); + } + + /** + * {@inheritDoc} + */ + public function jsonSerialize(): array + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'jsonSerialize', []); + + return parent::jsonSerialize(); + } + +} diff --git a/server/doctrine-proxy/__CG__AppEntityWebpageFamily.php b/server/doctrine-proxy/__CG__AppEntityWebpageFamily.php new file mode 100644 index 0000000000000000000000000000000000000000..5a5932356c40ec5c854db947736705effbbf3cf7 --- /dev/null +++ b/server/doctrine-proxy/__CG__AppEntityWebpageFamily.php @@ -0,0 +1,272 @@ +<?php + +namespace DoctrineProxies\__CG__\App\Entity; + + +/** + * DO NOT EDIT THIS FILE - IT WAS CREATED BY DOCTRINE'S PROXY GENERATOR + */ +class WebpageFamily extends \App\Entity\WebpageFamily implements \Doctrine\ORM\Proxy\Proxy +{ + /** + * @var \Closure the callback responsible for loading properties in the proxy object. This callback is called with + * three parameters, being respectively the proxy object to be initialized, the method that triggered the + * initialization process and an array of ordered parameters that were passed to that method. + * + * @see \Doctrine\Common\Proxy\Proxy::__setInitializer + */ + public $__initializer__; + + /** + * @var \Closure the callback responsible of loading properties that need to be copied in the cloned object + * + * @see \Doctrine\Common\Proxy\Proxy::__setCloner + */ + public $__cloner__; + + /** + * @var boolean flag indicating if this object was already initialized + * + * @see \Doctrine\Persistence\Proxy::__isInitialized + */ + public $__isInitialized__ = false; + + /** + * @var array<string, null> properties to be lazy loaded, indexed by property name + */ + public static $lazyPropertiesNames = array ( +); + + /** + * @var array<string, mixed> default values of properties to be lazy loaded, with keys being the property names + * + * @see \Doctrine\Common\Proxy\Proxy::__getLazyProperties + */ + public static $lazyPropertiesDefaults = array ( +); + + + + public function __construct(?\Closure $initializer = null, ?\Closure $cloner = null) + { + + $this->__initializer__ = $initializer; + $this->__cloner__ = $cloner; + } + + + + + + + + /** + * + * @return array + */ + public function __sleep() + { + if ($this->__isInitialized__) { + return ['__isInitialized__', 'id', 'label', 'icon', 'display', 'instance']; + } + + return ['__isInitialized__', 'id', 'label', 'icon', 'display', 'instance']; + } + + /** + * + */ + public function __wakeup() + { + if ( ! $this->__isInitialized__) { + $this->__initializer__ = function (WebpageFamily $proxy) { + $proxy->__setInitializer(null); + $proxy->__setCloner(null); + + $existingProperties = get_object_vars($proxy); + + foreach ($proxy::$lazyPropertiesDefaults as $property => $defaultValue) { + if ( ! array_key_exists($property, $existingProperties)) { + $proxy->$property = $defaultValue; + } + } + }; + + } + } + + /** + * + */ + public function __clone() + { + $this->__cloner__ && $this->__cloner__->__invoke($this, '__clone', []); + } + + /** + * Forces initialization of the proxy + */ + public function __load() + { + $this->__initializer__ && $this->__initializer__->__invoke($this, '__load', []); + } + + /** + * {@inheritDoc} + * @internal generated method: use only when explicitly handling proxy specific loading logic + */ + public function __isInitialized() + { + return $this->__isInitialized__; + } + + /** + * {@inheritDoc} + * @internal generated method: use only when explicitly handling proxy specific loading logic + */ + public function __setInitialized($initialized) + { + $this->__isInitialized__ = $initialized; + } + + /** + * {@inheritDoc} + * @internal generated method: use only when explicitly handling proxy specific loading logic + */ + public function __setInitializer(\Closure $initializer = null) + { + $this->__initializer__ = $initializer; + } + + /** + * {@inheritDoc} + * @internal generated method: use only when explicitly handling proxy specific loading logic + */ + public function __getInitializer() + { + return $this->__initializer__; + } + + /** + * {@inheritDoc} + * @internal generated method: use only when explicitly handling proxy specific loading logic + */ + public function __setCloner(\Closure $cloner = null) + { + $this->__cloner__ = $cloner; + } + + /** + * {@inheritDoc} + * @internal generated method: use only when explicitly handling proxy specific cloning logic + */ + public function __getCloner() + { + return $this->__cloner__; + } + + /** + * {@inheritDoc} + * @internal generated method: use only when explicitly handling proxy specific loading logic + * @deprecated no longer in use - generated code now relies on internal components rather than generated public API + * @static + */ + public function __getLazyProperties() + { + return self::$lazyPropertiesDefaults; + } + + + /** + * {@inheritDoc} + */ + public function getId() + { + if ($this->__isInitialized__ === false) { + return (int) parent::getId(); + } + + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'getId', []); + + return parent::getId(); + } + + /** + * {@inheritDoc} + */ + public function getLabel() + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'getLabel', []); + + return parent::getLabel(); + } + + /** + * {@inheritDoc} + */ + public function setLabel($label) + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'setLabel', [$label]); + + return parent::setLabel($label); + } + + /** + * {@inheritDoc} + */ + public function getIcon() + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'getIcon', []); + + return parent::getIcon(); + } + + /** + * {@inheritDoc} + */ + public function setIcon($icon) + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'setIcon', [$icon]); + + return parent::setIcon($icon); + } + + /** + * {@inheritDoc} + */ + public function getDisplay() + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'getDisplay', []); + + return parent::getDisplay(); + } + + /** + * {@inheritDoc} + */ + public function setDisplay($display) + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'setDisplay', [$display]); + + return parent::setDisplay($display); + } + + /** + * {@inheritDoc} + */ + public function jsonSerialize(): array + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'jsonSerialize', []); + + return parent::jsonSerialize(); + } + +} diff --git a/server/src/Action/InstanceAction.php b/server/src/Action/InstanceAction.php index 658c17e505c91c046d70613f33d44ee53deeb80f..26b7dca1b7b483c3e68ed6bcd52e04824a3b5675 100644 --- a/server/src/Action/InstanceAction.php +++ b/server/src/Action/InstanceAction.php @@ -78,8 +78,6 @@ final class InstanceAction extends AbstractAction 'design_background_color', 'design_logo', 'design_favicon', - 'home_component', - 'home_component_config', 'samp_enabled', 'back_to_portal', 'search_by_criteria_allowed', @@ -138,8 +136,6 @@ final class InstanceAction extends AbstractAction $instance->setDesignBackgroundColor($parsedBody['design_background_color']); $instance->setDesignLogo($parsedBody['design_logo']); $instance->setDesignFavicon($parsedBody['design_favicon']); - $instance->setHomeComponent($parsedBody['home_component']); - $instance->setHomeComponentConfig($parsedBody['home_component_config']); $instance->setSampEnabled($parsedBody['samp_enabled']); $instance->setBackToPortal($parsedBody['back_to_portal']); $instance->setSearchByCriteriaAllowed($parsedBody['search_by_criteria_allowed']); diff --git a/server/src/Action/InstanceListAction.php b/server/src/Action/InstanceListAction.php index 2cf086edd5736a3ed630a371233db76242abea3d..dce371131a1163c8d651040eaefdea9e12fcfa34 100644 --- a/server/src/Action/InstanceListAction.php +++ b/server/src/Action/InstanceListAction.php @@ -78,8 +78,6 @@ final class InstanceListAction extends AbstractAction 'design_background_color', 'design_logo', 'design_favicon', - 'home_component', - 'home_component_config', 'samp_enabled', 'back_to_portal', 'search_by_criteria_allowed', @@ -132,8 +130,6 @@ final class InstanceListAction extends AbstractAction $instance->setDesignBackgroundColor($parsedBody['design_background_color']); $instance->setDesignLogo($parsedBody['design_logo']); $instance->setDesignFavicon($parsedBody['design_favicon']); - $instance->setHomeComponent($parsedBody['home_component']); - $instance->setHomeComponentConfig($parsedBody['home_component_config']); $instance->setSampEnabled($parsedBody['samp_enabled']); $instance->setBackToPortal($parsedBody['back_to_portal']); $instance->setSearchByCriteriaAllowed($parsedBody['search_by_criteria_allowed']); diff --git a/server/src/Action/WebpageAction.php b/server/src/Action/WebpageAction.php new file mode 100644 index 0000000000000000000000000000000000000000..9ecd704f93d720d030e44d1dc42c648aa9ecbf18 --- /dev/null +++ b/server/src/Action/WebpageAction.php @@ -0,0 +1,107 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use App\Entity\Webpage; + +/** + * @author François Agneray <francois.agneray@lam.fr> + * @package App\Action + */ +final class WebpageAction extends AbstractAction +{ + /** + * `GET` Returns the webpage found + * `PUT` Full update the webpage and returns the new version + * `DELETE` Delete the webpage found and return a confirmation message + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke( + ServerRequestInterface $request, + ResponseInterface $response, + array $args + ): ResponseInterface { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); + } + + // Search the correct webpage with primary key + $webpage = $this->em->find('App\Entity\Webpage', $args['id']); + + // If webpage is not found 404 + if (is_null($webpage)) { + throw new HttpNotFoundException( + $request, + 'Webpage with id ' . $args['id'] . ' is not found' + ); + } + + if ($request->getMethod() === GET) { + $payload = json_encode($webpage); + } + + if ($request->getMethod() === PUT) { + $parsedBody = $request->getParsedBody(); + + $fields = array('label', 'icon', 'display', 'title', 'content'); + foreach ($fields as $a) { + if (!array_key_exists($a, $parsedBody)) { + throw new HttpBadRequestException( + $request, + 'Param ' . $a . ' needed to edit the webpage' + ); + } + } + + $this->editWebpage($webpage, $parsedBody); + $payload = json_encode($webpage); + } + + if ($request->getMethod() === DELETE) { + $id = $webpage->getId(); + $this->em->remove($webpage); + $this->em->flush(); + $payload = json_encode(array( + 'message' => 'Webpage with id ' . $id . ' is removed!' + )); + } + + $response->getBody()->write($payload); + return $response; + } + + /** + * Update webpage object with setters + * + * @param Webpage $webpage The webpage to update + * @param array $parsedBody Contains the new values ​​of the webpage sent by the user + */ + private function editWebpage(Webpage $webpage, array $parsedBody): void + { + $webpage->setLabel($parsedBody['label']); + $webpage->setIcon($parsedBody['icon']); + $webpage->setDisplay($parsedBody['display']); + $webpage->setTitle($parsedBody['title']); + $webpage->setContent($parsedBody['content']); + $this->em->flush(); + } +} diff --git a/server/src/Action/WebpageFamilyAction.php b/server/src/Action/WebpageFamilyAction.php new file mode 100644 index 0000000000000000000000000000000000000000..e84c397bc547e65a3fcde052353e1cd8819b7a5d --- /dev/null +++ b/server/src/Action/WebpageFamilyAction.php @@ -0,0 +1,105 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use App\Entity\WebpageFamily; + +/** + * @author François Agneray <francois.agneray@lam.fr> + * @package App\Action + */ +final class WebpageFamilyAction extends AbstractAction +{ + /** + * `GET` Returns the webpage family found + * `PUT` Full update the webpage family and returns the new version + * `DELETE` Delete the webpage family found and return a confirmation message + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke( + ServerRequestInterface $request, + ResponseInterface $response, + array $args + ): ResponseInterface { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); + } + + // Search the correct webpage family with primary key + $webpageFamily = $this->em->find('App\Entity\WebpageFamily', $args['id']); + + // If webpage family is not found 404 + if (is_null($webpageFamily)) { + throw new HttpNotFoundException( + $request, + 'Webpage family with id ' . $args['id'] . ' is not found' + ); + } + + if ($request->getMethod() === GET) { + $payload = json_encode($webpageFamily); + } + + if ($request->getMethod() === PUT) { + $parsedBody = $request->getParsedBody(); + + $fields = array('label', 'icon', 'display'); + foreach ($fields as $a) { + if (!array_key_exists($a, $parsedBody)) { + throw new HttpBadRequestException( + $request, + 'Param ' . $a . ' needed to edit the webpage family' + ); + } + } + + $this->editWebpageFamily($webpageFamily, $parsedBody); + $payload = json_encode($webpageFamily); + } + + if ($request->getMethod() === DELETE) { + $id = $webpageFamily->getId(); + $this->em->remove($webpageFamily); + $this->em->flush(); + $payload = json_encode(array( + 'message' => 'Webpage family with id ' . $id . ' is removed!' + )); + } + + $response->getBody()->write($payload); + return $response; + } + + /** + * Update webpage family object with setters + * + * @param WebpageFamily $family The webpage family to update + * @param array $parsedBody Contains the new values ​​of the webpage family sent by the user + */ + private function editWebpageFamily(WebpageFamily $family, array $parsedBody): void + { + $family->setLabel($parsedBody['label']); + $family->setIcon($parsedBody['icon']); + $family->setDisplay($parsedBody['display']); + $this->em->flush(); + } +} diff --git a/server/src/Action/WebpageFamilyListAction.php b/server/src/Action/WebpageFamilyListAction.php new file mode 100644 index 0000000000000000000000000000000000000000..20b0cf81d8956a063cdb1b33e29390a128ce4470 --- /dev/null +++ b/server/src/Action/WebpageFamilyListAction.php @@ -0,0 +1,104 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use App\Entity\Instance; +use App\Entity\WebpageFamily; + +/** + * @author François Agneray <francois.agneray@lam.fr> + * @package App\Action + */ +final class WebpageFamilyListAction extends AbstractAction +{ + /** + * `GET` Returns a list of all webpage family for a given instance + * `POST` Add a new webpage family to a given instance + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke( + ServerRequestInterface $request, + ResponseInterface $response, + array $args + ): ResponseInterface { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + } + + $instance = $this->em->find('App\Entity\Instance', $args['name']); + + // Returns HTTP 404 if the instance is not found + if (is_null($instance)) { + throw new HttpNotFoundException( + $request, + 'Instance with name ' . $args['name'] . ' is not found' + ); + } + + if ($request->getMethod() === GET) { + $families = $this->em->getRepository('App\Entity\WebpageFamily')->findBy(array('instance' => $instance)); + $payload = json_encode($families); + } + + if ($request->getMethod() === POST) { + $parsedBody = $request->getParsedBody(); + + // To work this action needs information + foreach (array('label', 'icon', 'display') as $a) { + if (!array_key_exists($a, $parsedBody)) { + throw new HttpBadRequestException( + $request, + 'Param ' . $a . ' needed to add a new webpage family' + ); + } + } + + $family = $this->postWebpageFamily($parsedBody, $instance); + $payload = json_encode($family); + $response = $response->withStatus(201); + } + + $response->getBody()->write($payload); + return $response; + } + + /** + * Add a new webpage family into the metamodel + * + * @param array $parsedBody Contains the values ​​of the new webpage family sent by the user + * @param Instance $instance The instance for adding the webpage family + * + * @return WebpageFamily + */ + private function postWebpageFamily(array $parsedBody, Instance $instance): WebpageFamily + { + $family = new WebpageFamily($instance); + $family->setLabel($parsedBody['label']); + $family->setIcon($parsedBody['icon']); + $family->setDisplay($parsedBody['display']); + + $this->em->persist($family); + $this->em->flush(); + + return $family; + } +} diff --git a/server/src/Action/WebpageListAction.php b/server/src/Action/WebpageListAction.php new file mode 100644 index 0000000000000000000000000000000000000000..71d9d8600bb8609cd1fb49f97591e8c306a84b8d --- /dev/null +++ b/server/src/Action/WebpageListAction.php @@ -0,0 +1,109 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; +use App\Entity\WebpageFamily; +use App\Entity\Webpage; + +/** + * @author François Agneray <francois.agneray@lam.fr> + * @package App\Action + */ +final class WebpageListAction extends AbstractAction +{ + /** + * `GET` Returns a list of all webpages for a given webpage family + * `POST` Add a new webpage + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke( + ServerRequestInterface $request, + ResponseInterface $response, + array $args + ): ResponseInterface { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + } + + $webpageFamily = $this->em->find('App\Entity\WebpageFamily', $args['id']); + + // Returns HTTP 404 if the dataset family is not found + if (is_null($webpageFamily)) { + throw new HttpNotFoundException( + $request, + 'Webpage family with id ' . $args['id'] . ' is not found' + ); + } + + if ($request->getMethod() === GET) { + $webpages = $this->em->getRepository('App\Entity\Webpage')->findBy( + array('webpageFamily' => $webpageFamily) + ); + $payload = json_encode($webpages); + } + + if ($request->getMethod() === POST) { + $parsedBody = $request->getParsedBody(); + + // To work this action needs information + foreach (array('label', 'icon', 'display', 'title', 'content') as $a) { + if (!array_key_exists($a, $parsedBody)) { + throw new HttpBadRequestException( + $request, + 'Param ' . $a . ' needed to add a new webpage' + ); + } + } + + $webpage = $this->postWebpage($parsedBody, $webpageFamily); + $payload = json_encode($webpage); + $response = $response->withStatus(201); + } + + $response->getBody()->write($payload); + return $response; + } + + /** + * Add a new webpage into the metamodel + * + * @param array $parsedBody Contains the values ​​of the new webpage sent by the user + * @param WebpageFamily $webpageFamily The wbepage family for adding the webpage + * + * @return Webpage + */ + private function postWebpage(array $parsedBody, WebpageFamily $webpageFamily): Webpage + { + $webpage = new Webpage(); + $webpage->setLabel($parsedBody['label']); + $webpage->setIcon($parsedBody['icon']); + $webpage->setDisplay($parsedBody['display']); + $webpage->setTitle($parsedBody['title']); + $webpage->setContent($parsedBody['content']); + $webpage->setWebpageFamily($webpageFamily); + + $this->em->persist($webpage); + $this->em->flush(); + + return $webpage; + } +} diff --git a/server/src/Action/WebpageListByInstanceAction.php b/server/src/Action/WebpageListByInstanceAction.php new file mode 100644 index 0000000000000000000000000000000000000000..bcd06714bee4244c558821d5840e6787ea4f98a2 --- /dev/null +++ b/server/src/Action/WebpageListByInstanceAction.php @@ -0,0 +1,79 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Action; + +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; +use Doctrine\ORM\EntityManagerInterface; +use Slim\Exception\HttpNotFoundException; + +/** + * @author François Agneray <francois.agneray@lam.fr> + * @package App\Action + */ +final class WebpageListByInstanceAction extends AbstractAction +{ + /** + * Create the classe before call __invoke to execute the action + * + * @param EntityManagerInterface $em Doctrine Entity Manager Interface + */ + public function __construct(EntityManagerInterface $em) + { + parent::__construct($em); + } + + /** + * `GET` Returns a list of all webpages for a given instance + * + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param ResponseInterface $response PSR-7 This object represents the HTTP response + * @param string[] $args This table contains information transmitted in the URL (see routes.php) + * + * @return ResponseInterface + */ + public function __invoke( + ServerRequestInterface $request, + ResponseInterface $response, + array $args + ): ResponseInterface { + if ($request->getMethod() === OPTIONS) { + return $response->withHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + } + + $instance = $this->em->find('App\Entity\Instance', $args['name']); + + // Returns HTTP 404 if the dataset is not found + if (is_null($instance)) { + throw new HttpNotFoundException( + $request, + 'Instance with name ' . $args['name'] . ' is not found' + ); + } + + if ($request->getMethod() === GET) { + $qb = $this->em->createQueryBuilder(); + $qb->select('w') + ->from('App\Entity\Webpage', 'w') + ->join('w.webpageFamily', 'f') + ->where($qb->expr()->eq('IDENTITY(f.instance)', ':instanceName')); + + $qb->setParameter('instanceName', $instance->getName()); + $webpages = $qb->getQuery()->getResult(); + $payload = json_encode($webpages); + } + + $response->getBody()->write($payload); + return $response; + } +} diff --git a/server/src/Entity/Instance.php b/server/src/Entity/Instance.php index 88f42af0d4cc230389286800040de3c2af4ba467..3c43b7787b9880380ca3234ddeed46d3e166b26f 100644 --- a/server/src/Entity/Instance.php +++ b/server/src/Entity/Instance.php @@ -129,20 +129,6 @@ class Instance implements \JsonSerializable */ protected $designFavicon; - /** - * @var string - * - * @Column(type="string", name="home_component", nullable=true) - */ - protected $homeComponent; - - /** - * @var array - * - * @Column(type="json", name="home_component_config", nullable=true) - */ - protected $homeComponentConfig; - /** * @var bool * @@ -365,26 +351,6 @@ class Instance implements \JsonSerializable $this->designFavicon = $designFavicon; } - public function getHomeComponent() - { - return $this->homeComponent; - } - - public function setHomeComponent($homeComponent) - { - $this->homeComponent = $homeComponent; - } - - public function getHomeComponentConfig() - { - return $this->homeComponentConfig; - } - - public function setHomeComponentConfig($homeComponentConfig) - { - $this->homeComponentConfig = $homeComponentConfig; - } - public function getSampEnabled() { return $this->sampEnabled; @@ -507,8 +473,6 @@ class Instance implements \JsonSerializable 'design_background_color' => $this->getDesignBackgroundColor(), 'design_logo' => $this->getDesignLogo(), 'design_favicon' => $this->getDesignFavicon(), - 'home_component' => $this->getHomeComponent(), - 'home_component_config' => $this->getHomeComponentConfig(), 'samp_enabled' => $this->getSampEnabled(), 'back_to_portal' => $this->getBackToPortal(), 'search_by_criteria_allowed' => $this->getSearchByCriteriaAllowed(), diff --git a/server/src/Entity/Webpage.php b/server/src/Entity/Webpage.php new file mode 100644 index 0000000000000000000000000000000000000000..bd17832a77ba89435624d95c234e4d0708d41e47 --- /dev/null +++ b/server/src/Entity/Webpage.php @@ -0,0 +1,153 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Entity; + +/** + * @author François Agneray <francois.agneray@lam.fr> + * @package App\Entity + * + * @Entity + * @Table(name="webpage") + */ +class Webpage implements \JsonSerializable +{ + /** + * @var int + * + * @Id + * @Column(type="integer", nullable=false) + * @GeneratedValue + */ + protected $id; + + /** + * @var string + * + * @Column(type="string", name="label", nullable=false) + */ + protected $label; + + /** + * @var string + * + * @Column(type="string", nullable=true) + */ + protected $icon; + + /** + * @var int + * + * @Column(type="integer", name="display", nullable=false) + */ + protected $display; + + /** + * @var string + * + * @Column(type="string", name="title", nullable=false) + */ + protected $title; + + /** + * @var string + * + * @Column(type="text", name="content", nullable=false) + */ + protected $content; + + /** + * @var WebpageFamily + * + * @ManyToOne(targetEntity="WebpageFamily") + * @JoinColumn(name="id_webpage_family", referencedColumnName="id", nullable=false, onDelete="CASCADE") + */ + protected $webpageFamily; + + public function getId() + { + return $this->id; + } + + public function getLabel() + { + return $this->label; + } + + public function setLabel($label) + { + $this->label = $label; + } + + public function getIcon() + { + return $this->icon; + } + + public function setIcon($icon) + { + $this->icon = $icon; + } + + public function getDisplay() + { + return $this->display; + } + + public function setDisplay($display) + { + $this->display = $display; + } + + public function getTitle() + { + return $this->title; + } + + public function setTitle($title) + { + $this->title = $title; + } + + public function getContent() + { + return $this->content; + } + + public function setContent($content) + { + $this->content = $content; + } + + public function getWebpageFamily() + { + return $this->webpageFamily; + } + + public function setWebpageFamily($webpageFamily) + { + $this->webpageFamily = $webpageFamily; + } + + public function jsonSerialize(): array + { + return [ + 'id' => $this->getId(), + 'label' => $this->getLabel(), + 'icon' => $this->getIcon(), + 'display' => $this->getDisplay(), + 'title' => $this->getTitle(), + 'content'=> $this->getContent(), + 'id_webpage_family' => $this->getWebpageFamily()->getId(), + ]; + } +} diff --git a/server/src/Entity/WebpageFamily.php b/server/src/Entity/WebpageFamily.php new file mode 100644 index 0000000000000000000000000000000000000000..e12b58399ca774f5da5f0e344adffb1c4b432aa8 --- /dev/null +++ b/server/src/Entity/WebpageFamily.php @@ -0,0 +1,111 @@ +<?php + +/* + * This file is part of Anis Server. + * + * (c) Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace App\Entity; + +/** + * @author François Agneray <francois.agneray@lam.fr> + * @package App\Entity + * + * @Entity + * @Table(name="webpage_family") + */ +class WebpageFamily implements \JsonSerializable +{ + /** + * @var int + * + * @Id + * @Column(type="integer", nullable=false) + * @GeneratedValue + */ + protected $id; + + /** + * @var string + * + * @Column(type="string", nullable=false) + */ + protected $label; + + /** + * @var string + * + * @Column(type="string", nullable=true) + */ + protected $icon; + + /** + * @var int + * + * @Column(type="integer", nullable=false) + */ + protected $display; + + /** + * @var Instance + * + * @ManyToOne(targetEntity="Instance") + * @JoinColumn(name="instance_name", referencedColumnName="name", nullable=false, onDelete="CASCADE") + */ + protected $instance; + + public function __construct(Instance $instance) + { + $this->instance = $instance; + } + + public function getId() + { + return $this->id; + } + + public function getLabel() + { + return $this->label; + } + + public function setLabel($label) + { + $this->label = $label; + } + + public function getIcon() + { + return $this->icon; + } + + public function setIcon($icon) + { + $this->icon = $icon; + } + + public function getDisplay() + { + return $this->display; + } + + public function setDisplay($display) + { + $this->display = $display; + } + + public function jsonSerialize(): array + { + return [ + 'id' => $this->getId(), + 'label' => $this->getLabel(), + 'icon' => $this->getIcon(), + 'display' => $this->getDisplay() + ]; + } +}