From b63ac73677a2727ce7d3987841e77cdd408b7d52 Mon Sep 17 00:00:00 2001 From: Paul Rangger <48455539+PaRangger@users.noreply.github.com> Date: Sun, 3 Nov 2024 20:11:40 +0100 Subject: [PATCH] Communication: Add undo button when deleting posts (#9624) --- .../answer-post/answer-post.component.html | 25 +++--- .../answer-post/answer-post.component.scss | 2 + .../answer-post/answer-post.component.ts | 8 ++ .../app/shared/metis/metis.component.scss | 31 +++++++ .../app/shared/metis/post/post.component.html | 41 ++++++---- .../app/shared/metis/post/post.component.ts | 24 ++---- .../posting-content.component.html | 10 ++- .../posting-content.components.ts | 5 +- .../post-footer/post-footer.component.html | 2 +- .../post-footer/post-footer.component.ts | 4 + .../answer-post-header.component.html | 80 +++++++++---------- .../answer-post-header.component.ts | 2 +- .../post-header/post-header.component.html | 45 ++++++----- .../post-header/post-header.component.ts | 2 +- .../posting-header.directive.ts | 5 +- .../app/shared/metis/posting.directive.ts | 58 +++++++++++++- .../profile-picture.component.html | 6 +- .../profile-picture.component.scss | 6 +- .../profile-picture.component.ts | 1 + src/main/webapp/i18n/de/metis.json | 4 +- src/main/webapp/i18n/en/metis.json | 4 +- .../discussion-section.component.spec.ts | 3 +- .../answer-post/answer-post.component.spec.ts | 12 +-- .../shared/metis/post/post.component.spec.ts | 49 +++++++++++- .../posting-content.component.spec.ts | 18 ++++- .../answer-post-header.component.spec.ts | 21 ++--- .../post-header/post-header.component.spec.ts | 19 +++-- 27 files changed, 334 insertions(+), 153 deletions(-) diff --git a/src/main/webapp/app/shared/metis/answer-post/answer-post.component.html b/src/main/webapp/app/shared/metis/answer-post/answer-post.component.html index 52b4aebd1e29..33641bc8ad2b 100644 --- a/src/main/webapp/app/shared/metis/answer-post/answer-post.component.html +++ b/src/main/webapp/app/shared/metis/answer-post/answer-post.component.html @@ -6,6 +6,8 @@ [isCommunicationPage]="isCommunicationPage" [lastReadDate]="lastReadDate" [hasChannelModerationRights]="hasChannelModerationRights" + [isDeleted]="isDeleted" + (isDeleteEvent)="onDeleteEvent(true)" /> @if (!createAnswerPostModal.isInputOpen) {
@@ -15,6 +17,9 @@ [author]="posting.author" [posting]="posting" [isReply]="true" + [isDeleted]="isDeleted" + [deleteTimerInSeconds]="deleteTimerInSeconds" + (onUndoDeleteEvent)="onDeleteEvent(false)" (userReferenceClicked)="userReferenceClicked.emit($event)" (channelReferenceClicked)="channelReferenceClicked.emit($event)" /> @@ -23,14 +28,16 @@
-
- -
+ @if (!isDeleted) { +
+ +
+ }
diff --git a/src/main/webapp/app/shared/metis/answer-post/answer-post.component.scss b/src/main/webapp/app/shared/metis/answer-post/answer-post.component.scss index 4b340b2ecc95..2289b1aa893c 100644 --- a/src/main/webapp/app/shared/metis/answer-post/answer-post.component.scss +++ b/src/main/webapp/app/shared/metis/answer-post/answer-post.component.scss @@ -2,6 +2,8 @@ @import 'bootstrap/scss/variables'; @import 'bootstrap/scss/mixins'; +@import 'src/main/webapp/app/shared/metis/metis.component'; + .answer-post { background-color: var(--metis-answer-post-background-color); border-radius: 7px; diff --git a/src/main/webapp/app/shared/metis/answer-post/answer-post.component.ts b/src/main/webapp/app/shared/metis/answer-post/answer-post.component.ts index 442ddc748082..177791de4758 100644 --- a/src/main/webapp/app/shared/metis/answer-post/answer-post.component.ts +++ b/src/main/webapp/app/shared/metis/answer-post/answer-post.component.ts @@ -2,12 +2,19 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ViewCh import { AnswerPost } from 'app/entities/metis/answer-post.model'; import { PostingDirective } from 'app/shared/metis/posting.directive'; import dayjs from 'dayjs/esm'; +import { animate, style, transition, trigger } from '@angular/animations'; @Component({ selector: 'jhi-answer-post', templateUrl: './answer-post.component.html', styleUrls: ['./answer-post.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + trigger('fade', [ + transition(':enter', [style({ opacity: 0 }), animate('300ms ease-in', style({ opacity: 1 }))]), + transition(':leave', [animate('300ms ease-out', style({ opacity: 0 }))]), + ]), + ], }) export class AnswerPostComponent extends PostingDirective { @Input() lastReadDate?: dayjs.Dayjs; @@ -15,6 +22,7 @@ export class AnswerPostComponent extends PostingDirective { @Output() openPostingCreateEditModal = new EventEmitter(); @Output() userReferenceClicked = new EventEmitter(); @Output() channelReferenceClicked = new EventEmitter(); + isAnswerPost = true; @Input() isReadOnlyMode = false; diff --git a/src/main/webapp/app/shared/metis/metis.component.scss b/src/main/webapp/app/shared/metis/metis.component.scss index 019ecf67506d..f17e5cb1389c 100644 --- a/src/main/webapp/app/shared/metis/metis.component.scss +++ b/src/main/webapp/app/shared/metis/metis.component.scss @@ -2,6 +2,8 @@ @import 'bootstrap/scss/variables'; @import 'bootstrap/scss/mixins'; +$delete-delay-duration: 6s; + .post-result-information { font-size: small; font-style: italic; @@ -98,3 +100,32 @@ font-size: 0.75rem !important; } } + +.post-delete-button-background { + position: absolute; + top: 0; + left: 0; + bottom: 0; + z-index: 0; + background-color: rgba($primary, 0.3); + animation: increaseWidth $delete-delay-duration forwards linear; + + @media (prefers-reduced-motion) { + animation: none; + } +} + +.post-delete-button-label { + position: relative; + z-index: 1; +} + +@keyframes increaseWidth { + from { + width: 0; + } + + to { + width: 100%; + } +} diff --git a/src/main/webapp/app/shared/metis/post/post.component.html b/src/main/webapp/app/shared/metis/post/post.component.html index 53d03e33673b..ca84c6b6580d 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.html +++ b/src/main/webapp/app/shared/metis/post/post.component.html @@ -4,10 +4,12 @@ [previewMode]="previewMode" [readOnlyMode]="readOnlyMode" [posting]="posting" + [isDeleted]="isDeleted" [isCommunicationPage]="isCommunicationPage" [hasChannelModerationRights]="hasChannelModerationRights" (isModalOpen)="displayInlineInput = true" [lastReadDate]="lastReadDate" + (isDeleteEvent)="onDeleteEvent(true)" />
@@ -57,33 +59,38 @@ [isEdited]="!!posting.updatedDate" [posting]="posting" [isReply]="false" + [isDeleted]="isDeleted" + [deleteTimerInSeconds]="deleteTimerInSeconds" + (onUndoDeleteEvent)="onDeleteEvent(false)" (userReferenceClicked)="onUserReferenceClicked($event)" (channelReferenceClicked)="onChannelReferenceClicked($event)" /> }
- @if (displayInlineInput && !readOnlyMode) { + @if (!isDeleted && displayInlineInput && !readOnlyMode) {
} -
- - @if (!previewMode) { - - } -
+ @if (!isDeleted) { +
+ + @if (!previewMode) { + + } +
+ } implements OnInit, OnChanges, AfterContentChecked { @Input() lastReadDate?: dayjs.Dayjs; @@ -64,8 +58,6 @@ export class PostComponent extends PostingDirective implements OnInit, OnC faCheckSquare = faCheckSquare; constructor( - private metisService: MetisService, - protected changeDetector: ChangeDetectorRef, private oneToOneChatService: OneToOneChatService, private metisConversationService: MetisConversationService, private router: Router, diff --git a/src/main/webapp/app/shared/metis/posting-content/posting-content.component.html b/src/main/webapp/app/shared/metis/posting-content/posting-content.component.html index 6500e1b28bbf..86d8860b100d 100644 --- a/src/main/webapp/app/shared/metis/posting-content/posting-content.component.html +++ b/src/main/webapp/app/shared/metis/posting-content/posting-content.component.html @@ -1,4 +1,12 @@ -@if (currentlyLoadedPosts) { +@if (isDeleted()) { + + + + +} @else if (currentlyLoadedPosts) { @if (previewMode) {
diff --git a/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts b/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts index d7f4b4a42f19..45b92cb0bdde 100644 --- a/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts +++ b/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, input, output, signal } from '@angular/core'; import { Params } from '@angular/router'; import { faAngleDown, faAngleUp } from '@fortawesome/free-solid-svg-icons'; import { Post } from 'app/entities/metis/post.model'; @@ -24,6 +24,9 @@ export class PostingContentComponent implements OnInit, OnChanges, OnDestroy { @Input() isReply?: boolean; @Output() userReferenceClicked = new EventEmitter(); @Output() channelReferenceClicked = new EventEmitter(); + isDeleted = input(false); + deleteTimerInSeconds = input(0); + onUndoDeleteEvent = output(); showContent = false; currentlyLoadedPosts: Post[]; diff --git a/src/main/webapp/app/shared/metis/posting-footer/post-footer/post-footer.component.html b/src/main/webapp/app/shared/metis/posting-footer/post-footer/post-footer.component.html index dcd03b7d6be1..c04bbd8392e7 100644 --- a/src/main/webapp/app/shared/metis/posting-footer/post-footer/post-footer.component.html +++ b/src/main/webapp/app/shared/metis/posting-footer/post-footer/post-footer.component.html @@ -12,7 +12,7 @@
- @for (answerPost of sortedAnswerPosts; track $index; let isLastAnswer = $last) { + @for (answerPost of sortedAnswerPosts; track postsTrackByFn($index, answerPost); let isLastAnswer = $last) { implements openCreateAnswerPostModal() { this.createAnswerPostModalComponent.open(); } + + protected postsTrackByFn(index: number, post: Post): number { + return post.id!; + } } diff --git a/src/main/webapp/app/shared/metis/posting-header/answer-post-header/answer-post-header.component.html b/src/main/webapp/app/shared/metis/posting-header/answer-post-header/answer-post-header.component.html index ca36ef227e51..7ff859bcdbd7 100644 --- a/src/main/webapp/app/shared/metis/posting-header/answer-post-header/answer-post-header.component.html +++ b/src/main/webapp/app/shared/metis/posting-header/answer-post-header/answer-post-header.component.html @@ -11,6 +11,7 @@ [authorName]="posting.author?.name" [imageUrl]="posting.author?.imageUrl" [isEditable]="currentUser !== undefined && posting.author.id === currentUser.id" + [isGray]="isDeleted()" > @@ -37,48 +38,47 @@ } - -
- @if (mayEditOrDelete) { - - } - @if (mayEditOrDelete) { - - } - @if (!isAnswerOfAnnouncement) { -
- @if (posting.resolvesPost) { -
- -
- } @else { - @if (isAtLeastTutorInCourse || isAuthorOfOriginalPost) { + @if (!isDeleted()) { +
+ @if (mayEditOrDelete) { + + } + @if (mayEditOrDelete) { + + } + @if (!isAnswerOfAnnouncement) { +
+ @if (posting.resolvesPost) { +
+ +
+ } @else if (isAtLeastTutorInCourse || isAuthorOfOriginalPost) {
} - } -
- } -
+
+ } +
+ } diff --git a/src/main/webapp/app/shared/metis/posting-header/answer-post-header/answer-post-header.component.ts b/src/main/webapp/app/shared/metis/posting-header/answer-post-header/answer-post-header.component.ts index a48dc5137678..1d70cdd34bc6 100644 --- a/src/main/webapp/app/shared/metis/posting-header/answer-post-header/answer-post-header.component.ts +++ b/src/main/webapp/app/shared/metis/posting-header/answer-post-header/answer-post-header.component.ts @@ -50,7 +50,7 @@ export class AnswerPostHeaderComponent extends PostingHeaderDirective
} -
- @if (mayEditOrDelete) { - - } - - @if (mayEditOrDelete) { - - } -
+ @if (!isDeleted()) { +
+ @if (mayEditOrDelete) { + + } + + @if (mayEditOrDelete) { + + } +
+ } diff --git a/src/main/webapp/app/shared/metis/posting-header/post-header/post-header.component.ts b/src/main/webapp/app/shared/metis/posting-header/post-header/post-header.component.ts index b0a52e3b88bc..cc05f6ed7b29 100644 --- a/src/main/webapp/app/shared/metis/posting-header/post-header/post-header.component.ts +++ b/src/main/webapp/app/shared/metis/posting-header/post-header/post-header.component.ts @@ -60,7 +60,7 @@ export class PostHeaderComponent extends PostingHeaderDirective implements * invokes the metis service to delete a post */ deletePosting(): void { - this.metisService.deletePost(this.posting); + this.isDeleteEvent.emit(true); } setMayEditOrDelete(): void { diff --git a/src/main/webapp/app/shared/metis/posting-header/posting-header.directive.ts b/src/main/webapp/app/shared/metis/posting-header/posting-header.directive.ts index b20c051bd433..7892a6df0f09 100644 --- a/src/main/webapp/app/shared/metis/posting-header/posting-header.directive.ts +++ b/src/main/webapp/app/shared/metis/posting-header/posting-header.directive.ts @@ -1,5 +1,5 @@ import { Posting } from 'app/entities/metis/posting.model'; -import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Directive, EventEmitter, Input, OnInit, Output, input, output } from '@angular/core'; import dayjs from 'dayjs/esm'; import { MetisService } from 'app/shared/metis/metis.service'; import { UserRole } from 'app/shared/metis/metis.util'; @@ -16,6 +16,9 @@ export abstract class PostingHeaderDirective implements OnIni @Input() hasChannelModerationRights = false; @Output() isModalOpen = new EventEmitter(); + + isDeleted = input(false); + isDeleteEvent = output(); isAtLeastTutorInCourse: boolean; isAuthorOfPosting: boolean; postingIsOfToday: boolean; diff --git a/src/main/webapp/app/shared/metis/posting.directive.ts b/src/main/webapp/app/shared/metis/posting.directive.ts index 8e6636ee4c7a..a6bfc936e855 100644 --- a/src/main/webapp/app/shared/metis/posting.directive.ts +++ b/src/main/webapp/app/shared/metis/posting.directive.ts @@ -1,8 +1,9 @@ import { Posting } from 'app/entities/metis/posting.model'; -import { Directive, Input, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Directive, Input, OnDestroy, OnInit, inject } from '@angular/core'; +import { MetisService } from 'app/shared/metis/metis.service'; @Directive() -export abstract class PostingDirective implements OnInit { +export abstract class PostingDirective implements OnInit, OnDestroy { @Input() posting: T; @Input() isCommunicationPage: boolean; @Input() showChannelReference?: boolean; @@ -10,9 +11,62 @@ export abstract class PostingDirective implements OnInit { @Input() hasChannelModerationRights = false; @Input() isThreadSidebar: boolean; + isAnswerPost = false; + isDeleted = false; + readonly timeToDeleteInSeconds = 6; + deleteTimerInSeconds = 6; + deleteTimer: NodeJS.Timeout | undefined; + deleteInterval: NodeJS.Timeout | undefined; + content?: string; + protected metisService = inject(MetisService); + protected changeDetector = inject(ChangeDetectorRef); + ngOnInit(): void { this.content = this.posting.content; } + + ngOnDestroy(): void { + if (this.deleteTimer !== undefined) { + clearTimeout(this.deleteTimer); + } + + if (this.deleteInterval !== undefined) { + clearInterval(this.deleteInterval); + } + } + + onDeleteEvent(isDelete: boolean) { + this.isDeleted = isDelete; + + if (this.deleteTimer !== undefined) { + clearTimeout(this.deleteTimer); + } + + if (this.deleteInterval !== undefined) { + clearInterval(this.deleteInterval); + } + + if (isDelete) { + this.deleteTimerInSeconds = this.timeToDeleteInSeconds; + + this.deleteTimer = setTimeout( + () => { + if (this.isAnswerPost) { + this.metisService.deleteAnswerPost(this.posting); + } else { + this.metisService.deletePost(this.posting); + } + }, + // We add a tiny buffer to make it possible for the user to react a bit longer than the ui displays (+1000) + this.deleteTimerInSeconds * 1000 + 1000, + ); + + this.deleteInterval = setInterval(() => { + this.deleteTimerInSeconds = Math.max(0, this.deleteTimerInSeconds - 1); + this.changeDetector.detectChanges(); + }, 1000); + } + } } diff --git a/src/main/webapp/app/shared/profile-picture/profile-picture.component.html b/src/main/webapp/app/shared/profile-picture/profile-picture.component.html index 2d08f4f7737c..ceebd3f4adfb 100644 --- a/src/main/webapp/app/shared/profile-picture/profile-picture.component.html +++ b/src/main/webapp/app/shared/profile-picture/profile-picture.component.html @@ -3,8 +3,8 @@ {{ userProfilePictureInitials }} } @else { @@ -14,7 +14,7 @@ class="profile-picture rounded-3" [src]="imageUrl()" [ngStyle]="{ 'background-color': profilePictureBackgroundColor }" - [ngClass]="imageClass()" + [ngClass]="imageClass() + (isGray() ? ' is-grayscale' : '')" /> } @if (isEditable()) { diff --git a/src/main/webapp/app/shared/profile-picture/profile-picture.component.scss b/src/main/webapp/app/shared/profile-picture/profile-picture.component.scss index 2cb3f625f5aa..7082b154b5fc 100644 --- a/src/main/webapp/app/shared/profile-picture/profile-picture.component.scss +++ b/src/main/webapp/app/shared/profile-picture/profile-picture.component.scss @@ -4,8 +4,12 @@ display: inline-flex; align-items: center; justify-content: center; - background-color: var(--gray-400); + background-color: var(--gray-600); color: var(--white); + + &.is-grayscale { + filter: grayscale(100%); + } } .profile-picture-wrap { diff --git a/src/main/webapp/app/shared/profile-picture/profile-picture.component.ts b/src/main/webapp/app/shared/profile-picture/profile-picture.component.ts index 5dd16a992369..f8600f3a0ccf 100644 --- a/src/main/webapp/app/shared/profile-picture/profile-picture.component.ts +++ b/src/main/webapp/app/shared/profile-picture/profile-picture.component.ts @@ -24,6 +24,7 @@ export class ProfilePictureComponent implements OnInit, OnChanges { readonly imageId = input(''); readonly defaultPictureId = input(''); readonly isEditable = input(false); + readonly isGray = input(false); profilePictureBackgroundColor: string; userProfilePictureInitials: string; diff --git a/src/main/webapp/i18n/de/metis.json b/src/main/webapp/i18n/de/metis.json index 441dcb362e63..7b278eeee22e 100644 --- a/src/main/webapp/i18n/de/metis.json +++ b/src/main/webapp/i18n/de/metis.json @@ -125,7 +125,9 @@ "exerciseOrLecture": "Übung / Vorlesung", "showAllPosts": "Zeige alle Nachrichten", "showContent": "Inhalt ausklappen", - "collapseContent": "Inhalt einklappen" + "collapseContent": "Inhalt einklappen", + "deletedContent": "Beitrag wird in {{ progress }} Sekunde(n) gelöscht.", + "undoDelete": "Löschen umkehren" }, "answerPost": { "created": "Antwort erfolgreich erstellt", diff --git a/src/main/webapp/i18n/en/metis.json b/src/main/webapp/i18n/en/metis.json index 4778ad6c3532..92a75b2da28d 100644 --- a/src/main/webapp/i18n/en/metis.json +++ b/src/main/webapp/i18n/en/metis.json @@ -125,7 +125,9 @@ "exerciseOrLecture": "Exercise / Lecture", "showAllPosts": "Show all messages", "showContent": "Show content", - "collapseContent": "Collapse content" + "collapseContent": "Collapse content", + "deletedContent": "Post is being deleted in {{ progress }} second(s).", + "undoDelete": "Undo delete" }, "answerPost": { "created": "New reply successfully created", diff --git a/src/test/javascript/spec/component/overview/discussion-section/discussion-section.component.spec.ts b/src/test/javascript/spec/component/overview/discussion-section/discussion-section.component.spec.ts index 262cae99dadd..3227b7de1259 100644 --- a/src/test/javascript/spec/component/overview/discussion-section/discussion-section.component.spec.ts +++ b/src/test/javascript/spec/component/overview/discussion-section/discussion-section.component.spec.ts @@ -45,6 +45,7 @@ import { NotificationService } from 'app/shared/notification/notification.servic import { MockNotificationService } from '../../../helpers/mocks/service/mock-notification.service'; import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ProfilePictureComponent } from 'app/shared/profile-picture/profile-picture.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @Directive({ // eslint-disable-next-line @angular-eslint/directive-selector @@ -66,7 +67,7 @@ describe('DiscussionSectionComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MockModule(FormsModule), MockModule(ReactiveFormsModule), MockModule(NgbTooltipModule), DiscussionSectionComponent], + imports: [MockModule(FormsModule), MockModule(ReactiveFormsModule), MockModule(NgbTooltipModule), DiscussionSectionComponent, MockModule(BrowserAnimationsModule)], providers: [ provideHttpClient(), provideHttpClientTesting(), diff --git a/src/test/javascript/spec/component/shared/metis/answer-post/answer-post.component.spec.ts b/src/test/javascript/spec/component/shared/metis/answer-post/answer-post.component.spec.ts index a3be0a1de0b2..b4b9eea75fd7 100644 --- a/src/test/javascript/spec/component/shared/metis/answer-post/answer-post.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/answer-post/answer-post.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { AnswerPostComponent } from 'app/shared/metis/answer-post/answer-post.component'; -import { MockComponent, MockPipe } from 'ng-mocks'; +import { MockComponent, MockModule, MockPipe } from 'ng-mocks'; import { DebugElement } from '@angular/core'; import { HtmlForMarkdownPipe } from 'app/shared/pipes/html-for-markdown.pipe'; import { getElement } from '../../../../helpers/utils/general.utils'; @@ -9,6 +9,9 @@ import { AnswerPostFooterComponent } from 'app/shared/metis/posting-footer/answe import { PostingContentComponent } from 'app/shared/metis/posting-content/posting-content.components'; import { metisResolvingAnswerPostUser1 } from '../../../../helpers/sample/metis-sample-data'; import { AnswerPostCreateEditModalComponent } from 'app/shared/metis/posting-create-edit-modal/answer-post-create-edit-modal/answer-post-create-edit-modal.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { MetisService } from 'app/shared/metis/metis.service'; +import { MockMetisService } from '../../../../helpers/mocks/service/mock-metis-service.service'; describe('AnswerPostComponent', () => { let component: AnswerPostComponent; @@ -17,6 +20,7 @@ describe('AnswerPostComponent', () => { beforeEach(() => { return TestBed.configureTestingModule({ + imports: [MockModule(BrowserAnimationsModule)], declarations: [ AnswerPostComponent, MockPipe(HtmlForMarkdownPipe), @@ -25,6 +29,7 @@ describe('AnswerPostComponent', () => { MockComponent(AnswerPostCreateEditModalComponent), MockComponent(AnswerPostFooterComponent), ], + providers: [{ provide: MetisService, useClass: MockMetisService }], }) .compileComponents() .then(() => { @@ -48,11 +53,6 @@ describe('AnswerPostComponent', () => { expect(answerPostCreateEditModal).not.toBeNull(); }); - it('should contain an answer post footer', () => { - const footer = getElement(debugElement, 'jhi-answer-post-footer'); - expect(footer).not.toBeNull(); - }); - it('should have correct content', () => { component.posting = metisResolvingAnswerPostUser1; component.ngOnInit(); diff --git a/src/test/javascript/spec/component/shared/metis/post/post.component.spec.ts b/src/test/javascript/spec/component/shared/metis/post/post.component.spec.ts index dac357f94bec..f2baf5576b7b 100644 --- a/src/test/javascript/spec/component/shared/metis/post/post.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/post/post.component.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MockComponent, MockDirective, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponent, MockDirective, MockModule, MockPipe, MockProvider } from 'ng-mocks'; import { DebugElement } from '@angular/core'; import { HtmlForMarkdownPipe } from 'app/shared/pipes/html-for-markdown.pipe'; import { PostComponent } from 'app/shared/metis/post/post.component'; @@ -33,6 +33,7 @@ import { HttpResponse } from '@angular/common/http'; import { MockRouter } from '../../../../helpers/mocks/mock-router'; import { AnswerPostCreateEditModalComponent } from 'app/shared/metis/posting-create-edit-modal/answer-post-create-edit-modal/answer-post-create-edit-modal.component'; import { PostReactionsBarComponent } from 'app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; describe('PostComponent', () => { let component: PostComponent; @@ -46,7 +47,7 @@ describe('PostComponent', () => { beforeEach(() => { return TestBed.configureTestingModule({ - imports: [MockDirective(NgbTooltip)], + imports: [MockDirective(NgbTooltip), MockModule(BrowserAnimationsModule)], providers: [ provideRouter([]), { provide: MetisService, useClass: MockMetisService }, @@ -214,4 +215,48 @@ describe('PostComponent', () => { expect(setActiveConversationSpy).toHaveBeenCalledWith(metisChannel.id!); }); + + it('should set isDeleted to true', () => { + component.onDeleteEvent(true); + expect(component.isDeleted).toBeTrue(); + }); + + it('should clear existing timers and intervals', () => { + const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout'); + const clearIntervalSpy = jest.spyOn(global, 'clearInterval'); + + component.deleteTimer = setTimeout(() => {}, 1000); + component.deleteInterval = setInterval(() => {}, 1000); + component.onDeleteEvent(true); + + expect(clearIntervalSpy).toHaveBeenCalledOnce(); + expect(clearTimeoutSpy).toHaveBeenCalledOnce(); + }); + + it('should clear existing timers and intervals on destroy', () => { + const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout'); + const clearIntervalSpy = jest.spyOn(global, 'clearInterval'); + + component.deleteTimer = setTimeout(() => {}, 1000); + component.deleteInterval = setInterval(() => {}, 1000); + component.ngOnDestroy(); + + expect(clearIntervalSpy).toHaveBeenCalledOnce(); + expect(clearTimeoutSpy).toHaveBeenCalledOnce(); + }); + + it('should set deleteTimer and deleteInterval when isDelete is true', () => { + component.onDeleteEvent(true); + + expect(component.deleteTimer).toBeDefined(); + expect(component.deleteInterval).toBeDefined(); + expect(component.deleteTimerInSeconds).toBe(component.timeToDeleteInSeconds); + }); + + it('should not set timers when isDelete is false', () => { + component.onDeleteEvent(false); + + expect(component.deleteTimer).toBeUndefined(); + expect(component.deleteInterval).toBeUndefined(); + }); }); diff --git a/src/test/javascript/spec/component/shared/metis/posting-content/posting-content.component.spec.ts b/src/test/javascript/spec/component/shared/metis/posting-content/posting-content.component.spec.ts index c54fa917d128..00be6399589b 100644 --- a/src/test/javascript/spec/component/shared/metis/posting-content/posting-content.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/posting-content/posting-content.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { PostingContentPartComponent } from 'app/shared/metis/posting-content/posting-content-part/posting-content-part.components'; -import { MockComponent, MockPipe } from 'ng-mocks'; +import { MockComponent, MockDirective, MockPipe } from 'ng-mocks'; import { PostingContentComponent } from 'app/shared/metis/posting-content/posting-content.components'; import { MetisService } from 'app/shared/metis/metis.service'; import { provideHttpClientTesting } from '@angular/common/http/testing'; @@ -13,6 +13,7 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { metisCourse, metisCoursePosts, metisExercisePosts, metisGeneralCourseWidePosts, metisLecturePosts } from '../../../../helpers/sample/metis-sample-data'; import { Params } from '@angular/router'; import { provideHttpClient } from '@angular/common/http'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; describe('PostingContentComponent', () => { let component: PostingContentComponent; @@ -23,7 +24,13 @@ describe('PostingContentComponent', () => { return TestBed.configureTestingModule({ imports: [], providers: [provideHttpClient(), provideHttpClientTesting(), { provide: MetisService, useClass: MockMetisService }], - declarations: [PostingContentComponent, MockComponent(PostingContentPartComponent), MockComponent(FaIconComponent), MockPipe(ArtemisTranslatePipe)], + declarations: [ + PostingContentComponent, + MockComponent(PostingContentPartComponent), + MockComponent(FaIconComponent), + MockPipe(ArtemisTranslatePipe), + MockDirective(TranslateDirective), + ], }) .compileComponents() .then(() => { @@ -117,6 +124,13 @@ describe('PostingContentComponent', () => { expect(component.getPatternMatches()).toEqual([firstMatch]); }); + it('should display undo delete prompt when isDeleted is set to true', () => { + fixture.componentRef.setInput('isDeleted', true); + fixture.detectChanges(); + expect(fixture.nativeElement.querySelector('.posting-content-undo-delete')).not.toBeNull(); + fixture.componentRef.setInput('isDeleted', false); + }); + it('should calculate correct pattern matches for content with post, multiple exercise, lecture and attachment references', () => { component.content = 'I do want to reference #4, #10, ' + diff --git a/src/test/javascript/spec/component/shared/metis/postings-header/answer-post-header/answer-post-header.component.spec.ts b/src/test/javascript/spec/component/shared/metis/postings-header/answer-post-header/answer-post-header.component.spec.ts index 750274c5b685..dc4d6d491b06 100644 --- a/src/test/javascript/spec/component/shared/metis/postings-header/answer-post-header/answer-post-header.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/postings-header/answer-post-header/answer-post-header.component.spec.ts @@ -32,7 +32,6 @@ describe('AnswerPostHeaderComponent', () => { let metisServiceUserIsAtLeastTutorMock: jest.SpyInstance; let metisServiceUserIsAtLeastInstructorMock: jest.SpyInstance; let metisServiceUserPostingAuthorMock: jest.SpyInstance; - let metisServiceDeleteAnswerPostMock: jest.SpyInstance; let metisServiceUpdateAnswerPostMock: jest.SpyInstance; const yesterday: dayjs.Dayjs = dayjs().subtract(1, 'day'); @@ -70,7 +69,6 @@ describe('AnswerPostHeaderComponent', () => { metisServiceUserIsAtLeastTutorMock = jest.spyOn(metisService, 'metisUserIsAtLeastTutorInCourse'); metisServiceUserIsAtLeastInstructorMock = jest.spyOn(metisService, 'metisUserIsAtLeastInstructorInCourse'); metisServiceUserPostingAuthorMock = jest.spyOn(metisService, 'metisUserIsAuthorOfPosting'); - metisServiceDeleteAnswerPostMock = jest.spyOn(metisService, 'deleteAnswerPost'); metisServiceUpdateAnswerPostMock = jest.spyOn(metisService, 'updateAnswerPost'); debugElement = fixture.debugElement; component.posting = metisResolvingAnswerPostUser1; @@ -190,12 +188,13 @@ describe('AnswerPostHeaderComponent', () => { expect(openPostingCreateEditModalEmitSpy).toHaveBeenCalledOnce(); }); - it('should invoke metis service when delete icon is clicked', () => { - metisServiceUserIsAtLeastTutorMock.mockReturnValue(true); + it('should not display edit and delete options when post is deleted', () => { + fixture.componentRef.setInput('isDeleted', true); + component.ngOnInit(); fixture.detectChanges(); - expect(getElement(debugElement, '.deleteIcon')).not.toBeNull(); - component.deletePosting(); - expect(metisServiceDeleteAnswerPostMock).toHaveBeenCalledOnce(); + expect(getElement(debugElement, '.editIcon')).toBeNull(); + expect(getElement(debugElement, '.deleteIcon')).toBeNull(); + fixture.componentRef.setInput('isDeleted', false); }); it('should invoke metis service when toggle resolve is clicked as tutor', () => { @@ -208,12 +207,4 @@ describe('AnswerPostHeaderComponent', () => { expect(component.posting.resolvesPost).toEqual(!previousState); expect(metisServiceUpdateAnswerPostMock).toHaveBeenCalledOnce(); }); - - it('should invoke metis service when toggle resolve is clicked as post author', () => { - metisServiceUserIsAtLeastTutorMock.mockReturnValue(true); - fixture.detectChanges(); - expect(getElement(debugElement, '.deleteIcon')).not.toBeNull(); - component.deletePosting(); - expect(metisServiceDeleteAnswerPostMock).toHaveBeenCalledOnce(); - }); }); diff --git a/src/test/javascript/spec/component/shared/metis/postings-header/post-header/post-header.component.spec.ts b/src/test/javascript/spec/component/shared/metis/postings-header/post-header/post-header.component.spec.ts index 8a244697b4fe..f599c236354d 100644 --- a/src/test/javascript/spec/component/shared/metis/postings-header/post-header/post-header.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/postings-header/post-header/post-header.component.spec.ts @@ -29,7 +29,6 @@ describe('PostHeaderComponent', () => { let metisServiceUserIsAtLeastTutorStub: jest.SpyInstance; let metisServiceUserIsAtLeastInstructorStub: jest.SpyInstance; let metisServiceUserIsAuthorOfPostingStub: jest.SpyInstance; - let metisServiceDeletePostMock: jest.SpyInstance; beforeEach(() => { return TestBed.configureTestingModule({ imports: [MockModule(FormsModule), MockModule(ReactiveFormsModule), MockDirective(NgbTooltip), MockModule(MetisModule)], @@ -54,7 +53,6 @@ describe('PostHeaderComponent', () => { metisServiceUserIsAtLeastTutorStub = jest.spyOn(metisService, 'metisUserIsAtLeastTutorInCourse'); metisServiceUserIsAtLeastInstructorStub = jest.spyOn(metisService, 'metisUserIsAtLeastInstructorInCourse'); metisServiceUserIsAuthorOfPostingStub = jest.spyOn(metisService, 'metisUserIsAuthorOfPosting'); - metisServiceDeletePostMock = jest.spyOn(metisService, 'deletePost'); debugElement = fixture.debugElement; component.posting = metisPostLectureUser1; component.ngOnInit(); @@ -111,14 +109,6 @@ describe('PostHeaderComponent', () => { expect(getElement(debugElement, '.deleteIcon')).toBeNull(); }); - it('should invoke metis service when delete icon is clicked', () => { - metisServiceUserIsAtLeastTutorStub.mockReturnValue(true); - fixture.detectChanges(); - expect(getElement(debugElement, '.deleteIcon')).not.toBeNull(); - component.deletePosting(); - expect(metisServiceDeletePostMock).toHaveBeenCalledOnce(); - }); - it('should not display edit and delete options to tutor if posting is announcement', () => { metisServiceUserIsAtLeastInstructorStub.mockReturnValue(false); component.posting = metisAnnouncement; @@ -137,6 +127,15 @@ describe('PostHeaderComponent', () => { expect(getElement(debugElement, '.deleteIcon')).not.toBeNull(); }); + it('should not display edit and delete options when post is deleted', () => { + fixture.componentRef.setInput('isDeleted', true); + component.ngOnInit(); + fixture.detectChanges(); + expect(getElement(debugElement, '.editIcon')).toBeNull(); + expect(getElement(debugElement, '.deleteIcon')).toBeNull(); + fixture.componentRef.setInput('isDeleted', false); + }); + it.each` input | expect ${UserRole.INSTRUCTOR} | ${'post-authority-icon-instructor'}