diff --git a/src/app/components/creator/_dialogs/comment-export/comment-export.component.html b/src/app/components/creator/_dialogs/comment-export/comment-export.component.html new file mode 100644 index 0000000000000000000000000000000000000000..a09413055f3429983cd01e71ff72a4efb5d7515e --- /dev/null +++ b/src/app/components/creator/_dialogs/comment-export/comment-export.component.html @@ -0,0 +1,14 @@ +<mat-radio-group (change)="onChange($event)"> + <mat-radio-button value="json" #json checked (click)="comma.checked=true">JSON</mat-radio-button> + <mat-divider></mat-divider> + <mat-radio-button #csv value="csv">CSV</mat-radio-button> +</mat-radio-group> +<div id="csvBlock"> + <mat-hint>{{'comment-page.delimiter' | translate}}</mat-hint> + <mat-radio-group (change)="onChange($event)" id="csvOptions"> + <mat-radio-button value="comma" #comma checked>{{'comment-page.comma' | translate}}</mat-radio-button> + <mat-radio-button value="semicolon" #semicolon>{{'comment-page.semicolon' | translate}}</mat-radio-button> + </mat-radio-group> +</div> +<button mat-raised-button color="primary" (click)="onExport()">{{'comment-page.export' | translate}}</button> +<button mat-raised-button color="warn" (click)="onNoClick()">{{'comment-page.abort' | translate}}</button> diff --git a/src/app/components/creator/_dialogs/comment-export/comment-export.component.scss b/src/app/components/creator/_dialogs/comment-export/comment-export.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..43f4092051cd4a58789b613933a6cf49df12a190 --- /dev/null +++ b/src/app/components/creator/_dialogs/comment-export/comment-export.component.scss @@ -0,0 +1,26 @@ +mat-radio-button { + display: block; +} + +mat-divider { + margin: 30px 0 30px 0; +} + +* { + margin-bottom: 10px; +} + +button { + float:right !important; + margin: 0 0 0 10px; +} + +#csvBlock { + margin: 10px 0 20px 30px; + visibility: hidden; + transform: scale(0.9); +} + +#csvOptions mat-radio-button{ + margin-top:10px; +} \ No newline at end of file diff --git a/src/app/components/creator/_dialogs/comment-export/comment-export.component.spec.ts b/src/app/components/creator/_dialogs/comment-export/comment-export.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e0df83fd5c0e5afcf5d765ad18859ef705b0df9e --- /dev/null +++ b/src/app/components/creator/_dialogs/comment-export/comment-export.component.spec.ts @@ -0,0 +1,25 @@ +/** import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CommentExportComponent } from './comment-export.component'; + +describe('CommentExportComponent', () => { + let component: CommentExportComponent; + let fixture: ComponentFixture<CommentExportComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ CommentExportComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CommentExportComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); */ diff --git a/src/app/components/creator/_dialogs/comment-export/comment-export.component.ts b/src/app/components/creator/_dialogs/comment-export/comment-export.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..3cc2197f13d609c964cb93eb2624b1a36cc1ac81 --- /dev/null +++ b/src/app/components/creator/_dialogs/comment-export/comment-export.component.ts @@ -0,0 +1,109 @@ +import { Component, OnInit, EventEmitter } from '@angular/core'; +import { MatRadioChange, MatDialogRef } from '@angular/material'; +import { CommentCreatorPageComponent } from '../../comment-creator-page/comment-creator-page.component'; +import { CommentService } from '../../../../services/http/comment.service'; +import { Comment } from '../../../../models/comment'; + +@Component({ + selector: 'app-comment-export', + templateUrl: './comment-export.component.html', + styleUrls: ['./comment-export.component.scss'] +}) +export class CommentExportComponent implements OnInit { + change: EventEmitter<MatRadioChange>; + currentButton: string; + csvSelected: boolean; + comments: Comment[]; + roomId: string; + + constructor(public dialogRef: MatDialogRef<CommentCreatorPageComponent>, + private commentService: CommentService) { } + + ngOnInit() { + this.currentButton = 'json'; + this.roomId = localStorage.getItem(`roomId`); + this.getComments(); + } + + getComments(): void { + this.commentService.getComments(this.roomId) + .subscribe(comments => { + this.comments = comments; + }); + } + + onChange(change: MatRadioChange): string { + const csv = document.getElementById('csvBlock'); + if (change.value === 'json') { + csv.style.visibility = 'hidden'; + this.csvSelected = false; + } + if (change.value === 'csv') { + csv.style.visibility = 'visible'; + this.csvSelected = true; + } + return this.currentButton = change.value; + } + + onNoClick(): void { + this.dialogRef.close(); + } + + exportJson(date: string): void { + const jsonComments = JSON.parse(JSON.stringify(this.comments)); + jsonComments.forEach(element => { + delete element.id; + delete element.roomId; + delete element.creatorId; + element.body = element.body.replace(/[\r\n]/g, ' ').replace(/ +/g, ' '); + }); + const myBlob = new Blob([JSON.stringify(jsonComments, null, 2)], { type: 'application/json' }); + const link = document.createElement('a'); + const fileName = 'comments_' + date + '.json'; + link.setAttribute('download', fileName); + link.href = window.URL.createObjectURL(myBlob); + link.click(); + } + + exportCsv(delimiter: string, date: string): void { + let csv: string; + let keyFields = ''; + let valueFields = ''; + keyFields = Object.keys(this.comments[0]).slice(3).join(delimiter) + '\r\n'; + this.comments.forEach(element => { + element.body = '"' + element.body.replace(/[\r\n]/g, ' ').replace(/ +/g, ' ').replace(/"/g, '""') + '"'; + valueFields += Object.values(element).slice(3).join(delimiter) + '\r\n'; + }); + csv = keyFields + valueFields; + const myBlob = new Blob([csv], { type: 'text/csv' }); + const link = document.createElement('a'); + const fileName = 'comments_' + date + '.csv'; + link.setAttribute('download', fileName); + link.href = window.URL.createObjectURL(myBlob); + link.click(); + } + + onExport(): void { + const date = new Date(); + const dateString = date.getFullYear() + '_' + ('0' + (date.getMonth() + 1)).slice(-2) + '_' + ('0' + date.getDate()).slice(-2); + const timeString = ('0' + date.getHours()).slice(-2) + ('0' + date.getMinutes()).slice(-2) + ('0' + date.getSeconds()).slice(-2); + const timestamp = dateString + '_' + timeString; + if (this.currentButton === 'json') { + this.exportJson(timestamp); + this.onNoClick(); + } + if (this.csvSelected) { + if (this.currentButton === 'comma') { + this.exportCsv(',', timestamp); + this.onNoClick(); + } + if (this.currentButton === 'semicolon') { + this.exportCsv(';', timestamp); + this.onNoClick(); + } else { + this.exportCsv(',', timestamp); + this.onNoClick(); + } + } + } +} diff --git a/src/app/components/creator/comment-creator-page/comment-creator-page.component.ts b/src/app/components/creator/comment-creator-page/comment-creator-page.component.ts index eedc6d5e88e697a81659a3d3fd51b809158c445a..cdd887fc0273b06dc866ae8158bfacc0cfdba422 100644 --- a/src/app/components/creator/comment-creator-page/comment-creator-page.component.ts +++ b/src/app/components/creator/comment-creator-page/comment-creator-page.component.ts @@ -1,4 +1,7 @@ import { Component, OnInit } from '@angular/core'; +import { CommentService } from '../../../services/http/comment.service'; +import { MatDialog } from '@angular/material'; +import { CommentExportComponent } from '../_dialogs/comment-export/comment-export.component'; @Component({ selector: 'app-comment-creator-page', @@ -7,9 +10,24 @@ import { Component, OnInit } from '@angular/core'; }) export class CommentCreatorPageComponent implements OnInit { - constructor() { } + constructor(private commentService: CommentService, + public dialog: MatDialog, + ) { } ngOnInit() { + this.commentService.exportButton.subscribe(s => { + if (s === true) { + this.showExportDialog(); + } + }); } + showExportDialog(): void { + this.commentService.exportButtonClicked(false); + if (this.dialog.openDialogs.length === 0) { + this.dialog.open(CommentExportComponent, { + width: '400px', height: '300px', restoreFocus: false + }); + } + } } diff --git a/src/app/components/creator/creator.module.ts b/src/app/components/creator/creator.module.ts index e006cc6eb83ca13bf61b3e2d43bc3079f605a906..5707a9ddd352e3e396d204340ccf13bdbf7a0823 100644 --- a/src/app/components/creator/creator.module.ts +++ b/src/app/components/creator/creator.module.ts @@ -22,6 +22,7 @@ import { ContentListComponent } from './content-list/content-list.component'; import { ContentEditComponent } from './_dialogs/content-edit/content-edit.component'; import { ContentPresentationComponent } from './content-presentation/content-presentation.component'; import { CommentCreatorPageComponent } from './comment-creator-page/comment-creator-page.component'; +import { CommentExportComponent } from './_dialogs/comment-export/comment-export.component'; @NgModule({ imports: [ @@ -52,7 +53,8 @@ import { CommentCreatorPageComponent } from './comment-creator-page/comment-crea ContentListComponent, ContentEditComponent, ContentPresentationComponent, - CommentCreatorPageComponent + CommentCreatorPageComponent, + CommentExportComponent ], entryComponents: [ RoomDeleteComponent, @@ -63,7 +65,8 @@ import { CommentCreatorPageComponent } from './comment-creator-page/comment-crea ContentLikertCreatorComponent, ContentTextCreatorComponent, ContentYesNoCreatorComponent, - ContentEditComponent + ContentEditComponent, + CommentExportComponent ] }) export class CreatorModule { diff --git a/src/app/components/shared/comment-list/comment-list.component.html b/src/app/components/shared/comment-list/comment-list.component.html index e4a6a3c36323838dd28dbdccd60653b420055b4f..14ecfd1f61af68c8a83afa3fa4c862346cf9f524 100644 --- a/src/app/components/shared/comment-list/comment-list.component.html +++ b/src/app/components/shared/comment-list/comment-list.component.html @@ -3,17 +3,21 @@ <mat-icon class="search-icon">search</mat-icon> </mat-label> <input #searchBox placeholder="{{ 'comment-list-page.search-box-placeholder-text' | translate }}" - (input)="searchComments(searchBox.value)"> + (input)="searchComments(searchBox.value)"> <button mat-button *ngIf="searchBox.value" (click)="hideCommentsList=false; searchBox.value=''"> <mat-icon>close</mat-icon> </button> + <button mat-button *ngIf="!searchBox.value && userRole == '1' && comments.length > 0" + [matTooltip]="'Export comments'" (click)="export(true)"> + <mat-icon class="add-icon" id="export-icon">cloud_download</mat-icon> + </button> <button mat-button *ngIf="!searchBox.value" color="accent" (click)="openSubmitDialog()"> <mat-icon class="add-icon">add_circle</mat-icon> </button> </div> <mat-card class="outer-card" *ngIf="hideCommentsList"> - <app-comment *ngFor="let current of filteredComments" [comment]="current"> </app-comment> + <app-comment *ngFor="let current of filteredComments" [comment]="current"></app-comment> </mat-card> <mat-card class="outer-card" *ngIf="!hideCommentsList"> - <app-comment *ngFor="let current of comments | orderBy: 'score'" [comment]="current"> </app-comment> -</mat-card> + <app-comment *ngFor="let current of comments | orderBy: 'score'" [comment]="current"></app-comment> +</mat-card> \ No newline at end of file diff --git a/src/app/components/shared/comment-list/comment-list.component.scss b/src/app/components/shared/comment-list/comment-list.component.scss index 16594a1e77869d3b556f091ffb88022f5b01fe16..c1bb2f0550977f919c7035e001fb7edbf423f9d8 100644 --- a/src/app/components/shared/comment-list/comment-list.component.scss +++ b/src/app/components/shared/comment-list/comment-list.component.scss @@ -26,7 +26,6 @@ input { margin-bottom: 10px; } - .add-button { width: 44px!important; height: 44px!important; @@ -44,3 +43,6 @@ input { padding: 10px; } +#export-icon { + color: rgba(30,136,229,0.7) +} diff --git a/src/app/components/shared/comment-list/comment-list.component.ts b/src/app/components/shared/comment-list/comment-list.component.ts index c3f09fc261bf3155bcc85c27430b69251be88059..506856101a42c3d51e5055316f9205878c3f9dec 100644 --- a/src/app/components/shared/comment-list/comment-list.component.ts +++ b/src/app/components/shared/comment-list/comment-list.component.ts @@ -9,6 +9,8 @@ import { SubmitCommentComponent } from '../_dialogs/submit-comment/submit-commen import { MatDialog } from '@angular/material'; import { WsCommentServiceService } from '../../../services/websockets/ws-comment-service.service'; import { User } from '../../../models/user'; +import { UserRole } from '../../../models/user-roles.enum'; +import { AuthenticationService } from '../../../services/http/authentication.service'; @Component({ selector: 'app-comment-list', @@ -22,13 +24,15 @@ export class CommentListComponent implements OnInit { isLoading = true; hideCommentsList: boolean; filteredComments: Comment[]; + userRole: UserRole; constructor(private commentService: CommentService, - private translateService: TranslateService, - public dialog: MatDialog, - protected langService: LanguageService, - private rxStompService: RxStompService, - private wsCommentService: WsCommentServiceService) { + private translateService: TranslateService, + public dialog: MatDialog, + protected langService: LanguageService, + private rxStompService: RxStompService, + private wsCommentService: WsCommentServiceService, + private authenticationService: AuthenticationService) { langService.langEmitter.subscribe(lang => translateService.use(lang)); } @@ -41,6 +45,8 @@ export class CommentListComponent implements OnInit { }); this.getComments(); this.translateService.use(localStorage.getItem('currentLang')); + this.userRole = this.authenticationService.getRole(); + } getComments(): void { @@ -80,13 +86,13 @@ export class CommentListComponent implements OnInit { case 'read': this.comments[i].read = <boolean>value; break; - case 'correct' : + case 'correct': this.comments[i].correct = <boolean>value; break; - case 'favorite' : + case 'favorite': this.comments[i].favorite = <boolean>value; break; - case 'score' : + case 'score': this.comments[i].score = <number>value; break; } @@ -115,5 +121,8 @@ export class CommentListComponent implements OnInit { send(comment: Comment): void { this.wsCommentService.add(comment); } -} + export(clicked: boolean): void { + this.commentService.exportButtonClicked(clicked); + } +} diff --git a/src/app/components/shared/comment/comment.component.html b/src/app/components/shared/comment/comment.component.html index 39e66912fc7cf44080ea0a5e0ae73f6bde524f7c..0b6491b5a35ac089cd40b0778f214f7094370324 100644 --- a/src/app/components/shared/comment/comment.component.html +++ b/src/app/components/shared/comment/comment.component.html @@ -1,7 +1,7 @@ -<mat-card class="card-container"> - <div fxLayout="column"> - <div fxLayout="row"> - <span class="fill-remaining-space"></span> +<mat-card class="card-container" [@slide]> + <div fxLayout="column"> + <div fxLayout="row"> + <span class="fill-remaining-space"></span> <div id="date"> <div *ngIf="language === 'de'; else englishDate"> {{comment.timestamp | date: ' HH:mm:ss '}}Uhr,{{comment.timestamp | date: ' M.d.yy'}} @@ -15,24 +15,24 @@ </button> <button mat-icon-button *ngIf="comment.favorite || !isStudent" [disabled]="isStudent" (click)="setFavorite(comment)" [matTooltip]="comment.favorite ? 'Mark as not favorite' : 'Mark as favorite'"> <mat-icon [ngClass]="{true: 'favorite-icon', false: 'not-favorite-icon'}[comment.favorite]">favorite_border</mat-icon> - </button> - <button mat-icon-button [disabled]="isStudent" (click)="setRead(comment)" [matTooltip]="comment.read ? 'Mark as unread' : 'Mark as read'"> - <mat-icon class="icon" [ngClass]="{true: 'read-icon', false: 'unread-icon'}[comment.read]">visibility</mat-icon> - </button> - </div> - <div fxLayout="row"> - <div class="body" (click)="openPresentDialog(comment.body)">{{comment.body}}</div> - <span class="fill-remaining-space"></span> - <div fxLayout="column"> - <button mat-icon-button [disabled]="!isStudent" (click)="voteUp(comment)"> - <mat-icon class="voting-icon" [ngClass]="{'upvoted' : hasVoted === 1}">keyboard_arrow_up</mat-icon> </button> - <h2>{{comment.score}}</h2> - <button mat-icon-button [disabled]="!isStudent" (click)="voteDown(comment)"> - <mat-icon class="voting-icon" [ngClass]="{'downvoted' : hasVoted === -1}">keyboard_arrow_down</mat-icon> + <button mat-icon-button [disabled]="isStudent" (click)="setRead(comment)" [matTooltip]="comment.read ? 'Mark as unread' : 'Mark as read'"> + <mat-icon class="icon" [ngClass]="{true: 'read-icon', false: 'unread-icon'}[comment.read]">visibility</mat-icon> </button> </div> + <div fxLayout="row"> + <div class="body" (click)="openPresentDialog(comment.body)">{{comment.body}}</div> + <span class="fill-remaining-space"></span> + <div fxLayout="column" (tap)="startAnimation('rubberBand')" [@rubberBand]="animationState" (@rubberBand.done)="resetAnimationState()"> + <button mat-icon-button [disabled]="!isStudent" (click)="voteUp(comment)" > + <mat-icon class="voting-icon" [ngClass]="{'upvoted' : hasVoted === 1}">keyboard_arrow_up</mat-icon> + </button> + <h2>{{comment.score}}</h2> + <button mat-icon-button [disabled]="!isStudent" (click)="voteDown(comment)"> + <mat-icon class="voting-icon" [ngClass]="{'downvoted' : hasVoted === -1}">keyboard_arrow_down</mat-icon> + </button> + </div> + </div> </div> - </div> -</mat-card> + </mat-card> diff --git a/src/app/components/shared/comment/comment.component.scss b/src/app/components/shared/comment/comment.component.scss index 7f1cc0354558e24aab0766873ad613e18c9c2cd7..8084452349db7c8769e683d3adf51b6bc7523886 100644 --- a/src/app/components/shared/comment/comment.component.scss +++ b/src/app/components/shared/comment/comment.component.scss @@ -73,8 +73,7 @@ h2 { text-align: start; font-size: 140%; max-height: 120px; - overflow: hidden; - text-overflow: ellipsis; + overflow: auto; padding-left: 2%; padding-right: 2%; } diff --git a/src/app/components/shared/comment/comment.component.ts b/src/app/components/shared/comment/comment.component.ts index b7f91a90cb77c8ae60e18b17bd390b4947b1f7dc..fae83d243f619ddf089006796738f394f23125bd 100644 --- a/src/app/components/shared/comment/comment.component.ts +++ b/src/app/components/shared/comment/comment.component.ts @@ -10,28 +10,50 @@ import { LanguageService } from '../../../services/util/language.service'; import { WsCommentServiceService } from '../../../services/websockets/ws-comment-service.service'; import { PresentCommentComponent } from '../_dialogs/present-comment/present-comment.component'; import { MatDialog } from '@angular/material'; +import { trigger, transition, style, animate, state, keyframes } from '@angular/animations'; + +export const rubberBand = [ + style({ transform: 'scale3d(1, 1, 1)', offset: 0 }), + style({ transform: 'scale3d(1.05, 0.75, 1)', offset: 0.3 }), + style({ transform: 'scale3d(0.75, 1.05, 1)', offset: 0.4 }), + style({ transform: 'scale3d(1.05, 0.95, 1)', offset: 0.5 }), + style({ transform: 'scale3d(0.95, 1.05, 1)', offset: 0.65 }), + style({ transform: 'scale3d(1.05, 0.95, 1)', offset: 0.75 }), + style({ transform: 'scale3d(1, 1, 1)', offset: 1 }) +]; @Component({ selector: 'app-comment', templateUrl: './comment.component.html', - styleUrls: ['./comment.component.scss'] + styleUrls: ['./comment.component.scss'], + animations: [ + trigger('slide', [ + state('void', style({ opacity: 0, transform: 'translateY(-10px)' })), + transition('void <=> *', animate(700)), + ]), + trigger('rubberBand', [ + transition('* => rubberBand', animate(1000, keyframes(rubberBand))), + ]) + ] }) + export class CommentComponent implements OnInit { @Input() comment: Comment; isStudent = false; isLoading = true; hasVoted = 0; language: string; + animationState: string; constructor(protected authenticationService: AuthenticationService, - private route: ActivatedRoute, - private location: Location, - private commentService: CommentService, - private notification: NotificationService, - private translateService: TranslateService, - public dialog: MatDialog, - protected langService: LanguageService, - private wsCommentService: WsCommentServiceService) { + private route: ActivatedRoute, + private location: Location, + private commentService: CommentService, + private notification: NotificationService, + private translateService: TranslateService, + public dialog: MatDialog, + protected langService: LanguageService, + private wsCommentService: WsCommentServiceService) { langService.langEmitter.subscribe(lang => { translateService.use(lang); this.language = lang; @@ -46,6 +68,16 @@ export class CommentComponent implements OnInit { this.translateService.use(this.language); } + startAnimation(state_: any): void { + if (!this.animationState) { + this.animationState = state_; + } + } + + resetAnimationState(): void { + this.animationState = ''; + } + setRead(comment: Comment): void { this.comment = this.wsCommentService.toggleRead(comment); } diff --git a/src/app/services/http/comment.service.ts b/src/app/services/http/comment.service.ts index c3179ad69aad8f1c8811b5bdd8254d3294e47f31..f658bc23e6d4759567efb2ea2f37d6c89ffc0094 100644 --- a/src/app/services/http/comment.service.ts +++ b/src/app/services/http/comment.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import { Observable, Subject } from 'rxjs'; import { Comment } from '../../models/comment'; import { catchError, tap } from 'rxjs/operators'; import { BaseHttpService } from './base-http.service'; @@ -17,6 +17,8 @@ export class CommentService extends BaseHttpService { find: '/find' }; + exportButton = new Subject<boolean>(); + constructor(private http: HttpClient) { super(); } @@ -66,4 +68,8 @@ export class CommentService extends BaseHttpService { catchError(this.handleError<any>('updateComment')) ); } + + exportButtonClicked(state: boolean): void { + this.exportButton.next(state); + } } diff --git a/src/assets/i18n/creator/de.json b/src/assets/i18n/creator/de.json index 7905b62d590b76d39f19381cc7ea6ff4e9b8de57..41537e07d379a26857f2fa9b4c047127a0cafb45 100644 --- a/src/assets/i18n/creator/de.json +++ b/src/assets/i18n/creator/de.json @@ -82,7 +82,11 @@ "abort": "Abbrechen", "error-comment": "Bitte geben Sie ein Kommentar ein.", "error-title": "Bitte geben Sie einen Titel ein.", - "error-both-fields": "Bitte füllen Sie alle Felder aus." + "error-both-fields": "Bitte füllen Sie alle Felder aus.", + "delimiter": "Bitte wählen Sie ein Trennzeichen:", + "comma": "Komma", + "semicolon": "Semikolon", + "export": "Exportieren" }, "comment-list-page": { "search-box-placeholder-text": "Kommentare durchsuchen", diff --git a/src/assets/i18n/creator/en.json b/src/assets/i18n/creator/en.json index 1a5d6cd7debcc12cec49cc6c288500de1d8a5639..9a64abd7d01fe37e13a49601d1a483fbde7e3f24 100644 --- a/src/assets/i18n/creator/en.json +++ b/src/assets/i18n/creator/en.json @@ -82,7 +82,11 @@ "abort": "Cancel", "error-title": "Please enter a title.", "error-comment": "Please enter a comment.", - "error-both-fields": "Please fill in all fields." + "error-both-fields": "Please fill in all fields.", + "delimiter": "Please select a delimiter:", + "comma": "Comma", + "semicolon": "Semicolon", + "export": "Export" }, "comment-list-page": { "search-box-placeholder-text": "Search in comments",