From b09c320de83292ca5d34f2cdc0417c697cccd32e Mon Sep 17 00:00:00 2001 From: Asli Aykan Date: Thu, 10 Oct 2024 20:48:40 +0200 Subject: [PATCH 01/31] consecutive message view with new reaction bar added --- .../app/entities/metis/answer-post.model.ts | 1 + .../webapp/app/entities/metis/post.model.ts | 1 + .../conversation-messages.component.html | 31 +++-- .../conversation-messages.component.scss | 22 ++++ .../conversation-messages.component.ts | 80 +++++++++++- .../answer-post/answer-post.component.html | 57 +++++--- .../answer-post/answer-post.component.scss | 49 +++++++ .../answer-post/answer-post.component.ts | 11 ++ .../app/shared/metis/post/post.component.html | 50 +++++-- .../app/shared/metis/post/post.component.scss | 50 +++++++ .../app/shared/metis/post/post.component.ts | 8 +- ...answer-post-create-edit-modal.component.ts | 7 +- .../post-footer/post-footer.component.html | 29 +++-- .../post-footer/post-footer.component.ts | 122 ++++++++++++++---- .../answer-post-header.component.html | 18 --- .../answer-post-header.component.ts | 15 --- .../post-header/post-header.component.ts | 11 -- .../answer-post-reactions-bar.component.html | 102 +++++++++------ .../answer-post-reactions-bar.component.ts | 49 ++++++- .../post-reactions-bar.component.html | 113 +++++++++------- .../post-reactions-bar.component.ts | 43 +++++- .../posting-reactions-bar.component.scss | 8 +- .../posting-thread.component.html | 1 + .../posting-thread.component.ts | 1 + 24 files changed, 645 insertions(+), 234 deletions(-) diff --git a/src/main/webapp/app/entities/metis/answer-post.model.ts b/src/main/webapp/app/entities/metis/answer-post.model.ts index 000b7d59837b..c76399075551 100644 --- a/src/main/webapp/app/entities/metis/answer-post.model.ts +++ b/src/main/webapp/app/entities/metis/answer-post.model.ts @@ -4,6 +4,7 @@ import { Posting } from 'app/entities/metis/posting.model'; export class AnswerPost extends Posting { public resolvesPost?: boolean; public post?: Post; + public isConsecutive?: boolean = false; constructor() { super(); diff --git a/src/main/webapp/app/entities/metis/post.model.ts b/src/main/webapp/app/entities/metis/post.model.ts index 593d9535dab2..60adfe3c64c0 100644 --- a/src/main/webapp/app/entities/metis/post.model.ts +++ b/src/main/webapp/app/entities/metis/post.model.ts @@ -13,6 +13,7 @@ export class Post extends Posting { public conversation?: Conversation; public displayPriority?: DisplayPriority; public resolved?: boolean; + public isConsecutive?: boolean = false; constructor() { super(); diff --git a/src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.html b/src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.html index 373d7b8d9694..31350e48fe09 100644 --- a/src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.html +++ b/src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.html @@ -74,19 +74,24 @@ > - @for (post of posts; track postsTrackByFn($index, post)) { -
- + @for (group of groupedPosts; track postsTrackByFn($index, group)) { +
+ @for (post of group.posts; track postsTrackByFn($index, post)) { +
+ +
+ }
}
diff --git a/src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.scss b/src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.scss index 01f09f46008e..151c15afc205 100644 --- a/src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.scss +++ b/src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.scss @@ -60,3 +60,25 @@ padding-bottom: 100px; } } + +.message-group { + display: flex; + flex-direction: column; + margin-bottom: 10px; +} + +.grouped-posts { + margin-left: 30px; + padding-left: 10px; +} + +.grouped-posts, +.grouped-post { + margin-top: 0; + margin-bottom: 0; + padding: 0; +} + +jhi-posting-thread { + margin-bottom: 5px; +} diff --git a/src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.ts b/src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.ts index e4b11dd87612..6d6130b89cd3 100644 --- a/src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.ts +++ b/src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.ts @@ -27,6 +27,22 @@ import { MetisConversationService } from 'app/shared/metis/metis-conversation.se import { OneToOneChat, isOneToOneChatDTO } from 'app/entities/metis/conversation/one-to-one-chat.model'; import { canCreateNewMessageInConversation } from 'app/shared/metis/conversations/conversation-permissions.utils'; import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; +import dayjs from 'dayjs/esm'; +import { User } from 'app/core/user/user.model'; + +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ standalone: true, name: 'dayjsToDate' }) +export class DayjsToDatePipe implements PipeTransform { + transform(value: dayjs.Dayjs | undefined): Date | undefined { + return value ? value.toDate() : undefined; + } +} + +interface PostGroup { + author: User | undefined; + posts: Post[]; +} @Component({ selector: 'jhi-conversation-messages', @@ -69,6 +85,7 @@ export class ConversationMessagesComponent implements OnInit, AfterViewInit, OnD newPost?: Post; posts: Post[] = []; + groupedPosts: PostGroup[] = []; totalNumberOfPosts = 0; page = 1; public isFetchingPosts = true; @@ -159,11 +176,70 @@ export class ConversationMessagesComponent implements OnInit, AfterViewInit, OnD }; } - setPosts(posts: Post[]): void { + toDate(dayjsDate: dayjs.Dayjs | undefined): Date | undefined { + return dayjsDate ? dayjsDate.toDate() : undefined; + } + + private groupPosts(): void { + if (!this.posts || this.posts.length === 0) { + this.groupedPosts = []; + return; + } + + const sortedPosts = this.posts.sort((a, b) => { + const aDate = (a as any).creationDateAsDate; + const bDate = (b as any).creationDateAsDate; + return (aDate?.getTime() || 0) - (bDate?.getTime() || 0); + }); + + const groups: PostGroup[] = []; + let currentGroup: PostGroup = { + author: sortedPosts[0].author, + posts: [{ ...sortedPosts[0], isConsecutive: false }], + }; + + for (let i = 1; i < sortedPosts.length; i++) { + const currentPost = sortedPosts[i]; + const lastPostInGroup = currentGroup.posts[currentGroup.posts.length - 1]; + + const currentDate = (currentPost as any).creationDateAsDate; + const lastDate = (lastPostInGroup as any).creationDateAsDate; + + let timeDiff = Number.MAX_SAFE_INTEGER; + if (currentDate && lastDate) { + timeDiff = Math.abs(currentDate.getTime() - lastDate.getTime()) / 60000; + } + + if (currentPost.author?.id === currentGroup.author?.id && timeDiff <= 60) { + currentGroup.posts.push({ ...currentPost, isConsecutive: true }); // consecutive post + } else { + groups.push(currentGroup); + currentGroup = { + author: currentPost.author, + posts: [{ ...currentPost, isConsecutive: false }], + }; + } + } + + groups.push(currentGroup); + this.groupedPosts = groups; + this.cdr.detectChanges(); + } + + private setPosts(posts: Post[]): void { if (this.content) { this.previousScrollDistanceFromTop = this.content.nativeElement.scrollHeight - this.content.nativeElement.scrollTop; } - this.posts = posts.slice().reverse(); + + this.posts = posts + .slice() + .reverse() + .map((post) => { + (post as any).creationDateAsDate = post.creationDate ? dayjs(post.creationDate).toDate() : undefined; + return post; + }); + + this.groupPosts(); } fetchNextPage() { 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..d2d020b28741 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 @@ -1,36 +1,51 @@
- + @if (!isConsecutive) { + + } @if (!createAnswerPostModal.isInputOpen) { -
- +
+
+ +
+ +
+
}
-
- + 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 4d929f96cf34..55e56fbe7856 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 @@ -6,3 +6,52 @@ .answer-post-content-margin { padding-left: 3.2rem; } + +.message-container { + position: relative; + border-radius: 5px; + transition: background-color 0.3s ease; +} + +.message-content { + padding-left: 0.3rem; +} + +.message-container:hover { + background: var(--metis-selection-option-hover-background); +} + +.hover-actions { + position: absolute; + top: 0; + right: 1%; + display: flex; + gap: 10px; + visibility: hidden; + transition: + opacity 0.2s ease-in-out, + visibility 0.2s ease-in-out; + background: var(--metis-selection-option-background); + padding: 5px; + border-radius: 5px; +} + +.message-container:hover .hover-actions { + opacity: 1; + visibility: visible; +} + +.clickable { + cursor: pointer; +} + +.reactionIcon, +.editIcon, +.deleteIcon { + font-size: 1rem; + color: var(--metis-blue); +} + +.hover-actions .clickable:hover { + color: var(--bs-white); +} 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..59a69ae792c0 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,6 +2,8 @@ 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 { Posting } from 'app/entities/metis/posting.model'; +import { Reaction } from 'app/entities/metis/reaction.model'; @Component({ selector: 'jhi-answer-post', @@ -20,4 +22,13 @@ export class AnswerPostComponent extends PostingDirective { isReadOnlyMode = false; // ng-container to render answerPostCreateEditModalComponent @ViewChild('createEditAnswerPostContainer', { read: ViewContainerRef }) containerRef: ViewContainerRef; + @Input() isConsecutive: boolean = false; + + onPostingUpdated(updatedPosting: Posting) { + this.posting = updatedPosting; + } + + onReactionsUpdated(updatedReactions: Reaction[]) { + this.posting = { ...this.posting, reactions: updatedReactions }; + } } 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..e4522dfdafeb 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.html +++ b/src/main/webapp/app/shared/metis/post/post.component.html @@ -1,5 +1,5 @@ -
-
+
+ @if (!isConsecutive) { -
+ }
@@ -50,16 +50,37 @@ }
@if (!displayInlineInput) { - +
+
+ +
+ @if (!previewMode) { + + } +
+
+
}
@@ -74,6 +95,7 @@ }
diff --git a/src/main/webapp/app/shared/metis/post/post.component.scss b/src/main/webapp/app/shared/metis/post/post.component.scss index d88762caf808..052b0a0455af 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.scss +++ b/src/main/webapp/app/shared/metis/post/post.component.scss @@ -14,3 +14,53 @@ .reference-hash { color: inherit; } + +.message-container { + position: relative; + border-radius: 5px; + transition: background-color 0.3s ease; +} + +.message-content { + padding-left: 0.3rem; +} + +.message-container:hover { + background: var(--metis-selection-option-hover-background); +} + +.hover-actions { + position: absolute; + top: 0; + right: 1%; + display: flex; + gap: 10px; + visibility: hidden; + transition: + opacity 0.2s ease-in-out, + visibility 0.2s ease-in-out; + background: var(--metis-selection-option-background); + padding: 5px; + border-radius: 5px; +} + +.message-container:hover .hover-actions { + opacity: 1; + visibility: visible; +} + +.clickable { + cursor: pointer; +} + +.reactionIcon, +.editIcon, +.deleteIcon { + font-size: 1rem; + color: var(--metis-blue); +} + +/* Optional: Additional styling for icons on hover */ +.hover-actions .clickable:hover { + color: var(--bs-white); +} diff --git a/src/main/webapp/app/shared/metis/post/post.component.ts b/src/main/webapp/app/shared/metis/post/post.component.ts index e9a0a8cf7bf5..cb36d154282c 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.ts +++ b/src/main/webapp/app/shared/metis/post/post.component.ts @@ -16,7 +16,7 @@ import { PostingDirective } from 'app/shared/metis/posting.directive'; import { MetisService } from 'app/shared/metis/metis.service'; import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { ContextInformation, DisplayPriority, PageType, RouteComponents } from '../metis.util'; -import { faBullhorn, faCheckSquare } from '@fortawesome/free-solid-svg-icons'; +import { faBullhorn, faPencilAlt, faTrash } from '@fortawesome/free-solid-svg-icons'; import dayjs from 'dayjs/esm'; import { PostFooterComponent } from 'app/shared/metis/posting-footer/post-footer/post-footer.component'; import { OneToOneChatService } from 'app/shared/metis/conversations/one-to-one-chat.service'; @@ -60,8 +60,10 @@ export class PostComponent extends PostingDirective implements OnInit, OnC readonly DisplayPriority = DisplayPriority; // Icons - faBullhorn = faBullhorn; - faCheckSquare = faCheckSquare; + readonly faBullhorn = faBullhorn; + readonly faPencilAlt = faPencilAlt; + readonly faTrash = faTrash; + @Input() isConsecutive: boolean = false; constructor( private metisService: MetisService, diff --git a/src/main/webapp/app/shared/metis/posting-create-edit-modal/answer-post-create-edit-modal/answer-post-create-edit-modal.component.ts b/src/main/webapp/app/shared/metis/posting-create-edit-modal/answer-post-create-edit-modal/answer-post-create-edit-modal.component.ts index 4df22238350c..56a031a07dd8 100644 --- a/src/main/webapp/app/shared/metis/posting-create-edit-modal/answer-post-create-edit-modal/answer-post-create-edit-modal.component.ts +++ b/src/main/webapp/app/shared/metis/posting-create-edit-modal/answer-post-create-edit-modal/answer-post-create-edit-modal.component.ts @@ -1,10 +1,11 @@ -import { Component, Input, ViewContainerRef, ViewEncapsulation } from '@angular/core'; +import { Component, EventEmitter, Input, Output, ViewContainerRef, ViewEncapsulation } from '@angular/core'; import { PostingCreateEditModalDirective } from 'app/shared/metis/posting-create-edit-modal/posting-create-edit-modal.directive'; import { AnswerPost } from 'app/entities/metis/answer-post.model'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { MetisService } from 'app/shared/metis/metis.service'; import { FormBuilder, Validators } from '@angular/forms'; import { PostContentValidationPattern } from 'app/shared/metis/metis.util'; +import { Posting } from 'app/entities/metis/posting.model'; @Component({ selector: 'jhi-answer-post-create-edit-modal', @@ -15,6 +16,7 @@ import { PostContentValidationPattern } from 'app/shared/metis/metis.util'; export class AnswerPostCreateEditModalComponent extends PostingCreateEditModalDirective { @Input() createEditAnswerPostContainerRef: ViewContainerRef; isInputOpen = false; + @Output() postingUpdated = new EventEmitter(); constructor( protected metisService: MetisService, @@ -78,7 +80,8 @@ export class AnswerPostCreateEditModalComponent extends PostingCreateEditModalDi updatePosting(): void { this.posting.content = this.formGroup.get('content')?.value; this.metisService.updateAnswerPost(this.posting).subscribe({ - next: () => { + next: (updatedPost: AnswerPost) => { + this.postingUpdated.emit(updatedPost); this.isLoading = false; this.isInputOpen = false; this.createEditAnswerPostContainerRef?.clear(); 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..6a980a907348 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,19 +12,22 @@
- @for (answerPost of sortedAnswerPosts; track $index; let isLastAnswer = $last) { - + @for (group of groupedAnswerPosts; track trackGroupByFn($index, group)) { + @for (answerPost of group.posts; track trackPostByFn($index, answerPost)) { + + } } } diff --git a/src/main/webapp/app/shared/metis/posting-footer/post-footer/post-footer.component.ts b/src/main/webapp/app/shared/metis/posting-footer/post-footer/post-footer.component.ts index d5e72f69949d..34024e5ac295 100644 --- a/src/main/webapp/app/shared/metis/posting-footer/post-footer/post-footer.component.ts +++ b/src/main/webapp/app/shared/metis/posting-footer/post-footer/post-footer.component.ts @@ -1,4 +1,17 @@ -import { AfterContentChecked, ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild, ViewContainerRef } from '@angular/core'; +import { + AfterContentChecked, + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + SimpleChanges, + ViewChild, + ViewContainerRef, +} from '@angular/core'; import { PostingFooterDirective } from 'app/shared/metis/posting-footer/posting-footer.directive'; import { Post } from 'app/entities/metis/post.model'; import { MetisService } from 'app/shared/metis/metis.service'; @@ -6,58 +19,62 @@ import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { AnswerPostCreateEditModalComponent } from 'app/shared/metis/posting-create-edit-modal/answer-post-create-edit-modal/answer-post-create-edit-modal.component'; import { AnswerPost } from 'app/entities/metis/answer-post.model'; import dayjs from 'dayjs/esm'; +import { User } from 'app/core/user/user.model'; + +interface PostGroup { + author: User | undefined; + posts: AnswerPost[]; +} @Component({ selector: 'jhi-post-footer', templateUrl: './post-footer.component.html', styleUrls: ['./post-footer.component.scss'], }) -export class PostFooterComponent extends PostingFooterDirective implements OnInit, OnDestroy, AfterContentChecked { +export class PostFooterComponent extends PostingFooterDirective implements OnInit, OnDestroy, AfterContentChecked, OnChanges { @Input() lastReadDate?: dayjs.Dayjs; @Input() readOnlyMode = false; - @Input() previewMode: boolean; - // if the post is previewed in the create/edit modal, - // we need to pass the ref in order to close it when navigating to the previewed post via post context + @Input() previewMode = false; @Input() modalRef?: NgbModalRef; - tags: string[]; - courseId: number; - @Input() - hasChannelModerationRights = false; + @Input() hasChannelModerationRights = false; + @Input() showAnswers = false; + @Input() isCommunicationPage = false; + @Input() sortedAnswerPosts: AnswerPost[] = []; - @ViewChild(AnswerPostCreateEditModalComponent) answerPostCreateEditModal?: AnswerPostCreateEditModalComponent; - @Input() showAnswers: boolean; - @Input() isCommunicationPage: boolean; - @Input() sortedAnswerPosts: AnswerPost[]; @Output() openThread = new EventEmitter(); @Output() userReferenceClicked = new EventEmitter(); @Output() channelReferenceClicked = new EventEmitter(); - createdAnswerPost: AnswerPost; - isAtLeastTutorInCourse: boolean; + @ViewChild(AnswerPostCreateEditModalComponent) answerPostCreateEditModal?: AnswerPostCreateEditModalComponent; + @ViewChild('createEditAnswerPostContainer', { read: ViewContainerRef }) containerRef!: ViewContainerRef; + @ViewChild('createAnswerPostModal') createAnswerPostModalComponent!: AnswerPostCreateEditModalComponent; - // ng-container to render createEditAnswerPostComponent - @ViewChild('createEditAnswerPostContainer', { read: ViewContainerRef }) containerRef: ViewContainerRef; - @ViewChild('createAnswerPostModal') createAnswerPostModalComponent: AnswerPostCreateEditModalComponent; + createdAnswerPost: AnswerPost; + isAtLeastTutorInCourse = false; + courseId!: number; + groupedAnswerPosts: PostGroup[] = []; constructor( private metisService: MetisService, protected changeDetector: ChangeDetectorRef, ) { super(); + this.createdAnswerPost = this.createEmptyAnswerPost(); } - /** - * on initialization: updates the post tags and the context information - */ ngOnInit(): void { this.courseId = this.metisService.getCourse().id!; this.isAtLeastTutorInCourse = this.metisService.metisUserIsAtLeastTutorInCourse(); - this.createdAnswerPost = this.createEmptyAnswerPost(); + this.groupAnswerPosts(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['sortedAnswerPosts']) { + this.groupAnswerPosts(); + this.changeDetector.detectChanges(); + } } - /** - * on leaving the page, the container for answerPost creation or editing should be cleared - */ ngOnDestroy(): void { this.answerPostCreateEditModal?.createEditAnswerPostContainerRef?.clear(); } @@ -82,6 +99,61 @@ export class PostFooterComponent extends PostingFooterDirective implements return answerPost; } + private groupAnswerPosts(): void { + if (!this.sortedAnswerPosts || this.sortedAnswerPosts.length === 0) { + this.groupedAnswerPosts = []; + return; + } + + const sortedPosts: AnswerPost[] = [...this.sortedAnswerPosts].sort((a, b) => { + const aDate = a.creationDate ? dayjs(a.creationDate).toDate() : new Date(0); + const bDate = b.creationDate ? dayjs(b.creationDate).toDate() : new Date(0); + return aDate.getTime() - bDate.getTime(); + }); + + const groups: PostGroup[] = []; + let currentGroup: PostGroup = { + author: sortedPosts[0].author, + posts: [{ ...sortedPosts[0], isConsecutive: false }], + }; + + for (let i = 1; i < sortedPosts.length; i++) { + const currentPost = sortedPosts[i]; + const lastPostInGroup = currentGroup.posts[currentGroup.posts.length - 1]; + + const currentDate = currentPost.creationDate ? dayjs(currentPost.creationDate).toDate() : new Date(0); + const lastDate = lastPostInGroup.creationDate ? dayjs(lastPostInGroup.creationDate).toDate() : new Date(0); + + const timeDiff = Math.abs(currentDate.getTime() - lastDate.getTime()) / 60000; // in minutes + + if (currentPost.author?.id === currentGroup.author?.id && timeDiff <= 60) { + currentGroup.posts.push({ ...currentPost, isConsecutive: true }); + } else { + groups.push(currentGroup); + currentGroup = { + author: currentPost.author, + posts: [{ ...currentPost, isConsecutive: false }], + }; + } + } + + groups.push(currentGroup); + this.groupedAnswerPosts = groups; + } + + trackGroupByFn(_: number, group: PostGroup): number { + return group.posts[0].id!; + } + + trackPostByFn(_: number, post: AnswerPost): number { + return post.id!; + } + + isLastPost(group: PostGroup, answerPost: AnswerPost): boolean { + const lastPostInGroup = group.posts[group.posts.length - 1]; + return lastPostInGroup.id === answerPost.id; + } + /** * Open create answer modal */ 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 b869f78d9258..125eca92edf1 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 @@ -55,24 +55,6 @@
- @if (mayEditOrDelete) { - - } - @if (mayEditOrDelete) { - - } @if (!isAnswerOfAnnouncement) {
@if (posting.resolvesPost) { 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..16a01e9564ef 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 @@ -4,7 +4,6 @@ import { PostingHeaderDirective } from 'app/shared/metis/posting-header/posting- import { MetisService } from 'app/shared/metis/metis.service'; import { faCheck, faCog, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; import dayjs from 'dayjs/esm'; -import { getAsChannelDTO } from 'app/entities/metis/conversation/channel.model'; import { AccountService } from 'app/core/auth/account.service'; @Component({ @@ -21,7 +20,6 @@ export class AnswerPostHeaderComponent extends PostingHeaderDirective implements ngOnInit() { super.ngOnInit(); - this.setMayEditOrDelete(); } /** @@ -45,7 +43,6 @@ export class PostHeaderComponent extends PostingHeaderDirective implements */ ngOnChanges() { this.setUserProperties(); - this.setMayEditOrDelete(); this.setUserAuthorityIconAndTooltip(); } @@ -63,13 +60,5 @@ export class PostHeaderComponent extends PostingHeaderDirective implements this.metisService.deletePost(this.posting); } - setMayEditOrDelete(): void { - this.isAtLeastInstructorInCourse = this.metisService.metisUserIsAtLeastInstructorInCourse(); - const isCourseWideChannel = getAsChannelDTO(this.posting.conversation)?.isCourseWide ?? false; - const mayEditOrDeleteOtherUsersAnswer = - (isCourseWideChannel && this.isAtLeastInstructorInCourse) || (getAsChannelDTO(this.metisService.getCurrentConversation())?.hasChannelModerationRights ?? false); - this.mayEditOrDelete = !this.readOnlyMode && !this.previewMode && (this.isAuthorOfPosting || mayEditOrDeleteOtherUsersAnswer); - } - protected readonly CachingStrategy = CachingStrategy; } diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html index 8819002acd4d..989fcd3f9dad 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html @@ -1,50 +1,68 @@
@for (reactionMetaData of reactionMetaDataMap | keyvalue; track reactionMetaData) { -
- -
+ @if (isEmojiCount) { +
+ +
+ } }
- -
- @if (!isReadOnlyMode) { - - @if (!isReadOnlyMode) { - + + @if (!isReadOnlyMode) { + + + + } + + } + @if (!isEmojiCount && mayEditOrDelete) { + + + + } +
@if (isLastAnswer && !isThreadSidebar) {
diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts index bbd67d6324a8..cc3c3d918259 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts @@ -1,9 +1,10 @@ -import { Component, Input, OnChanges, OnInit } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; import { Reaction } from 'app/entities/metis/reaction.model'; -import { PostingsReactionsBarDirective } from 'app/shared/metis/posting-reactions-bar/posting-reactions-bar.component'; +import { PostingsReactionsBarDirective } from 'app/shared/metis/posting-reactions-bar/posting-reactions-bar.directive'; import { AnswerPost } from 'app/entities/metis/answer-post.model'; -import { faSmile } from '@fortawesome/free-regular-svg-icons'; +import { faPencilAlt, faSmile } from '@fortawesome/free-solid-svg-icons'; import { MetisService } from 'app/shared/metis/metis.service'; +import { getAsChannelDTO } from 'app/entities/metis/conversation/channel.model'; @Component({ selector: 'jhi-answer-post-reactions-bar', @@ -16,12 +17,37 @@ export class AnswerPostReactionsBarComponent extends PostingsReactionsBarDirecti @Input() isLastAnswer = false; // Icons - farSmile = faSmile; + readonly farSmile = faSmile; + @Output() openPostingCreateEditModal = new EventEmitter(); + isAuthorOfOriginalPost: boolean; + isAnswerOfAnnouncement: boolean; + mayEditOrDelete = false; + readonly faPencilAlt = faPencilAlt; + @Input() isEmojiCount: boolean = false; + @Output() postingUpdated = new EventEmitter(); constructor(metisService: MetisService) { super(metisService); } + ngOnInit() { + super.ngOnInit(); + this.setMayEditOrDelete(); + } + + ngOnChanges() { + super.ngOnChanges(); + this.setMayEditOrDelete(); + } + + isAnyReactionCountAboveZero(): boolean { + return Object.values(this.reactionMetaDataMap).some((reaction) => reaction.count >= 1); + } + + deletePosting(): void { + this.metisService.deleteAnswerPost(this.posting); + } + /** * builds and returns a Reaction model out of an emojiId and thereby sets the answerPost property properly * @param emojiId emojiId to build the model for @@ -32,4 +58,19 @@ export class AnswerPostReactionsBarComponent extends PostingsReactionsBarDirecti reaction.answerPost = this.posting; return reaction; } + + setMayEditOrDelete(): void { + // determines if the current user is the author of the original post, that the answer belongs to + this.isAuthorOfOriginalPost = this.metisService.metisUserIsAuthorOfPosting(this.posting.post!); + this.isAnswerOfAnnouncement = getAsChannelDTO(this.posting.post?.conversation)?.isAnnouncementChannel ?? false; + const isCourseWideChannel = getAsChannelDTO(this.posting.post?.conversation)?.isCourseWide ?? false; + const isAtLeastInstructorInCourse = this.metisService.metisUserIsAtLeastInstructorInCourse(); + const mayEditOrDeleteOtherUsersAnswer = + (isCourseWideChannel && isAtLeastInstructorInCourse) || (getAsChannelDTO(this.metisService.getCurrentConversation())?.hasChannelModerationRights ?? false); + this.mayEditOrDelete = !this.isReadOnlyMode && (this.isAuthorOfPosting || mayEditOrDeleteOtherUsersAnswer); + } + + onEditPosting() { + this.openPostingCreateEditModal.emit(); + } } diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html index d3ead68a6c0c..8e44c04cbb1f 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html @@ -32,7 +32,7 @@
} } @else { - @if (!isThreadSidebar) { + @if (!isEmojiCount && !isThreadSidebar) { @if (sortedAnswerPosts.length === 0) {
@@ -58,63 +58,86 @@ } } @for (reactionMetaData of reactionMetaDataMap | keyvalue; track reactionMetaData) { -
- -
+ @if (isEmojiCount) { +
+ +
+ } }
- -
- - @if (!readOnlyMode) { - + > + + + + + + + + + @if (!isEmojiCount && mayEditOrDelete) { + } - - @if (displayPriority === DisplayPriority.PINNED || canPin) { -
+ + @if (!isEmojiCount && mayEditOrDelete) { + + } + @if (!isEmojiCount && (displayPriority === DisplayPriority.PINNED || canPin)) { -
- } - + } +
@if (getShowNewMessageIcon()) {
diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts index abc4b835be61..23127b8f74a4 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts @@ -1,25 +1,26 @@ -import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; import { Reaction } from 'app/entities/metis/reaction.model'; import { Post } from 'app/entities/metis/post.model'; -import { PostingsReactionsBarDirective } from 'app/shared/metis/posting-reactions-bar/posting-reactions-bar.component'; +import { PostingsReactionsBarDirective } from 'app/shared/metis/posting-reactions-bar/posting-reactions-bar.directive'; import { DisplayPriority } from 'app/shared/metis/metis.util'; import { MetisService } from 'app/shared/metis/metis.service'; -import { faSmile } from '@fortawesome/free-regular-svg-icons'; -import { faArrowRight } from '@fortawesome/free-solid-svg-icons'; +import { faTrashAlt } from '@fortawesome/free-regular-svg-icons'; +import { faArrowRight, faPencilAlt, faSmile, faThumbTack } from '@fortawesome/free-solid-svg-icons'; import { AnswerPost } from 'app/entities/metis/answer-post.model'; import dayjs from 'dayjs/esm'; -import { isChannelDTO } from 'app/entities/metis/conversation/channel.model'; +import { getAsChannelDTO, isChannelDTO } from 'app/entities/metis/conversation/channel.model'; import { isGroupChatDTO } from 'app/entities/metis/conversation/group-chat.model'; import { AccountService } from 'app/core/auth/account.service'; import { isOneToOneChatDTO } from 'app/entities/metis/conversation/one-to-one-chat.model'; import { ConversationDTO } from 'app/entities/metis/conversation/conversation.model'; +import { PostCreateEditModalComponent } from 'app/shared/metis/posting-create-edit-modal/post-create-edit-modal/post-create-edit-modal.component'; @Component({ selector: 'jhi-post-reactions-bar', templateUrl: './post-reactions-bar.component.html', styleUrls: ['../posting-reactions-bar.component.scss'], }) -export class PostReactionsBarComponent extends PostingsReactionsBarDirective implements OnInit, OnChanges { +export class PostReactionsBarComponent extends PostingsReactionsBarDirective implements OnInit, OnChanges, OnDestroy { pinTooltip: string; displayPriority: DisplayPriority; canPin = false; @@ -28,6 +29,9 @@ export class PostReactionsBarComponent extends PostingsReactionsBarDirective(); @Output() openPostingCreateEditModal = new EventEmitter(); @Output() openThread = new EventEmitter(); + @Input() previewMode: boolean; + isAtLeastInstructorInCourse: boolean; + mayEditOrDelete = false; + @ViewChild(PostCreateEditModalComponent) postCreateEditModal?: PostCreateEditModalComponent; + @Input() isEmojiCount: boolean = false; constructor( metisService: MetisService, @@ -47,6 +56,10 @@ export class PostReactionsBarComponent extends PostingsReactionsBarDirective reaction.count >= 1); + } + /** * on initialization: call resetTooltipsAndPriority */ @@ -56,6 +69,11 @@ export class PostReactionsBarComponent extends PostingsReactionsBarDirective(); + @Input() isConsecutive: boolean | undefined = false; } From fedc900c79f80a232a2af22e36551dc34140122a Mon Sep 17 00:00:00 2001 From: Asli Aykan Date: Thu, 10 Oct 2024 20:49:06 +0200 Subject: [PATCH 02/31] renamed file --- ...ent.ts => posting-reactions-bar.directive.ts} | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) rename src/main/webapp/app/shared/metis/posting-reactions-bar/{posting-reactions-bar.component.ts => posting-reactions-bar.directive.ts} (90%) diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/posting-reactions-bar.component.ts b/src/main/webapp/app/shared/metis/posting-reactions-bar/posting-reactions-bar.directive.ts similarity index 90% rename from src/main/webapp/app/shared/metis/posting-reactions-bar/posting-reactions-bar.component.ts rename to src/main/webapp/app/shared/metis/posting-reactions-bar/posting-reactions-bar.directive.ts index 614952f32e9c..e85328b1542f 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/posting-reactions-bar.component.ts +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/posting-reactions-bar.directive.ts @@ -50,9 +50,13 @@ export abstract class PostingsReactionsBarDirective implement @Input() posting: T; @Input() isThreadSidebar: boolean; @Output() openPostingCreateEditModal = new EventEmitter(); + @Output() reactionsUpdated = new EventEmitter(); showReactionSelector = false; currentUserIsAtLeastTutor: boolean; + isAtLeastTutorInCourse: boolean; + isAuthorOfPosting: boolean; + @Output() isModalOpen = new EventEmitter(); /* * icons (as svg paths) to be used as category preview image in emoji mart selector @@ -101,6 +105,12 @@ export abstract class PostingsReactionsBarDirective implement ngOnInit(): void { this.updatePostingWithReactions(); this.currentUserIsAtLeastTutor = this.metisService.metisUserIsAtLeastTutorInCourse(); + this.isAuthorOfPosting = this.metisService.metisUserIsAuthorOfPosting(this.posting); + this.isAtLeastTutorInCourse = this.metisService.metisUserIsAtLeastTutorInCourse(); + } + + deletePosting(): void { + this.metisService.deletePost(this.posting); } /** @@ -136,6 +146,7 @@ export abstract class PostingsReactionsBarDirective implement * i.e. when agree on an existing reaction (+1) or removing own reactions (-1) */ updateReaction(emojiId: string): void { + console.log('update e girdi mii'); if (emojiId != undefined) { this.addOrRemoveReaction(emojiId); } @@ -153,12 +164,17 @@ export abstract class PostingsReactionsBarDirective implement if (this.posting.reactions && existingReactionIdx > -1) { const reactionToDelete = this.posting.reactions[existingReactionIdx]; this.metisService.deleteReaction(reactionToDelete).subscribe(() => { + this.posting.reactions = this.posting.reactions?.filter((reaction) => reaction.id !== reactionToDelete.id); + this.updatePostingWithReactions(); this.showReactionSelector = false; + this.reactionsUpdated.emit(this.posting.reactions); }); } else { const reactionToCreate = this.buildReaction(emojiId); this.metisService.createReaction(reactionToCreate).subscribe(() => { + this.updatePostingWithReactions(); this.showReactionSelector = false; + this.reactionsUpdated.emit(this.posting.reactions); }); } } From 1113a4b172c6b33c9c6ef38009fd0c975ac34a5c Mon Sep 17 00:00:00 2001 From: Asli Aykan Date: Thu, 10 Oct 2024 21:21:39 +0200 Subject: [PATCH 03/31] tests updated --- .../conversation-messages.component.ts | 2 +- .../post-footer/post-footer.component.ts | 4 +- .../conversation-messages.component.spec.ts | 20 +++++ .../shared/metis/post/post.component.spec.ts | 5 -- .../post-footer/post-footer.component.spec.ts | 90 +++++++++++++++---- 5 files changed, 95 insertions(+), 26 deletions(-) diff --git a/src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.ts b/src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.ts index 6d6130b89cd3..de438cf9467b 100644 --- a/src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.ts +++ b/src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.ts @@ -226,7 +226,7 @@ export class ConversationMessagesComponent implements OnInit, AfterViewInit, OnD this.cdr.detectChanges(); } - private setPosts(posts: Post[]): void { + setPosts(posts: Post[]): void { if (this.content) { this.previousScrollDistanceFromTop = this.content.nativeElement.scrollHeight - this.content.nativeElement.scrollTop; } diff --git a/src/main/webapp/app/shared/metis/posting-footer/post-footer/post-footer.component.ts b/src/main/webapp/app/shared/metis/posting-footer/post-footer/post-footer.component.ts index 34024e5ac295..d87d35e68a06 100644 --- a/src/main/webapp/app/shared/metis/posting-footer/post-footer/post-footer.component.ts +++ b/src/main/webapp/app/shared/metis/posting-footer/post-footer/post-footer.component.ts @@ -59,12 +59,12 @@ export class PostFooterComponent extends PostingFooterDirective implements protected changeDetector: ChangeDetectorRef, ) { super(); - this.createdAnswerPost = this.createEmptyAnswerPost(); } ngOnInit(): void { this.courseId = this.metisService.getCourse().id!; this.isAtLeastTutorInCourse = this.metisService.metisUserIsAtLeastTutorInCourse(); + this.createdAnswerPost = this.createEmptyAnswerPost(); this.groupAnswerPosts(); } @@ -99,7 +99,7 @@ export class PostFooterComponent extends PostingFooterDirective implements return answerPost; } - private groupAnswerPosts(): void { + groupAnswerPosts(): void { if (!this.sortedAnswerPosts || this.sortedAnswerPosts.length === 0) { this.groupedAnswerPosts = []; return; diff --git a/src/test/javascript/spec/component/overview/course-conversations/layout/conversation-messages/conversation-messages.component.spec.ts b/src/test/javascript/spec/component/overview/course-conversations/layout/conversation-messages/conversation-messages.component.spec.ts index cb41b848f929..826a0cdea4d2 100644 --- a/src/test/javascript/spec/component/overview/course-conversations/layout/conversation-messages/conversation-messages.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-conversations/layout/conversation-messages/conversation-messages.component.spec.ts @@ -19,6 +19,7 @@ import { By } from '@angular/platform-browser'; import { Course } from 'app/entities/course.model'; import { getAsChannelDTO } from 'app/entities/metis/conversation/channel.model'; import { PostCreateEditModalComponent } from 'app/shared/metis/posting-create-edit-modal/post-create-edit-modal/post-create-edit-modal.component'; +import dayjs from 'dayjs'; const examples: ConversationDTO[] = [ generateOneToOneChatDTO({}), @@ -140,6 +141,25 @@ examples.forEach((activeConversation) => { expect(conversation!.id).toEqual(activeConversation.id); })); + it('should set posts and group them correctly', () => { + // Mock posts with different creation dates and authors + const posts = [ + { id: 1, creationDate: dayjs().subtract(2, 'hours'), author: { id: 1 } } as Post, + { id: 2, creationDate: dayjs().subtract(1, 'minutes'), author: { id: 1 } } as Post, + { id: 3, creationDate: dayjs(), author: { id: 2 } } as Post, + { id: 4, creationDate: dayjs().subtract(30, 'minutes'), author: { id: 1 } } as Post, + ]; + + component.setPosts(posts); + + expect(component.posts).toHaveLength(4); + expect(component.groupedPosts).toHaveLength(3); + + expect(component.groupedPosts[0].posts).toHaveLength(1); + expect(component.groupedPosts[1].posts).toHaveLength(2); + expect(component.groupedPosts[2].posts).toHaveLength(1); + }); + if (getAsChannelDTO(activeConversation)?.isAnnouncementChannel) { it('should display the "new announcement" button when the conversation is an announcement channel', fakeAsync(() => { const announcementButton = fixture.debugElement.query(By.css('.btn.btn-md.btn-primary')); 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 a5618407cc41..57501c525f35 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 @@ -105,11 +105,6 @@ describe('PostComponent', () => { expect(component.sortedAnswerPosts).toEqual([]); }); - it('should contain a post header', () => { - const header = getElement(debugElement, 'jhi-post-header'); - expect(header).not.toBeNull(); - }); - it('should set router link and query params', () => { metisServiceGetLinkSpy = jest.spyOn(metisService, 'getLinkForPost'); metisServiceGetQueryParamsSpy = jest.spyOn(metisService, 'getQueryParamsForPost'); diff --git a/src/test/javascript/spec/component/shared/metis/postings-footer/post-footer/post-footer.component.spec.ts b/src/test/javascript/spec/component/shared/metis/postings-footer/post-footer/post-footer.component.spec.ts index a6cffee4a26d..45c38925db52 100644 --- a/src/test/javascript/spec/component/shared/metis/postings-footer/post-footer/post-footer.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/postings-footer/post-footer/post-footer.component.spec.ts @@ -15,6 +15,13 @@ import { AnswerPostCreateEditModalComponent } from 'app/shared/metis/posting-cre import { TranslatePipeMock } from '../../../../../helpers/mocks/service/mock-translate.service'; import { MockMetisService } from '../../../../../helpers/mocks/service/mock-metis-service.service'; import { metisPostExerciseUser1, post, unsortedAnswerArray } from '../../../../../helpers/sample/metis-sample-data'; +import { AnswerPost } from 'app/entities/metis/answer-post.model'; +import { User } from 'app/core/user/user.model'; + +interface PostGroup { + author: User | undefined; + posts: AnswerPost[]; +} describe('PostFooterComponent', () => { let component: PostFooterComponent; @@ -62,6 +69,71 @@ describe('PostFooterComponent', () => { expect(component.createdAnswerPost.resolvesPost).toBeTrue(); }); + it('should group answer posts correctly', () => { + component.sortedAnswerPosts = unsortedAnswerArray; + component.groupAnswerPosts(); + expect(component.groupedAnswerPosts.length).toBeGreaterThan(0); // Ensure groups are created + expect(component.groupedAnswerPosts[0].posts.length).toBeGreaterThan(0); // Ensure posts exist in groups + }); + + it('should group answer posts and detect changes on changes to sortedAnswerPosts input', () => { + const changeDetectorSpy = jest.spyOn(component['changeDetector'], 'detectChanges'); + component.sortedAnswerPosts = unsortedAnswerArray; + component.ngOnChanges({ sortedAnswerPosts: { currentValue: unsortedAnswerArray, previousValue: [], firstChange: true, isFirstChange: () => true } }); + expect(component.groupedAnswerPosts.length).toBeGreaterThan(0); + expect(changeDetectorSpy).toHaveBeenCalled(); + }); + + it('should clear answerPostCreateEditModal container on destroy', () => { + const mockContainerRef = { clear: jest.fn() } as any; + component.answerPostCreateEditModal = { + createEditAnswerPostContainerRef: mockContainerRef, + } as AnswerPostCreateEditModalComponent; + + const clearSpy = jest.spyOn(mockContainerRef, 'clear'); + component.ngOnDestroy(); + expect(clearSpy).toHaveBeenCalled(); + }); + + it('should return the ID of the post in trackPostByFn', () => { + const mockPost: AnswerPost = { id: 200 } as AnswerPost; + + const result = component.trackPostByFn(0, mockPost); + expect(result).toBe(200); + }); + + it('should return the ID of the first post in the group in trackGroupByFn', () => { + const mockGroup: PostGroup = { + author: { id: 1, login: 'user1' } as User, + posts: [{ id: 100, author: { id: 1 } } as AnswerPost], + }; + + const result = component.trackGroupByFn(0, mockGroup); + expect(result).toBe(100); + }); + + it('should return true if the post is the last post in the group in isLastPost', () => { + const mockPost: AnswerPost = { id: 300 } as AnswerPost; + const mockGroup: PostGroup = { + author: { id: 1, login: 'user1' } as User, + posts: [{ id: 100, author: { id: 1 } } as AnswerPost, mockPost], + }; + + const result = component.isLastPost(mockGroup, mockPost); + expect(result).toBeTrue(); + }); + + it('should return false if the post is not the last post in the group in isLastPost', () => { + const mockPost: AnswerPost = { id: 100 } as AnswerPost; + const mockGroup: PostGroup = { + author: { id: 1, login: 'user1' } as User, + posts: [mockPost, { id: 300, author: { id: 1 } } as AnswerPost], + }; + + const result = component.isLastPost(mockGroup, mockPost); + expect(result).toBeFalse(); + }); + it('should be initialized correctly for users that are not at least tutors in course', () => { component.posting = post; component.posting.answers = unsortedAnswerArray; @@ -71,24 +143,6 @@ describe('PostFooterComponent', () => { expect(component.createdAnswerPost.resolvesPost).toBeFalse(); }); - it('should not contain an answer post', () => { - component.posting = post; - component.posting.answers = unsortedAnswerArray; - component.showAnswers = false; - fixture.detectChanges(); - const answerPostComponent = fixture.debugElement.nativeElement.querySelector('jhi-answer-post'); - expect(answerPostComponent).toBeNull(); - }); - - it('should contain reference to container for rendering answerPostCreateEditModal component', () => { - expect(component.containerRef).not.toBeNull(); - }); - - it('should contain component to create a new answer post', () => { - const answerPostCreateEditModal = fixture.debugElement.nativeElement.querySelector('jhi-answer-post-create-edit-modal'); - expect(answerPostCreateEditModal).not.toBeNull(); - }); - it('should open create answer post modal', () => { component.posting = metisPostExerciseUser1; component.ngOnInit(); From 8a487238c1eecaf75b1b2ccf20925b6f046a1498 Mon Sep 17 00:00:00 2001 From: Asli Aykan Date: Fri, 11 Oct 2024 17:43:55 +0200 Subject: [PATCH 04/31] tests updated --- .../post-header/post-header.component.html | 21 --- .../post-header/post-header.component.ts | 1 - .../answer-post-reactions-bar.component.html | 4 +- .../post-reactions-bar.component.html | 2 +- .../posting-reactions-bar.directive.ts | 1 - .../answer-post-header.component.spec.ts | 83 +--------- .../post-header/post-header.component.spec.ts | 69 +------- ...nswer-post-reactions-bar.component.spec.ts | 97 +++++++++++- .../post-reactions-bar.component.spec.ts | 147 +++++++++++++++++- 9 files changed, 241 insertions(+), 184 deletions(-) diff --git a/src/main/webapp/app/shared/metis/posting-header/post-header/post-header.component.html b/src/main/webapp/app/shared/metis/posting-header/post-header/post-header.component.html index 0470927f971e..de9ed9b2e74b 100644 --- a/src/main/webapp/app/shared/metis/posting-header/post-header/post-header.component.html +++ b/src/main/webapp/app/shared/metis/posting-header/post-header/post-header.component.html @@ -61,25 +61,4 @@ }
-
- @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 469215e436ac..68cf05384e01 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 @@ -20,7 +20,6 @@ export class PostHeaderComponent extends PostingHeaderDirective implements @Input() previewMode: boolean; @ViewChild(PostCreateEditModalComponent) postCreateEditModal?: PostCreateEditModalComponent; isAtLeastInstructorInCourse: boolean; - mayEditOrDelete = false; // Icons faPencilAlt = faPencilAlt; diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html index 989fcd3f9dad..bb27900a610d 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html @@ -49,11 +49,11 @@ } @if (!isEmojiCount && mayEditOrDelete) { - - + } diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts index cc3c3d918259..8f3ae89e0c58 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts @@ -2,7 +2,7 @@ import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angu import { Reaction } from 'app/entities/metis/reaction.model'; import { PostingsReactionsBarDirective } from 'app/shared/metis/posting-reactions-bar/posting-reactions-bar.directive'; import { AnswerPost } from 'app/entities/metis/answer-post.model'; -import { faPencilAlt, faSmile } from '@fortawesome/free-solid-svg-icons'; +import { faCheck, faPencilAlt, faSmile } from '@fortawesome/free-solid-svg-icons'; import { MetisService } from 'app/shared/metis/metis.service'; import { getAsChannelDTO } from 'app/entities/metis/conversation/channel.model'; @@ -18,6 +18,7 @@ export class AnswerPostReactionsBarComponent extends PostingsReactionsBarDirecti @Input() isLastAnswer = false; // Icons readonly farSmile = faSmile; + readonly faCheck = faCheck; @Output() openPostingCreateEditModal = new EventEmitter(); isAuthorOfOriginalPost: boolean; isAnswerOfAnnouncement: boolean; @@ -73,4 +74,15 @@ export class AnswerPostReactionsBarComponent extends PostingsReactionsBarDirecti onEditPosting() { this.openPostingCreateEditModal.emit(); } + + /** + * toggles the resolvesPost property of an answer post if the user is at least tutor in a course or the user is the author of the original post, + * delegates the update to the metis service + */ + toggleResolvesPost(): void { + if (this.isAtLeastTutorInCourse || this.isAuthorOfOriginalPost) { + this.posting.resolvesPost = !this.posting.resolvesPost; + this.metisService.updateAnswerPost(this.posting).subscribe(); + } + } } 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 4d12dc8079e1..38a54e3c3e2e 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 @@ -29,7 +29,6 @@ describe('AnswerPostHeaderComponent', () => { let metisService: MetisService; let metisServiceUserIsAtLeastTutorMock: jest.SpyInstance; let metisServiceUserPostingAuthorMock: jest.SpyInstance; - let metisServiceUpdateAnswerPostMock: jest.SpyInstance; const yesterday: dayjs.Dayjs = dayjs().subtract(1, 'day'); @@ -64,7 +63,6 @@ describe('AnswerPostHeaderComponent', () => { metisService = TestBed.inject(MetisService); metisServiceUserIsAtLeastTutorMock = jest.spyOn(metisService, 'metisUserIsAtLeastTutorInCourse'); metisServiceUserPostingAuthorMock = jest.spyOn(metisService, 'metisUserIsAuthorOfPosting'); - metisServiceUpdateAnswerPostMock = jest.spyOn(metisService, 'updateAnswerPost'); debugElement = fixture.debugElement; component.posting = metisResolvingAnswerPostUser1; component.posting.creationDate = yesterday; @@ -97,25 +95,6 @@ describe('AnswerPostHeaderComponent', () => { expect(getElement(debugElement, '#today-flag')).toBeNull(); }); - it('should initialize answer post as marked as resolved', () => { - metisServiceUserIsAtLeastTutorMock.mockReturnValue(false); - fixture.detectChanges(); - expect(component.posting.resolvesPost).toBeTruthy(); - expect(getElement(debugElement, '.resolved')).not.toBeNull(); - expect(getElement(debugElement, '.notResolved')).toBeNull(); - }); - - it('should initialize answer post not marked as resolved but show the check to mark it as such', () => { - // tutors should see the check to mark an answer post as resolving - metisServiceUserIsAtLeastTutorMock.mockReturnValue(true); - // answer post that is not resolving original post - component.posting = metisAnswerPostUser2; - component.ngOnInit(); - fixture.detectChanges(); - expect(getElement(debugElement, '.resolved')).toBeNull(); - expect(getElement(debugElement, '.notResolved')).not.toBeNull(); - }); - it('should initialize answer post not marked as resolved and not show the check to mark it as such', () => { // user, that is not author of original post, should not see the check to mark an answer post as resolving metisServiceUserIsAtLeastTutorMock.mockReturnValue(false); @@ -127,15 +106,4 @@ describe('AnswerPostHeaderComponent', () => { expect(getElement(debugElement, '.resolved')).toBeNull(); expect(getElement(debugElement, '.notResolved')).toBeNull(); }); - - it('should invoke metis service when toggle resolve is clicked as tutor', () => { - metisServiceUserIsAtLeastTutorMock.mockReturnValue(true); - fixture.detectChanges(); - const resolveIcon = getElement(debugElement, '.resolved'); - expect(resolveIcon).not.toBeNull(); - const previousState = component.posting.resolvesPost; - component.toggleResolvesPost(); - expect(component.posting.resolvesPost).toEqual(!previousState); - expect(metisServiceUpdateAnswerPostMock).toHaveBeenCalledOnce(); - }); }); diff --git a/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.spec.ts b/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.spec.ts index c384dded3e5e..f99c277ec924 100644 --- a/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.spec.ts @@ -44,6 +44,7 @@ describe('AnswerPostReactionsBarComponent', () => { let metisServiceUserIsAtLeastInstructorMock: jest.SpyInstance; let metisServiceUserPostingAuthorMock: jest.SpyInstance; let metisServiceDeleteAnswerPostMock: jest.SpyInstance; + let metisServiceUpdateAnswerPostMock: jest.SpyInstance; beforeEach(() => { return TestBed.configureTestingModule({ @@ -78,6 +79,7 @@ describe('AnswerPostReactionsBarComponent', () => { metisServiceUserIsAtLeastInstructorMock = jest.spyOn(metisService, 'metisUserIsAtLeastInstructorInCourse'); metisServiceUserPostingAuthorMock = jest.spyOn(metisService, 'metisUserIsAuthorOfPosting'); metisServiceDeleteAnswerPostMock = jest.spyOn(metisService, 'deleteAnswerPost'); + metisServiceUpdateAnswerPostMock = jest.spyOn(metisService, 'updateAnswerPost'); component = fixture.componentInstance; answerPost = new AnswerPost(); answerPost.id = 1; @@ -105,6 +107,10 @@ describe('AnswerPostReactionsBarComponent', () => { return debugElement.query(By.css('.delete')); } + function getResolveButton(): DebugElement | null { + return debugElement.query(By.css('#toggleElement')); + } + it('should invoke metis service method with correctly built reaction to create it', () => { component.ngOnInit(); fixture.detectChanges(); @@ -213,4 +219,14 @@ describe('AnswerPostReactionsBarComponent', () => { const answerNowButton = fixture.debugElement.query(By.css('.reply-btn')).nativeElement; expect(answerNowButton.innerHTML).toContain('reply'); }); + + it('should invoke metis service when toggle resolve is clicked', () => { + metisServiceUserPostingAuthorMock.mockReturnValue(true); + fixture.detectChanges(); + expect(getResolveButton()).not.toBeNull(); + const previousState = component.posting.resolvesPost; + component.toggleResolvesPost(); + expect(component.posting.resolvesPost).toEqual(!previousState); + expect(metisServiceUpdateAnswerPostMock).toHaveBeenCalledOnce(); + }); }); From a53c7ba827ada22edfa98312d9e039dc233d3115 Mon Sep 17 00:00:00 2001 From: Asli Aykan Date: Sat, 12 Oct 2024 22:33:41 +0200 Subject: [PATCH 10/31] updated reaction bar style --- .../app/shared/metis/answer-post/answer-post.component.scss | 1 + src/main/webapp/app/shared/metis/post/post.component.scss | 1 + .../answer-post-reactions-bar.component.html | 2 +- .../post-reactions-bar/post-reactions-bar.component.html | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) 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 0e79ff344b44..cea9d9adaac9 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 @@ -34,6 +34,7 @@ background: var(--metis-selection-option-background); padding: 5px; border-radius: 5px; + border: 0.01rem solid var(--metis-gray); } .message-container:hover .hover-actions { diff --git a/src/main/webapp/app/shared/metis/post/post.component.scss b/src/main/webapp/app/shared/metis/post/post.component.scss index 8b13aba1bb16..d11dadece0aa 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.scss +++ b/src/main/webapp/app/shared/metis/post/post.component.scss @@ -42,6 +42,7 @@ background: var(--metis-selection-option-background); padding: 5px; border-radius: 5px; + border: 0.01rem solid var(--metis-gray); } .message-container:hover .hover-actions { diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html index 0dcd46c5a822..f01a17e8cf69 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html @@ -1,4 +1,4 @@ -
+
@for (reactionMetaData of reactionMetaDataMap | keyvalue; track reactionMetaData) { @if (isEmojiCount) {
diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html index 0e7cd49345a2..fc71a802fef4 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html @@ -1,4 +1,4 @@ -
+
@if (!isCommunicationPage) { @if (sortedAnswerPosts.length) { From 6794ca19359e8d505c4e9cbbac4f9f6607940e89 Mon Sep 17 00:00:00 2001 From: Asli Aykan Date: Mon, 21 Oct 2024 17:19:47 +0200 Subject: [PATCH 11/31] added dropdown --- .../app/shared/metis/post/post.component.html | 24 +++++- .../app/shared/metis/post/post.component.scss | 15 ++++ .../app/shared/metis/post/post.component.ts | 76 ++++++++++++++++++- .../post-reactions-bar.component.html | 2 +- .../post-reactions-bar.component.ts | 9 +++ 5 files changed, 123 insertions(+), 3 deletions(-) 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 db519ba894c8..996f3611b5ea 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.html +++ b/src/main/webapp/app/shared/metis/post/post.component.html @@ -50,7 +50,7 @@ }
@if (!displayInlineInput) { -
+
+ + + + +
+ + + + diff --git a/src/main/webapp/app/shared/metis/post/post.component.scss b/src/main/webapp/app/shared/metis/post/post.component.scss index d11dadece0aa..3ef04651beba 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.scss +++ b/src/main/webapp/app/shared/metis/post/post.component.scss @@ -19,6 +19,15 @@ position: relative; border-radius: 5px; transition: background-color 0.3s ease; + + &.force-hover { + background: var(--metis-selection-option-hover-background); + + .hover-actions { + opacity: 1; + visibility: visible; + } + } } .message-content { @@ -53,3 +62,9 @@ .clickable { cursor: pointer; } + +.item-icon { + width: 20px; + height: 20px; + margin-right: 0.2rem; +} diff --git a/src/main/webapp/app/shared/metis/post/post.component.ts b/src/main/webapp/app/shared/metis/post/post.component.ts index cb36d154282c..10dead08335c 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.ts +++ b/src/main/webapp/app/shared/metis/post/post.component.ts @@ -4,6 +4,7 @@ import { ChangeDetectorRef, Component, EventEmitter, + HostListener, Input, OnChanges, OnInit, @@ -16,7 +17,7 @@ import { PostingDirective } from 'app/shared/metis/posting.directive'; import { MetisService } from 'app/shared/metis/metis.service'; import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { ContextInformation, DisplayPriority, PageType, RouteComponents } from '../metis.util'; -import { faBullhorn, faPencilAlt, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { faBullhorn, faComments, faPencilAlt, faSmile, faThumbtack, faTrash } from '@fortawesome/free-solid-svg-icons'; import dayjs from 'dayjs/esm'; import { PostFooterComponent } from 'app/shared/metis/posting-footer/post-footer/post-footer.component'; import { OneToOneChatService } from 'app/shared/metis/conversations/one-to-one-chat.service'; @@ -26,6 +27,9 @@ import { MetisConversationService } from 'app/shared/metis/metis-conversation.se import { getAsChannelDTO } from 'app/entities/metis/conversation/channel.model'; import { AnswerPost } from 'app/entities/metis/answer-post.model'; import { AnswerPostCreateEditModalComponent } from 'app/shared/metis/posting-create-edit-modal/answer-post-create-edit-modal/answer-post-create-edit-modal.component'; +import { PostCreateEditModalComponent } from 'app/shared/metis/posting-create-edit-modal/post-create-edit-modal/post-create-edit-modal.component'; +import { PostReactionsBarComponent } from 'app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component'; +import { CdkOverlayOrigin } from '@angular/cdk/overlay'; @Component({ selector: 'jhi-post', @@ -43,8 +47,13 @@ export class PostComponent extends PostingDirective implements OnInit, OnC @Input() showAnswers: boolean; @Output() openThread = new EventEmitter(); @ViewChild('createAnswerPostModal') createAnswerPostModalComponent: AnswerPostCreateEditModalComponent; + @ViewChild('createEditModal') createEditModal!: PostCreateEditModalComponent; + @ViewChild(PostReactionsBarComponent) reactionsBar!: PostReactionsBarComponent; @ViewChild('createEditAnswerPostContainer', { read: ViewContainerRef }) containerRef: ViewContainerRef; @ViewChild('postFooter') postFooterComponent: PostFooterComponent; + showReactionSelector = false; + @ViewChild('emojiPickerTrigger') emojiPickerTrigger!: CdkOverlayOrigin; + static activeDropdownPost: PostComponent | null = null; displayInlineInput = false; routerLink: RouteComponents; @@ -61,9 +70,13 @@ export class PostComponent extends PostingDirective implements OnInit, OnC // Icons readonly faBullhorn = faBullhorn; + readonly faComments = faComments; readonly faPencilAlt = faPencilAlt; + readonly faSmile = faSmile; readonly faTrash = faTrash; + readonly faThumbtack = faThumbtack; @Input() isConsecutive: boolean = false; + clickPosition = { x: 0, y: 0 }; constructor( private metisService: MetisService, @@ -74,6 +87,67 @@ export class PostComponent extends PostingDirective implements OnInit, OnC ) { super(); } + showDropdown = false; + dropdownPosition = { x: 0, y: 0 }; + + onRightClick(event: MouseEvent) { + event.preventDefault(); + + if (PostComponent.activeDropdownPost && PostComponent.activeDropdownPost !== this) { + PostComponent.activeDropdownPost.showDropdown = false; + PostComponent.activeDropdownPost.changeDetector.detectChanges(); + } + + PostComponent.activeDropdownPost = this; + + this.dropdownPosition = { + x: event.clientX, + y: event.clientY, + }; + + this.showDropdown = true; + } + + editPosting() { + this.reactionsBar.editPosting(); + this.showDropdown = false; + } + + togglePin() { + this.reactionsBar.togglePin(); + this.showDropdown = false; + } + + deletePost() { + this.reactionsBar.deletePosting(); + this.showDropdown = false; + } + + selectReaction(event: any) { + this.reactionsBar.selectReaction(event); + this.showReactionSelector = false; + } + + addReaction(event: MouseEvent) { + event.preventDefault(); + this.showDropdown = false; + + this.clickPosition = { + x: event.clientX, + y: event.clientY, + }; + + this.showReactionSelector = true; + } + + toggleEmojiSelect() { + this.showReactionSelector = !this.showReactionSelector; + } + + @HostListener('document:click', ['$event']) + onClickOutside() { + this.showDropdown = false; + } /** * on initialization: evaluates post context and page type diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html index fc71a802fef4..96bc23838bbf 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html @@ -109,7 +109,7 @@
diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html index 96bc23838bbf..e6a2ad7a831d 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html @@ -32,9 +32,9 @@
} } @else { - @if (!isEmojiCount && !isThreadSidebar) { + @if (!isThreadSidebar) { - @if (sortedAnswerPosts.length === 0) { + @if (hoverBar && sortedAnswerPosts.length === 0) {
+ + + +
+ +
+ + + + 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 cea9d9adaac9..1913756b66f2 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 @@ -15,6 +15,15 @@ .message-content { padding-left: 0.3rem; + + &.force-hover { + background: var(--metis-selection-option-hover-background); + + .hover-actions { + opacity: 1; + visibility: visible; + } + } } .message-content:hover { @@ -45,3 +54,9 @@ .clickable { cursor: pointer; } + +.item-icon { + width: 20px; + height: 20px; + margin-right: 0.2rem; +} 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 59a69ae792c0..1b76cc3ebabe 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 @@ -1,9 +1,11 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ViewChild, ViewContainerRef } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostListener, Input, Output, ViewChild, ViewContainerRef } from '@angular/core'; import { AnswerPost } from 'app/entities/metis/answer-post.model'; import { PostingDirective } from 'app/shared/metis/posting.directive'; import dayjs from 'dayjs/esm'; import { Posting } from 'app/entities/metis/posting.model'; import { Reaction } from 'app/entities/metis/reaction.model'; +import { faComments, faPencilAlt, faSmile, faThumbtack, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { AnswerPostReactionsBarComponent } from 'app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component'; @Component({ selector: 'jhi-answer-post', @@ -23,6 +25,24 @@ export class AnswerPostComponent extends PostingDirective { // ng-container to render answerPostCreateEditModalComponent @ViewChild('createEditAnswerPostContainer', { read: ViewContainerRef }) containerRef: ViewContainerRef; @Input() isConsecutive: boolean = false; + readonly faComments = faComments; + readonly faPencilAlt = faPencilAlt; + readonly faSmile = faSmile; + readonly faTrash = faTrash; + readonly faThumbtack = faThumbtack; + static activeDropdownPost: AnswerPostComponent | null = null; + + @ViewChild(AnswerPostReactionsBarComponent) + private reactionsBarComponent!: AnswerPostReactionsBarComponent; + + constructor(protected changeDetector: ChangeDetectorRef) { + super(); + } + + // Implement the abstract getter + protected get reactionsBar() { + return this.reactionsBarComponent; + } onPostingUpdated(updatedPosting: Posting) { this.posting = updatedPosting; @@ -31,4 +51,27 @@ export class AnswerPostComponent extends PostingDirective { onReactionsUpdated(updatedReactions: Reaction[]) { this.posting = { ...this.posting, reactions: updatedReactions }; } + + @HostListener('document:click', ['$event']) + onClickOutside() { + this.showDropdown = false; + } + + onRightClick(event: MouseEvent) { + event.preventDefault(); + + if (AnswerPostComponent.activeDropdownPost && AnswerPostComponent.activeDropdownPost !== this) { + AnswerPostComponent.activeDropdownPost.showDropdown = false; + AnswerPostComponent.activeDropdownPost.changeDetector.detectChanges(); + } + + AnswerPostComponent.activeDropdownPost = this; + + this.dropdownPosition = { + x: event.clientX, + y: event.clientY, + }; + + this.showDropdown = true; + } } diff --git a/src/main/webapp/app/shared/metis/post/post.component.ts b/src/main/webapp/app/shared/metis/post/post.component.ts index 10dead08335c..4c2986cdfd61 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.ts +++ b/src/main/webapp/app/shared/metis/post/post.component.ts @@ -48,7 +48,6 @@ export class PostComponent extends PostingDirective implements OnInit, OnC @Output() openThread = new EventEmitter(); @ViewChild('createAnswerPostModal') createAnswerPostModalComponent: AnswerPostCreateEditModalComponent; @ViewChild('createEditModal') createEditModal!: PostCreateEditModalComponent; - @ViewChild(PostReactionsBarComponent) reactionsBar!: PostReactionsBarComponent; @ViewChild('createEditAnswerPostContainer', { read: ViewContainerRef }) containerRef: ViewContainerRef; @ViewChild('postFooter') postFooterComponent: PostFooterComponent; showReactionSelector = false; @@ -90,6 +89,14 @@ export class PostComponent extends PostingDirective implements OnInit, OnC showDropdown = false; dropdownPosition = { x: 0, y: 0 }; + @ViewChild(PostReactionsBarComponent) + private reactionsBarComponent!: PostReactionsBarComponent; + + // Implement the abstract getter + protected get reactionsBar() { + return this.reactionsBarComponent; + } + onRightClick(event: MouseEvent) { event.preventDefault(); @@ -108,42 +115,6 @@ export class PostComponent extends PostingDirective implements OnInit, OnC this.showDropdown = true; } - editPosting() { - this.reactionsBar.editPosting(); - this.showDropdown = false; - } - - togglePin() { - this.reactionsBar.togglePin(); - this.showDropdown = false; - } - - deletePost() { - this.reactionsBar.deletePosting(); - this.showDropdown = false; - } - - selectReaction(event: any) { - this.reactionsBar.selectReaction(event); - this.showReactionSelector = false; - } - - addReaction(event: MouseEvent) { - event.preventDefault(); - this.showDropdown = false; - - this.clickPosition = { - x: event.clientX, - y: event.clientY, - }; - - this.showReactionSelector = true; - } - - toggleEmojiSelect() { - this.showReactionSelector = !this.showReactionSelector; - } - @HostListener('document:click', ['$event']) onClickOutside() { this.showDropdown = false; diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html index f01a17e8cf69..b194e0dc63fb 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html @@ -73,7 +73,7 @@ } } - diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts index 8f3ae89e0c58..249f8edc6ca5 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts @@ -71,7 +71,7 @@ export class AnswerPostReactionsBarComponent extends PostingsReactionsBarDirecti this.mayEditOrDelete = !this.isReadOnlyMode && (this.isAuthorOfPosting || mayEditOrDeleteOtherUsersAnswer); } - onEditPosting() { + editPosting() { this.openPostingCreateEditModal.emit(); } diff --git a/src/main/webapp/app/shared/metis/posting.directive.ts b/src/main/webapp/app/shared/metis/posting.directive.ts index 8e6636ee4c7a..7ba005a4a5bd 100644 --- a/src/main/webapp/app/shared/metis/posting.directive.ts +++ b/src/main/webapp/app/shared/metis/posting.directive.ts @@ -9,10 +9,52 @@ export abstract class PostingDirective implements OnInit { @Input() hasChannelModerationRights = false; @Input() isThreadSidebar: boolean; + protected abstract get reactionsBar(): any; + showDropdown = false; + dropdownPosition = { x: 0, y: 0 }; + showReactionSelector = false; + clickPosition = { x: 0, y: 0 }; content?: string; ngOnInit(): void { this.content = this.posting.content; } + + editPosting() { + console.log('buraya girdii'); + this.reactionsBar.editPosting(); + this.showDropdown = false; + } + + togglePin() { + this.reactionsBar.togglePin(); + this.showDropdown = false; + } + + deletePost() { + this.reactionsBar.deletePosting(); + this.showDropdown = false; + } + + selectReaction(event: any) { + this.reactionsBar.selectReaction(event); + this.showReactionSelector = false; + } + + addReaction(event: MouseEvent) { + event.preventDefault(); + this.showDropdown = false; + + this.clickPosition = { + x: event.clientX, + y: event.clientY, + }; + + this.showReactionSelector = true; + } + + toggleEmojiSelect() { + this.showReactionSelector = !this.showReactionSelector; + } } From 0cc49d9ae122255670ccd304668df95b7357327d Mon Sep 17 00:00:00 2001 From: Asli Aykan Date: Mon, 21 Oct 2024 23:34:29 +0200 Subject: [PATCH 15/31] updated strings --- .../answer-post/answer-post.component.html | 20 ++++++++++++--- .../app/shared/metis/post/post.component.html | 25 +++++++++++++++---- .../app/shared/metis/posting.directive.ts | 1 - src/main/webapp/i18n/de/metis.json | 7 +++++- src/main/webapp/i18n/en/metis.json | 7 +++++- 5 files changed, 48 insertions(+), 12 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 2c5650cf79a6..236e510a25e8 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 @@ -52,10 +52,22 @@
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 986a072cffc6..c5193b36c2d0 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.html +++ b/src/main/webapp/app/shared/metis/post/post.component.html @@ -129,11 +129,26 @@
diff --git a/src/main/webapp/app/shared/metis/posting.directive.ts b/src/main/webapp/app/shared/metis/posting.directive.ts index 7ba005a4a5bd..49ef7a500077 100644 --- a/src/main/webapp/app/shared/metis/posting.directive.ts +++ b/src/main/webapp/app/shared/metis/posting.directive.ts @@ -22,7 +22,6 @@ export abstract class PostingDirective implements OnInit { } editPosting() { - console.log('buraya girdii'); this.reactionsBar.editPosting(); this.showDropdown = false; } diff --git a/src/main/webapp/i18n/de/metis.json b/src/main/webapp/i18n/de/metis.json index 8a669a9c8c8c..c84ed6f2d406 100644 --- a/src/main/webapp/i18n/de/metis.json +++ b/src/main/webapp/i18n/de/metis.json @@ -123,7 +123,12 @@ "exerciseOrLecture": "Übung / Vorlesung", "showAllPosts": "Zeige alle Nachrichten", "showContent": "Inhalt ausklappen", - "collapseContent": "Inhalt einklappen" + "collapseContent": "Inhalt einklappen", + "addReaction": "Reaktion hinzufügen", + "pinMessage": "Nachricht anheften", + "editMessage": "Nachricht bearbeiten", + "deleteMessage": "Nachricht löschen", + "replyMessage": "Antworten im Thread" }, "answerPost": { "created": "Antwort erfolgreich erstellt", diff --git a/src/main/webapp/i18n/en/metis.json b/src/main/webapp/i18n/en/metis.json index 9a3ff0977f2b..6c4bc49b4cf5 100644 --- a/src/main/webapp/i18n/en/metis.json +++ b/src/main/webapp/i18n/en/metis.json @@ -123,7 +123,12 @@ "exerciseOrLecture": "Exercise / Lecture", "showAllPosts": "Show all messages", "showContent": "Show content", - "collapseContent": "Collapse content" + "collapseContent": "Collapse content", + "addReaction": "Add reaction", + "pinMessage": "Pin message", + "editMessage": "Edit message", + "deleteMessage": "Delete message", + "replyMessage": "Reply in thread" }, "answerPost": { "created": "New reply successfully created", From 29a25c5ec19cea82b4583cbf8e81a783d62c82a3 Mon Sep 17 00:00:00 2001 From: Asli Aykan Date: Tue, 22 Oct 2024 15:23:02 +0200 Subject: [PATCH 16/31] refactor code, fix pin/unpin string --- .../metis/answer-post/answer-post.component.html | 2 +- .../metis/answer-post/answer-post.component.ts | 12 ++++-------- .../app/shared/metis/post/post.component.html | 4 ++-- .../webapp/app/shared/metis/post/post.component.ts | 13 +++++-------- .../post-reactions-bar.component.html | 2 +- .../post-reactions-bar.component.ts | 9 ++++++--- .../webapp/app/shared/metis/posting.directive.ts | 5 +++++ src/main/webapp/i18n/de/metis.json | 1 + src/main/webapp/i18n/en/metis.json | 1 + 9 files changed, 26 insertions(+), 23 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 236e510a25e8..61ec120e6b91 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 @@ -1,5 +1,5 @@
- @if (!isConsecutive) { + @if (!isConsecutive()) { { isReadOnlyMode = false; // ng-container to render answerPostCreateEditModalComponent @ViewChild('createEditAnswerPostContainer', { read: ViewContainerRef }) containerRef: ViewContainerRef; - @Input() isConsecutive: boolean = false; - readonly faComments = faComments; + isConsecutive = input(false); readonly faPencilAlt = faPencilAlt; readonly faSmile = faSmile; readonly faTrash = faTrash; readonly faThumbtack = faThumbtack; static activeDropdownPost: AnswerPostComponent | null = null; - - @ViewChild(AnswerPostReactionsBarComponent) - private reactionsBarComponent!: AnswerPostReactionsBarComponent; + @ViewChild(AnswerPostReactionsBarComponent) private reactionsBarComponent!: AnswerPostReactionsBarComponent; constructor(protected changeDetector: ChangeDetectorRef) { super(); } - // Implement the abstract getter protected get reactionsBar() { return this.reactionsBarComponent; } 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 c5193b36c2d0..7a372ce96104 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.html +++ b/src/main/webapp/app/shared/metis/post/post.component.html @@ -1,5 +1,5 @@
- @if (!isConsecutive) { + @if (!isConsecutive()) { implements OnInit { @@ -36,6 +37,10 @@ export abstract class PostingDirective implements OnInit { this.showDropdown = false; } + checkIfPinned(): DisplayPriority { + return this.reactionsBar.checkIfPinned(); + } + selectReaction(event: any) { this.reactionsBar.selectReaction(event); this.showReactionSelector = false; diff --git a/src/main/webapp/i18n/de/metis.json b/src/main/webapp/i18n/de/metis.json index c84ed6f2d406..5810d9ff5473 100644 --- a/src/main/webapp/i18n/de/metis.json +++ b/src/main/webapp/i18n/de/metis.json @@ -126,6 +126,7 @@ "collapseContent": "Inhalt einklappen", "addReaction": "Reaktion hinzufügen", "pinMessage": "Nachricht anheften", + "unpinMessage": "Nachricht lösen", "editMessage": "Nachricht bearbeiten", "deleteMessage": "Nachricht löschen", "replyMessage": "Antworten im Thread" diff --git a/src/main/webapp/i18n/en/metis.json b/src/main/webapp/i18n/en/metis.json index 6c4bc49b4cf5..3c0101886c5d 100644 --- a/src/main/webapp/i18n/en/metis.json +++ b/src/main/webapp/i18n/en/metis.json @@ -126,6 +126,7 @@ "collapseContent": "Collapse content", "addReaction": "Add reaction", "pinMessage": "Pin message", + "unpinMessage": "Unpin message", "editMessage": "Edit message", "deleteMessage": "Delete message", "replyMessage": "Reply in thread" From 2f730142c8f3e1e7d26060ec79f3896cd78f2499 Mon Sep 17 00:00:00 2001 From: Asli Aykan Date: Wed, 23 Oct 2024 16:51:29 +0200 Subject: [PATCH 17/31] opened dropdown disables scrolling --- ...conversation-thread-sidebar.component.html | 2 +- .../answer-post/answer-post.component.html | 8 +--- .../answer-post/answer-post.component.scss | 6 --- .../answer-post/answer-post.component.ts | 42 ++++++++++++++++++- .../app/shared/metis/post/post.component.ts | 22 ++++++++++ 5 files changed, 65 insertions(+), 15 deletions(-) diff --git a/src/main/webapp/app/overview/course-conversations/layout/conversation-thread-sidebar/conversation-thread-sidebar.component.html b/src/main/webapp/app/overview/course-conversations/layout/conversation-thread-sidebar/conversation-thread-sidebar.component.html index 7690217a158c..25889884ccf1 100644 --- a/src/main/webapp/app/overview/course-conversations/layout/conversation-thread-sidebar/conversation-thread-sidebar.component.html +++ b/src/main/webapp/app/overview/course-conversations/layout/conversation-thread-sidebar/conversation-thread-sidebar.component.html @@ -22,7 +22,7 @@
+
@if (post !== undefined) {
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 61ec120e6b91..20c118be05c0 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 @@ -9,8 +9,8 @@ /> } @if (!createAnswerPostModal.isInputOpen) { -
-
+
+
-
@@ -56,14 +57,16 @@ - - + @if (mayEditOrDelete) { + + + }
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 7a739f3b2aa7..49bb0e772757 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 @@ -44,6 +44,7 @@ export class AnswerPostComponent extends PostingDirective { readonly faTrash = faTrash; readonly faThumbtack = faThumbtack; static activeDropdownPost: AnswerPostComponent | null = null; + mayEditOrDelete: boolean = false; @ViewChild(AnswerPostReactionsBarComponent) private reactionsBarComponent!: AnswerPostReactionsBarComponent; constructor( @@ -86,6 +87,10 @@ export class AnswerPostComponent extends PostingDirective { } } + onMayEditOrDelete(value: boolean) { + this.mayEditOrDelete = value; + } + onRightClick(event: MouseEvent) { event.preventDefault(); 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 da7685a8b147..421cc9d0ed0d 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.html +++ b/src/main/webapp/app/shared/metis/post/post.component.html @@ -76,6 +76,7 @@ (openPostingCreateEditModal)="openCreateAnswerPostModal()" (openThread)="openThread.emit()" (isModalOpen)="displayInlineInput = true" + (mayEditOrDelete)="onMayEditOrDelete($event)" /> }
@@ -137,14 +138,16 @@ - - + @if (mayEditOrDelete) { + + + } + @if ((isAnyReactionCountAboveZero() && isEmojiCount) || !isEmojiCount) { + + - - - - + + @if (!readOnlyMode) { + + } + + + } @if (!isEmojiCount && mayEditOrDelete) {
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 421cc9d0ed0d..cd61e6336f02 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.html +++ b/src/main/webapp/app/shared/metis/post/post.component.html @@ -76,7 +76,7 @@ (openPostingCreateEditModal)="openCreateAnswerPostModal()" (openThread)="openThread.emit()" (isModalOpen)="displayInlineInput = true" - (mayEditOrDelete)="onMayEditOrDelete($event)" + (mayEditOrDeleteOutput)="onMayEditOrDelete($event)" /> }
diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts index 79dc5928f3ca..3bd34b6fbe4a 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts @@ -22,7 +22,8 @@ export class AnswerPostReactionsBarComponent extends PostingsReactionsBarDirecti @Output() openPostingCreateEditModal = new EventEmitter(); isAuthorOfOriginalPost: boolean; isAnswerOfAnnouncement: boolean; - @Output() mayEditOrDelete = new EventEmitter(); + @Output() mayEditOrDeleteOutput = new EventEmitter(); + mayEditOrDelete: boolean; readonly faPencilAlt = faPencilAlt; @Input() isEmojiCount: boolean = false; @Output() postingUpdated = new EventEmitter(); @@ -68,8 +69,8 @@ export class AnswerPostReactionsBarComponent extends PostingsReactionsBarDirecti const isAtLeastInstructorInCourse = this.metisService.metisUserIsAtLeastInstructorInCourse(); const mayEditOrDeleteOtherUsersAnswer = (isCourseWideChannel && isAtLeastInstructorInCourse) || (getAsChannelDTO(this.metisService.getCurrentConversation())?.hasChannelModerationRights ?? false); - const canEditOrDelete = !this.isReadOnlyMode && (this.isAuthorOfPosting || mayEditOrDeleteOtherUsersAnswer); - this.mayEditOrDelete.emit(canEditOrDelete); + this.mayEditOrDelete = !this.isReadOnlyMode && (this.isAuthorOfPosting || mayEditOrDeleteOtherUsersAnswer); + this.mayEditOrDeleteOutput.emit(this.mayEditOrDelete); } editPosting() { diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts index a8146b261557..bf447e399dae 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts @@ -44,7 +44,8 @@ export class PostReactionsBarComponent extends PostingsReactionsBarDirective(); @Input() previewMode: boolean; isAtLeastInstructorInCourse: boolean; - @Output() mayEditOrDelete = new EventEmitter(); + @Output() mayEditOrDeleteOutput = new EventEmitter(); + mayEditOrDelete: boolean; @ViewChild(PostCreateEditModalComponent) postCreateEditModal?: PostCreateEditModalComponent; @Input() isEmojiCount = false; @Input() hoverBar: boolean = true; @@ -184,7 +185,7 @@ export class PostReactionsBarComponent extends PostingsReactionsBarDirective Date: Sun, 3 Nov 2024 22:38:27 +0100 Subject: [PATCH 27/31] moved undo event to reaction bar directive --- .../shared/metis/answer-post/answer-post.component.html | 2 +- src/main/webapp/app/shared/metis/post/post.component.html | 2 +- src/main/webapp/app/shared/metis/post/post.component.ts | 2 +- .../answer-post-header/answer-post-header.component.ts | 7 ------- .../posting-header/post-header/post-header.component.ts | 7 ------- .../metis/posting-header/posting-header.directive.ts | 6 ++---- .../answer-post-reactions-bar.component.ts | 5 ++++- .../post-reactions-bar/post-reactions-bar.component.ts | 5 ++++- .../posting-reactions-bar.directive.ts | 3 ++- 9 files changed, 15 insertions(+), 24 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 43ca7a6f0b99..073e4e907593 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 @@ -7,7 +7,6 @@ [lastReadDate]="lastReadDate" [hasChannelModerationRights]="hasChannelModerationRights" [isDeleted]="isDeleted" - (isDeleteEvent)="onDeleteEvent(true)" /> } @if (!createAnswerPostModal.isInputOpen) { @@ -34,6 +33,7 @@ (openPostingCreateEditModal)="createAnswerPostModal.open()" (reactionsUpdated)="onReactionsUpdated($event)" (mayEditOrDeleteOutput)="onMayEditOrDelete($event)" + (isDeleteEvent)="onDeleteEvent(true)" />
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 0686e0ffcebe..6aeaa9f211ef 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.html +++ b/src/main/webapp/app/shared/metis/post/post.component.html @@ -9,7 +9,6 @@ [hasChannelModerationRights]="hasChannelModerationRights" (isModalOpen)="displayInlineInput = true" [lastReadDate]="lastReadDate" - (isDeleteEvent)="onDeleteEvent(true)" /> }
@@ -82,6 +81,7 @@ (openThread)="openThread.emit()" (isModalOpen)="displayInlineInput = true" (mayEditOrDeleteOutput)="onMayEditOrDelete($event)" + (isDeleteEvent)="onDeleteEvent(true)" /> }
diff --git a/src/main/webapp/app/shared/metis/post/post.component.ts b/src/main/webapp/app/shared/metis/post/post.component.ts index 8731bf075a18..21c472153d53 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.ts +++ b/src/main/webapp/app/shared/metis/post/post.component.ts @@ -92,7 +92,7 @@ export class PostComponent extends PostingDirective implements OnInit, OnC @ViewChild(PostReactionsBarComponent) private reactionsBarComponent!: PostReactionsBarComponent; constructor( - private metisService: MetisService, + public metisService: MetisService, public changeDetector: ChangeDetectorRef, private oneToOneChatService: OneToOneChatService, private metisConversationService: MetisConversationService, 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 57ab1a005955..9871b3bbee8f 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 @@ -41,11 +41,4 @@ export class AnswerPostHeaderComponent extends PostingHeaderDirective implements this.postCreateEditModal?.modalRef?.close(); } - /** - * invokes the metis service to delete a post - */ - deletePosting(): void { - this.isDeleteEvent.emit(true); - } - protected readonly CachingStrategy = CachingStrategy; } 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 7892a6df0f09..e6deddb407b8 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, input, output } from '@angular/core'; +import { Directive, EventEmitter, Input, OnInit, Output, input } from '@angular/core'; import dayjs from 'dayjs/esm'; import { MetisService } from 'app/shared/metis/metis.service'; import { UserRole } from 'app/shared/metis/metis.util'; @@ -18,7 +18,7 @@ export abstract class PostingHeaderDirective implements OnIni @Output() isModalOpen = new EventEmitter(); isDeleted = input(false); - isDeleteEvent = output(); + isAtLeastTutorInCourse: boolean; isAuthorOfPosting: boolean; postingIsOfToday: boolean; @@ -97,6 +97,4 @@ export abstract class PostingHeaderDirective implements OnIni this.userAuthorityTooltip = toolTipTranslationPath + this.userAuthority; } } - - abstract deletePosting(): void; } diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts index 3bd34b6fbe4a..9023089670d5 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts @@ -46,8 +46,11 @@ export class AnswerPostReactionsBarComponent extends PostingsReactionsBarDirecti return Object.values(this.reactionMetaDataMap).some((reaction) => reaction.count >= 1); } + /** + * invokes the metis service to delete an answer post + */ deletePosting(): void { - this.metisService.deleteAnswerPost(this.posting); + this.isDeleteEvent.emit(true); } /** diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts index bf447e399dae..b18befbbfa71 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts @@ -168,8 +168,11 @@ export class PostReactionsBarComponent extends PostingsReactionsBarDirective implement isAtLeastTutorInCourse: boolean; isAuthorOfPosting: boolean; @Output() isModalOpen = new EventEmitter(); + isDeleteEvent = output(); /* * icons (as svg paths) to be used as category preview image in emoji mart selector From fe6537fc1104e2762cafb93f910b11140990bfa7 Mon Sep 17 00:00:00 2001 From: Asli Aykan Date: Sun, 3 Nov 2024 23:17:05 +0100 Subject: [PATCH 28/31] fix tests --- ...nswer-post-reactions-bar.component.spec.ts | 18 ------------------ .../post-reactions-bar.component.spec.ts | 19 ------------------- .../spec/directive/posting.directive.spec.ts | 4 ++++ 3 files changed, 4 insertions(+), 37 deletions(-) diff --git a/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.spec.ts b/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.spec.ts index f99c277ec924..8e5b51590166 100644 --- a/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.spec.ts @@ -43,7 +43,6 @@ describe('AnswerPostReactionsBarComponent', () => { let metisServiceUserIsAtLeastTutorMock: jest.SpyInstance; let metisServiceUserIsAtLeastInstructorMock: jest.SpyInstance; let metisServiceUserPostingAuthorMock: jest.SpyInstance; - let metisServiceDeleteAnswerPostMock: jest.SpyInstance; let metisServiceUpdateAnswerPostMock: jest.SpyInstance; beforeEach(() => { @@ -78,7 +77,6 @@ describe('AnswerPostReactionsBarComponent', () => { 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'); component = fixture.componentInstance; answerPost = new AnswerPost(); @@ -162,22 +160,6 @@ describe('AnswerPostReactionsBarComponent', () => { expect(getDeleteButton()).toBeNull(); }); - it('should invoke metis service when delete icon is clicked', () => { - metisServiceUserPostingAuthorMock.mockReturnValue(true); - component.ngOnInit(); - fixture.detectChanges(); - - expect(getDeleteButton()).toBeDefined(); - expect(getDeleteButton()).not.toBeNull(); - - const confirmIconComponent = getDeleteButton()!.query(By.directive(ConfirmIconComponent)); - expect(confirmIconComponent).not.toBeNull(); - confirmIconComponent.triggerEventHandler('confirmEvent', null); - fixture.detectChanges(); - - expect(metisServiceDeleteAnswerPostMock).toHaveBeenCalledOnce(); - }); - it('should emit event to create embedded view when edit icon is clicked', () => { component.posting = metisResolvingAnswerPostUser1; const openPostingCreateEditModalEmitSpy = jest.spyOn(component.openPostingCreateEditModal, 'emit'); diff --git a/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/post-reactions-bar/post-reactions-bar.component.spec.ts b/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/post-reactions-bar/post-reactions-bar.component.spec.ts index 5c29f1658a7d..f5ccda3ada15 100644 --- a/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/post-reactions-bar/post-reactions-bar.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/post-reactions-bar/post-reactions-bar.component.spec.ts @@ -136,25 +136,6 @@ describe('PostReactionsBarComponent', () => { }); }); - it('should invoke metis service when delete icon is clicked', () => { - component.readOnlyMode = false; - component.previewMode = false; - jest.spyOn(metisService, 'metisUserIsAuthorOfPosting').mockReturnValue(true); - const deletePostSpy = jest.spyOn(metisService, 'deletePost'); - component.posting = { id: 1 } as Post; - - component.ngOnInit(); - fixture.detectChanges(); - const confirmIconDebugElement = debugElement.query(By.directive(ConfirmIconComponent)); - expect(confirmIconDebugElement).not.toBeNull(); - - confirmIconDebugElement.componentInstance.confirmEvent.emit(); - fixture.detectChanges(); - - expect(deletePostSpy).toHaveBeenCalledOnce(); - expect(deletePostSpy).toHaveBeenCalledWith(component.posting); - }); - it('should display edit and delete options to the author when not in read-only or preview mode', () => { component.readOnlyMode = false; component.previewMode = false; diff --git a/src/test/javascript/spec/directive/posting.directive.spec.ts b/src/test/javascript/spec/directive/posting.directive.spec.ts index 15f44432274f..ab17325e9b18 100644 --- a/src/test/javascript/spec/directive/posting.directive.spec.ts +++ b/src/test/javascript/spec/directive/posting.directive.spec.ts @@ -3,6 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Posting } from 'app/entities/metis/posting.model'; import { DisplayPriority } from 'app/shared/metis/metis.util'; import { PostingDirective } from 'app/shared/metis/posting.directive'; +import { MetisService } from 'app/shared/metis/metis.service'; class MockPosting implements Posting { content: string; @@ -20,6 +21,8 @@ class MockReactionsBar { selectReaction = jest.fn(); } +class MockMetisService {} + @Component({ template: `
`, }) @@ -43,6 +46,7 @@ describe('PostingDirective', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [TestPostingComponent], + providers: [{ provide: MetisService, useClass: MockMetisService }], }).compileComponents(); }); From ae47652d3b1fbd08f2f516cc787230021c62e133 Mon Sep 17 00:00:00 2001 From: Asli Aykan Date: Mon, 4 Nov 2024 00:10:10 +0100 Subject: [PATCH 29/31] fix pin message option visibility on dropdown menu --- .../webapp/app/shared/metis/post/post.component.html | 11 +++++++---- .../webapp/app/shared/metis/post/post.component.ts | 5 +++++ .../post-reactions-bar.component.ts | 2 ++ 3 files changed, 14 insertions(+), 4 deletions(-) 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 6aeaa9f211ef..0682204dc715 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.html +++ b/src/main/webapp/app/shared/metis/post/post.component.html @@ -81,6 +81,7 @@ (openThread)="openThread.emit()" (isModalOpen)="displayInlineInput = true" (mayEditOrDeleteOutput)="onMayEditOrDelete($event)" + (canPinOutput)="onCanPin($event)" (isDeleteEvent)="onDeleteEvent(true)" /> } @@ -141,10 +142,12 @@ - + @if (canPin) { + + } @if (mayEditOrDelete) { + @if (mayEditOrDelete) { + - + + } }
From 56104438bbf4beab180ee5b535dce56edc6a8228 Mon Sep 17 00:00:00 2001 From: Asli Aykan Date: Mon, 4 Nov 2024 10:20:42 +0100 Subject: [PATCH 31/31] fix codacy issue --- .../answer-post/answer-post.component.scss | 32 +++++++++---------- .../app/shared/metis/post/post.component.scss | 32 +++++++++---------- 2 files changed, 32 insertions(+), 32 deletions(-) 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 2e3dc25e0176..f744b2dafdd3 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 @@ -19,6 +19,22 @@ } } +.hover-actions { + position: absolute; + top: -1.8rem; + right: 3%; + display: flex; + gap: 10px; + visibility: hidden; + transition: + opacity 0.2s ease-in-out, + visibility 0.2s ease-in-out; + background: var(--metis-selection-option-background); + padding: 5px; + border-radius: 5px; + border: 0.01rem solid var(--metis-gray); +} + .message-container { position: relative; border-radius: 5px; @@ -42,22 +58,6 @@ background: var(--metis-selection-option-hover-background); } -.hover-actions { - position: absolute; - top: -1.8rem; - right: 3%; - display: flex; - gap: 10px; - visibility: hidden; - transition: - opacity 0.2s ease-in-out, - visibility 0.2s ease-in-out; - background: var(--metis-selection-option-background); - padding: 5px; - border-radius: 5px; - border: 0.01rem solid var(--metis-gray); -} - .message-container:hover .hover-actions { opacity: 1; visibility: visible; diff --git a/src/main/webapp/app/shared/metis/post/post.component.scss b/src/main/webapp/app/shared/metis/post/post.component.scss index 41a3e1546b2f..1a6cf4b3ace2 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.scss +++ b/src/main/webapp/app/shared/metis/post/post.component.scss @@ -15,6 +15,22 @@ color: inherit; } +.hover-actions { + position: absolute; + top: -1.8rem; + right: 1%; + display: flex; + gap: 10px; + visibility: hidden; + transition: + opacity 0.2s ease-in-out, + visibility 0.2s ease-in-out; + background: var(--metis-selection-option-background); + padding: 5px; + border-radius: 5px; + border: 0.01rem solid var(--metis-gray); +} + .message-container { position: relative; border-radius: 5px; @@ -38,22 +54,6 @@ background: var(--metis-selection-option-hover-background); } -.hover-actions { - position: absolute; - top: -1.8rem; - right: 1%; - display: flex; - gap: 10px; - visibility: hidden; - transition: - opacity 0.2s ease-in-out, - visibility 0.2s ease-in-out; - background: var(--metis-selection-option-background); - padding: 5px; - border-radius: 5px; - border: 0.01rem solid var(--metis-gray); -} - .message-container:hover .hover-actions { opacity: 1; visibility: visible;