From 9018795e6e792f8cec3cfd70a00e8b25ac71e328 Mon Sep 17 00:00:00 2001 From: George Kalampokis Date: Thu, 13 Feb 2020 18:15:34 +0200 Subject: [PATCH] Add User Guide Editor (ref #239) --- .../controllers/UserGuideController.java | 22 ++++- .../models/data/userguide/UserGuide.java | 23 +++++ dmp-frontend/angular.json | 17 +++- dmp-frontend/package.json | 2 + dmp-frontend/src/app/app-routing.module.ts | 8 ++ .../services/user-guide/user-guide.service.ts | 4 + .../src/app/ui/sidebar/sidebar.component.ts | 3 +- .../user-guide-editor.component.html | 30 ++++++ .../user-guide-editor.component.scss | 13 +++ .../user-guide-editor.component.ts | 91 +++++++++++++++++++ .../user-guide-editor.module.ts | 18 ++++ .../user-guide-editor.routing.ts | 15 +++ dmp-frontend/src/assets/i18n/en.json | 6 +- dmp-frontend/src/assets/i18n/es.json | 6 +- dmp-frontend/src/environments/environment.ts | 1 + 15 files changed, 246 insertions(+), 13 deletions(-) create mode 100644 dmp-backend/web/src/main/java/eu/eudat/models/data/userguide/UserGuide.java create mode 100644 dmp-frontend/src/app/ui/user-guide-editor/user-guide-editor.component.html create mode 100644 dmp-frontend/src/app/ui/user-guide-editor/user-guide-editor.component.scss create mode 100644 dmp-frontend/src/app/ui/user-guide-editor/user-guide-editor.component.ts create mode 100644 dmp-frontend/src/app/ui/user-guide-editor/user-guide-editor.module.ts create mode 100644 dmp-frontend/src/app/ui/user-guide-editor/user-guide-editor.routing.ts diff --git a/dmp-backend/web/src/main/java/eu/eudat/controllers/UserGuideController.java b/dmp-backend/web/src/main/java/eu/eudat/controllers/UserGuideController.java index e6a5b7c50..102bd66c2 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/controllers/UserGuideController.java +++ b/dmp-backend/web/src/main/java/eu/eudat/controllers/UserGuideController.java @@ -1,5 +1,8 @@ package eu.eudat.controllers; +import eu.eudat.models.data.helpers.responses.ResponseItem; +import eu.eudat.models.data.userguide.UserGuide; +import eu.eudat.types.ApiMessageCode; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import org.springframework.http.HttpHeaders; @@ -8,9 +11,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -39,10 +40,13 @@ public class UserGuideController { String fileName = result.get(0); InputStream is = new FileInputStream(fileName); + String[] filepath = fileName.split("\\.")[0].split("\\\\"); + String simplename = filepath[filepath.length - 1]; + HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.setContentLength(is.available()); responseHeaders.setContentType(MediaType.TEXT_HTML); - responseHeaders.set("Content-Disposition", "attachment;filename=" + fileName); + responseHeaders.set("Content-Disposition", "attachment;filename=" + simplename); responseHeaders.set("Access-Control-Expose-Headers", "Content-Disposition"); responseHeaders.get("Access-Control-Expose-Headers").add("Content-Type"); @@ -53,4 +57,14 @@ public class UserGuideController { return new ResponseEntity<>(content, responseHeaders, HttpStatus.OK); } + + @RequestMapping(value = "current", method = RequestMethod.POST) + public @ResponseBody + ResponseEntity> updateGuide(@RequestBody UserGuide guide) throws Exception { + String fileName = this.environment.getProperty("userguide.path") + guide.getName() + ".html"; + OutputStream os = new FileOutputStream(fileName); + os.write(guide.getHtml().getBytes()); + os.close(); + return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem().status(ApiMessageCode.SUCCESS_MESSAGE).message("Updated").payload("Updated")); + } } diff --git a/dmp-backend/web/src/main/java/eu/eudat/models/data/userguide/UserGuide.java b/dmp-backend/web/src/main/java/eu/eudat/models/data/userguide/UserGuide.java new file mode 100644 index 000000000..019bcb15e --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/models/data/userguide/UserGuide.java @@ -0,0 +1,23 @@ +package eu.eudat.models.data.userguide; + +public class UserGuide { + + private String name; + private String html; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getHtml() { + return html; + } + + public void setHtml(String html) { + this.html = html; + } +} diff --git a/dmp-frontend/angular.json b/dmp-frontend/angular.json index 6de6bf84c..e3a5fc57b 100644 --- a/dmp-frontend/angular.json +++ b/dmp-frontend/angular.json @@ -18,7 +18,10 @@ "polyfills": "src/polyfills.ts", "assets": [ "src/assets", - "src/favicon.ico" + "src/favicon.ico", + { "glob": "**/*", "input": "node_modules/tinymce/skins", "output": "/tinymce/skins/" }, + { "glob": "**/*", "input": "node_modules/tinymce/themes", "output": "/tinymce/themes/" }, + { "glob": "**/*", "input": "node_modules/tinymce/plugins", "output": "/tinymce/plugins/" } ], "styles": [ "src/styles.scss", @@ -27,7 +30,9 @@ "node_modules/cookieconsent/build/cookieconsent.min.css" ], "scripts": [ - "node_modules/cookieconsent/build/cookieconsent.min.js" + "node_modules/cookieconsent/build/cookieconsent.min.js", + "node_modules/tinymce/tinymce.min.js" + ] }, "configurations": { @@ -95,11 +100,15 @@ "styles": [ "src/styles.scss", "src/assets/scss/material-dashboard.scss", - "src/assets/css/demo.css" + "src/assets/css/demo.css", + "node_modules/tinymce/tinymce.min.js" ], "assets": [ "src/assets", - "src/favicon.ico" + "src/favicon.ico", + { "glob": "**/*", "input": "node_modules/tinymce/skins", "output": "/tinymce/skins/" }, + { "glob": "**/*", "input": "node_modules/tinymce/themes", "output": "/tinymce/themes/" }, + { "glob": "**/*", "input": "node_modules/tinymce/plugins", "output": "/tinymce/plugins/" } ] } }, diff --git a/dmp-frontend/package.json b/dmp-frontend/package.json index cc6e1ab8a..38701dc4c 100644 --- a/dmp-frontend/package.json +++ b/dmp-frontend/package.json @@ -22,6 +22,7 @@ "@ngx-translate/core": "^11.0.1", "@ngx-translate/http-loader": "^4.0.0", "@swimlane/ngx-datatable": "^16.0.2", + "@tinymce/tinymce-angular": "^3.4.0", "@w11k/angular-sticky-things": "^1.1.2", "bootstrap": "^4.3.1", "cookieconsent": "^3.1.1", @@ -32,6 +33,7 @@ "ngx-cookie-service": "^2.2.0", "ngx-cookieconsent": "^2.2.3", "rxjs": "^6.3.2", + "tinymce": "^5.1.6", "tslib": "^1.10.0", "web-animations-js": "^2.3.2", "zone.js": "~0.9.1" diff --git a/dmp-frontend/src/app/app-routing.module.ts b/dmp-frontend/src/app/app-routing.module.ts index 85b0655aa..db7a467c4 100644 --- a/dmp-frontend/src/app/app-routing.module.ts +++ b/dmp-frontend/src/app/app-routing.module.ts @@ -180,6 +180,14 @@ const appRoutes: Routes = [ title: 'GENERAL.TITLES.LANGUAGE-EDITOR' }, }, + { + path: 'user-guide-editor', + loadChildren: () => import('./ui/user-guide-editor/user-guide-editor.module').then(m => m.UserGuideEditorModule), + data: { + breadcrumb: true, + title: 'GENERAL.TITLES.GUIDE-EDITOR' + }, + }, { path: 'login/admin', loadChildren: () => import('./ui/auth/admin-login/admin-login.module').then(m => m.AdminLoginModule), diff --git a/dmp-frontend/src/app/core/services/user-guide/user-guide.service.ts b/dmp-frontend/src/app/core/services/user-guide/user-guide.service.ts index 7eaddb288..4a4133c10 100644 --- a/dmp-frontend/src/app/core/services/user-guide/user-guide.service.ts +++ b/dmp-frontend/src/app/core/services/user-guide/user-guide.service.ts @@ -22,4 +22,8 @@ export class UserGuideService { 'Access-Control-Allow-Credentials': 'true'} }); } + public updateUserGuide(data: any): Observable { + return this.http.post(`${this.userGuideUrl}/current`, data); + } + } diff --git a/dmp-frontend/src/app/ui/sidebar/sidebar.component.ts b/dmp-frontend/src/app/ui/sidebar/sidebar.component.ts index 5b7036ca0..aa0acf9ba 100644 --- a/dmp-frontend/src/app/ui/sidebar/sidebar.component.ts +++ b/dmp-frontend/src/app/ui/sidebar/sidebar.component.ts @@ -51,7 +51,8 @@ export const ADMIN_ROUTES: RouteInfo[] = [ { path: '/dmp-profiles', title: 'SIDE-BAR.DMP-TEMPLATES', icon: 'library_books' }, { path: '/dataset-profiles', title: 'SIDE-BAR.DATASET-TEMPLATES', icon: 'library_books' }, { path: '/users', title: 'SIDE-BAR.USERS', icon: 'people' }, - { path: '/language-editor', title: 'SIDE-BAR.LANGUAGE-EDITOR', icon: 'language'} + { path: '/language-editor', title: 'SIDE-BAR.LANGUAGE-EDITOR', icon: 'language'}, + { path: '/user-guide-editor', title: 'SIDE-BAR.GUIDE-EDITOR', icon: 'import_contacts'} ]; // export const HISTORY_ROUTES: RouteInfo[] = [ // { path: '/typography', title: 'SIDE-BAR.HISTORY-VISITED', icon: 'visibility'}, diff --git a/dmp-frontend/src/app/ui/user-guide-editor/user-guide-editor.component.html b/dmp-frontend/src/app/ui/user-guide-editor/user-guide-editor.component.html new file mode 100644 index 000000000..dfee70317 --- /dev/null +++ b/dmp-frontend/src/app/ui/user-guide-editor/user-guide-editor.component.html @@ -0,0 +1,30 @@ +
+
+
+ +
{{'GUIDE.TITLE' | translate}}
+ + + +
+ +
+ +
+
diff --git a/dmp-frontend/src/app/ui/user-guide-editor/user-guide-editor.component.scss b/dmp-frontend/src/app/ui/user-guide-editor/user-guide-editor.component.scss new file mode 100644 index 000000000..341b95343 --- /dev/null +++ b/dmp-frontend/src/app/ui/user-guide-editor/user-guide-editor.component.scss @@ -0,0 +1,13 @@ +.user-guide-editor { + padding-top: 5em; + padding-bottom: 2em; + + .save-btn { + padding-top: inherit !important; + top: auto !important; + width: 56px !important; + bottom: 10px; + position: fixed; + right: 10px; + } +} diff --git a/dmp-frontend/src/app/ui/user-guide-editor/user-guide-editor.component.ts b/dmp-frontend/src/app/ui/user-guide-editor/user-guide-editor.component.ts new file mode 100644 index 000000000..c840ef808 --- /dev/null +++ b/dmp-frontend/src/app/ui/user-guide-editor/user-guide-editor.component.ts @@ -0,0 +1,91 @@ +import { Component, OnInit } from '@angular/core'; +import { FormGroup, FormBuilder } from '@angular/forms'; +import { BaseComponent } from '@common/base/base.component'; +import { UserGuideService } from '@app/core/services/user-guide/user-guide.service'; +import { takeUntil } from 'rxjs/operators'; +import { UiNotificationService, SnackBarNotificationLevel } from '@app/core/services/notification/ui-notification-service'; +import { TranslateService } from '@ngx-translate/core'; +import { Router } from '@angular/router'; +import { isNullOrUndefined } from 'util'; +import { environment } from 'environments/environment'; + +@Component({ + selector: 'app-user-guide-editor', + templateUrl: './user-guide-editor.component.html', + styleUrls: ['./user-guide-editor.component.scss'] +}) +export class UserGuideEditorComponent extends BaseComponent implements OnInit { + public formGroup: FormGroup; + private formBuilder: FormBuilder; + + constructor(private userGuideService: UserGuideService, + private uiNotificationService: UiNotificationService, + private translate: TranslateService, + private router: Router, + ) { super(); } + + ngOnInit() { + this.formBuilder = new FormBuilder(); + this.formGroup = this.formBuilder.group({ + name: [''], + html: [''] + }); + this.userGuideService.getUserGuide().pipe(takeUntil(this._destroyed)).subscribe(data => { + const contentDispositionHeader = data.headers.get('Content-Disposition'); + const filename = contentDispositionHeader.split(';')[1].trim().split('=')[1].replace(/"/g, ''); + + this.formGroup.get('name').patchValue(filename); + this.loadFile(data.body); + }); + } + + private parseText(source: string): string { + source = source.replace(/src="images/g, `src="${environment.guideAssets}`); + source = source.replace(/\r\n +>/g, '>\r\n'); + const brokenElements = Array.from(new Set(source.match(/<\/\w+\d* >/g))); + if (!isNullOrUndefined(brokenElements)) { + brokenElements.forEach((brokenElement) => { + const tempvalue = brokenElement.match(/\/\w+\d*/)[0]; + source = source.replace(new RegExp(brokenElement, 'g'), `<${tempvalue}>\r\n`); + }); + } + return source; + } + + loadFile(data: Blob) { + const reader = new FileReader(); + reader.addEventListener('load', () => { + let result = this.parseText(reader.result as string); + result = result.replace(/class="href" path="/g, 'href="#'); + this.formGroup.get('html').patchValue(result); + }, false); + reader.readAsText(data); + } + + submit() { + let result = this.parseText(this.formGroup.get('html').value); + result = result.replace(/href="#/g, 'class="href" path="'); + this.formGroup.get('html').patchValue(result); + this.userGuideService.updateUserGuide(this.formGroup.value).pipe(takeUntil(this._destroyed)) + .subscribe( + complete => { + this.onCallbackSuccess(complete); + }, + error => { + this.onCallbackError(error); + } + ); + +} + +onCallbackSuccess(id?: String): void { + this.uiNotificationService.snackBarNotification( this.translate.instant('GENERAL.SNACK-BAR.SUCCESSFUL-UPDATE'), SnackBarNotificationLevel.Success); + this.router.navigate(['/reload']).then(() => this.router.navigate(['/user-guide-editor'])); +} + +onCallbackError(error: any) { + this.uiNotificationService.snackBarNotification( error, SnackBarNotificationLevel.Error); + //this.validateAllFormFields(this.formGroup); +} + +} diff --git a/dmp-frontend/src/app/ui/user-guide-editor/user-guide-editor.module.ts b/dmp-frontend/src/app/ui/user-guide-editor/user-guide-editor.module.ts new file mode 100644 index 000000000..556b481de --- /dev/null +++ b/dmp-frontend/src/app/ui/user-guide-editor/user-guide-editor.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { EditorModule } from '@tinymce/tinymce-angular'; +import { UserGuideEditorRoutingModule } from './user-guide-editor.routing'; +import { UserGuideEditorComponent } from './user-guide-editor.component'; +import { CommonUiModule } from '@common/ui/common-ui.module'; +import { CommonFormsModule } from '@common/forms/common-forms.module'; + + +@NgModule({ + declarations: [UserGuideEditorComponent], + imports: [ + CommonUiModule, + CommonFormsModule, + UserGuideEditorRoutingModule, + EditorModule + ] +}) +export class UserGuideEditorModule { } diff --git a/dmp-frontend/src/app/ui/user-guide-editor/user-guide-editor.routing.ts b/dmp-frontend/src/app/ui/user-guide-editor/user-guide-editor.routing.ts new file mode 100644 index 000000000..52817c887 --- /dev/null +++ b/dmp-frontend/src/app/ui/user-guide-editor/user-guide-editor.routing.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; +import { UserGuideEditorComponent } from './user-guide-editor.component'; +import { AdminAuthGuard } from '@app/core/admin-auth-guard.service'; + + +const routes: Routes = [ + { path: '', component: UserGuideEditorComponent, canActivate: [AdminAuthGuard] }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class UserGuideEditorRoutingModule { } diff --git a/dmp-frontend/src/assets/i18n/en.json b/dmp-frontend/src/assets/i18n/en.json index 9d3e44658..fb8e4d94d 100644 --- a/dmp-frontend/src/assets/i18n/en.json +++ b/dmp-frontend/src/assets/i18n/en.json @@ -120,7 +120,8 @@ "DATASET-UPDATE": "Update Dataset Description", "DATASET-PROFILES-NEW-VERSION": "New Version of Dataset Description Template", "DATASET-PROFILES-CLONE": "New Clone of Dataset Description Template", - "LANGUAGE-EDITOR": "Language Editor" + "LANGUAGE-EDITOR": "Language Editor", + "GUIDE-EDITOR": "User Guide Editor" }, "FILE-TYPES": { "PDF": "PDF", @@ -215,7 +216,8 @@ "DATASET-TEMPLATES": "Dataset Description Templates", "DMP-TEMPLATES": "DMP Templates", "USERS": "Users", - "LANGUAGE-EDITOR":"Language Editor" + "LANGUAGE-EDITOR":"Language Editor", + "GUIDE-EDITOR": "User Guide Editor" }, "DATASET-PROFILE-EDITOR": { "TITLE": { diff --git a/dmp-frontend/src/assets/i18n/es.json b/dmp-frontend/src/assets/i18n/es.json index fcf309b64..e66bdd35a 100644 --- a/dmp-frontend/src/assets/i18n/es.json +++ b/dmp-frontend/src/assets/i18n/es.json @@ -120,7 +120,8 @@ "DATASET-UPDATE": "Update Dataset Description", "DATASET-PROFILES-NEW-VERSION": "New Version of Dataset Description Template", "DATASET-PROFILES-CLONE": "New Clone of Dataset Description Template", - "LANGUAGE-EDITOR": "Language Editor" + "LANGUAGE-EDITOR": "Language Editor", + "GUIDE-EDITOR": "User Guide Editor" }, "FILE-TYPES": { "PDF": "PDF", @@ -215,7 +216,8 @@ "DATASET-TEMPLATES": "Dataset Description Templates", "DMP-TEMPLATES": "DMP Templates", "USERS": "Users", - "LANGUAGE-EDITOR":"Language Editor" + "LANGUAGE-EDITOR":"Language Editor", + "GUIDE-EDITOR": "User Guide Editor" }, "DATASET-PROFILE-EDITOR": { "TITLE": { diff --git a/dmp-frontend/src/environments/environment.ts b/dmp-frontend/src/environments/environment.ts index b5d47a815..ab3e5f23b 100644 --- a/dmp-frontend/src/environments/environment.ts +++ b/dmp-frontend/src/environments/environment.ts @@ -44,4 +44,5 @@ export const environment = { logLevels: ["debug", "info", "warning", "error"] }, lockInterval: 60000, + guideAssets: "assets/images/guide" };