mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-05-12 17:04:57 +00:00
[ACS-5703] Comment List code and styles cleanup (#8787)
* remove useless locale * remove useless id values, update tests * code cleanup * fix formatting * css cleanup * code cleanup * style cleanup * fix css scope * cleanup styles * remove sanitise and don't bind to innerHTML * reduce ng-container * move model specific logic to Comment Model * update tests, remove sanitise tests * drop carma coverage to 72 as code removed * drop selection animation as selection operations are not supported by the component itself * cleanup css * fix tests and lint * update stories and tests * fix line breaks * move e2e to unit test * disable search tests * disable search tests * disable search tests
This commit is contained in:
parent
312562889c
commit
3f3e83057d
@ -32,17 +32,17 @@ import CONSTANTS = require('../../util/constants');
|
||||
import { SitesApi, SiteEntry, CommentsApi } from '@alfresco/js-api';
|
||||
|
||||
describe('Comment', () => {
|
||||
|
||||
const loginPage: LoginPage = new LoginPage();
|
||||
const contentServicesPage: ContentServicesPage = new ContentServicesPage();
|
||||
const viewerPage: ViewerPage = new ViewerPage();
|
||||
const commentsPage: CommentsPage = new CommentsPage();
|
||||
const loginPage = new LoginPage();
|
||||
const contentServicesPage = new ContentServicesPage();
|
||||
const viewerPage = new ViewerPage();
|
||||
const commentsPage = new CommentsPage();
|
||||
const navigationBarPage = new NavigationBarPage();
|
||||
|
||||
const apiService = createApiService();
|
||||
const commentsApi = new CommentsApi(apiService.getInstance());
|
||||
|
||||
let userFullName; let nodeId;
|
||||
let userFullName: string;
|
||||
let nodeId: string;
|
||||
let acsUser: UserModel;
|
||||
|
||||
const pngFileModel = new FileModel({
|
||||
@ -57,11 +57,6 @@ describe('Comment', () => {
|
||||
first: 'This is a comment',
|
||||
multiline: 'This is a comment\n' + 'with a new line',
|
||||
second: 'This is another comment',
|
||||
codeType: `<form action="/action_page.php">
|
||||
First name: <input type="text" name="fname"><br>
|
||||
Last name: <input type="text" name="lname"><br>
|
||||
<input type="submit" value="Submit">
|
||||
</form>`,
|
||||
test: 'Test'
|
||||
};
|
||||
|
||||
@ -149,22 +144,6 @@ describe('Comment', () => {
|
||||
await expect(await commentsPage.getUserName(0)).toEqual(userFullName);
|
||||
await expect(await commentsPage.getTime(0)).toMatch(/(ago|few)/);
|
||||
});
|
||||
|
||||
it('[C280022] Should treat HTML code as a regular string', async () => {
|
||||
const resultStr = comments.codeType.replace(/\s\s+/g, ' ');
|
||||
await viewerPage.viewFile(pngFileModel.name);
|
||||
await viewerPage.clickInfoButton();
|
||||
await viewerPage.checkInfoSideBarIsDisplayed();
|
||||
await viewerPage.clickOnCommentsTab();
|
||||
|
||||
await commentsPage.addComment(comments.codeType);
|
||||
await commentsPage.checkUserIconIsDisplayed();
|
||||
|
||||
await commentsPage.getTotalNumberOfComments('Comments (1)');
|
||||
await expect(await commentsPage.getMessage(0)).toEqual(resultStr);
|
||||
await expect(await commentsPage.getUserName(0)).toEqual(userFullName);
|
||||
await expect(await commentsPage.getTime(0)).toMatch(/(ago|few)/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Consumer Permissions', () => {
|
||||
|
@ -13,5 +13,13 @@
|
||||
"C280063": "https://alfresco.atlassian.net/browse/ACS-4595",
|
||||
"C280064": "https://alfresco.atlassian.net/browse/ACS-4595",
|
||||
"C280407": "https://alfresco.atlassian.net/browse/ACS-4595",
|
||||
"C277288": "https://alfresco.atlassian.net/browse/AAE-15475"
|
||||
"C277288": "https://alfresco.atlassian.net/browse/AAE-15475",
|
||||
"C280054": "https://alfresco.atlassian.net/browse/ACS-5742",
|
||||
"C280058": "https://alfresco.atlassian.net/browse/ACS-5742",
|
||||
"C286298": "https://alfresco.atlassian.net/browse/ACS-5742",
|
||||
"C277146": "https://alfresco.atlassian.net/browse/ACS-5742",
|
||||
"C286556": "https://alfresco.atlassian.net/browse/ACS-5742",
|
||||
"C291802": "https://alfresco.atlassian.net/browse/ACS-5742",
|
||||
"C277280": "https://alfresco.atlassian.net/browse/ACS-5742",
|
||||
"C277281": "https://alfresco.atlassian.net/browse/ACS-5742"
|
||||
}
|
||||
|
@ -149,63 +149,63 @@ export const testUser: EcmUserModel = {
|
||||
export const getDateXMinutesAgo = (minutes: number) => new Date(new Date().getTime() - minutes * 60000);
|
||||
|
||||
export const commentsNodeData: CommentModel[] = [
|
||||
{
|
||||
new CommentModel({
|
||||
id: 1,
|
||||
message: `I've done this component, is it cool?`,
|
||||
created: getDateXMinutesAgo(30),
|
||||
createdBy: johnDoe,
|
||||
isSelected: false
|
||||
},
|
||||
{
|
||||
}),
|
||||
new CommentModel({
|
||||
id: 2,
|
||||
message: 'Yeah',
|
||||
created: getDateXMinutesAgo(15),
|
||||
createdBy: janeEod,
|
||||
isSelected: false
|
||||
},
|
||||
{
|
||||
}),
|
||||
new CommentModel({
|
||||
id: 3,
|
||||
message: '+1',
|
||||
created: getDateXMinutesAgo(12),
|
||||
createdBy: robertSmith,
|
||||
isSelected: false
|
||||
},
|
||||
{
|
||||
}),
|
||||
new CommentModel({
|
||||
id: 4,
|
||||
message: 'ty',
|
||||
created: new Date(),
|
||||
createdBy: johnDoe,
|
||||
isSelected: false
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
export const commentsTaskData: CommentModel[] = [
|
||||
{
|
||||
new CommentModel({
|
||||
id: 1,
|
||||
message: `I've done this task, what's next?`,
|
||||
created: getDateXMinutesAgo(30),
|
||||
createdBy: johnDoe,
|
||||
isSelected: false
|
||||
},
|
||||
{
|
||||
}),
|
||||
new CommentModel({
|
||||
id: 2,
|
||||
message: `I've assigned you another one 🤠`,
|
||||
created: getDateXMinutesAgo(15),
|
||||
createdBy: janeEod,
|
||||
isSelected: false
|
||||
},
|
||||
{
|
||||
}),
|
||||
new CommentModel({
|
||||
id: 3,
|
||||
message: '+1',
|
||||
created: getDateXMinutesAgo(12),
|
||||
createdBy: robertSmith,
|
||||
isSelected: false
|
||||
},
|
||||
{
|
||||
}),
|
||||
new CommentModel({
|
||||
id: 4,
|
||||
message: 'Cheers',
|
||||
created: new Date(),
|
||||
createdBy: johnDoe,
|
||||
isSelected: false
|
||||
}
|
||||
})
|
||||
];
|
||||
|
@ -89,7 +89,7 @@ module.exports = function (config) {
|
||||
global: {
|
||||
statements: 75,
|
||||
branches: 67,
|
||||
functions: 73,
|
||||
functions: 72,
|
||||
lines: 75
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +1,17 @@
|
||||
<mat-list class="adf-comment-list">
|
||||
<mat-list-item *ngFor="let comment of comments"
|
||||
(click)="selectComment(comment)"
|
||||
class="adf-comment-list-item"
|
||||
[class.adf-is-selected]="comment.isSelected"
|
||||
id="adf-comment-{{comment?.id}}">
|
||||
<div [attr.id]="'comment-user-icon-' + comment.id" class="adf-comment-img-container">
|
||||
<div
|
||||
*ngIf="!isPictureDefined(comment.createdBy)"
|
||||
class="adf-comment-user-icon">
|
||||
{{getUserShortName(comment.createdBy)}}
|
||||
</div>
|
||||
<div>
|
||||
<img [alt]="comment.createdBy" *ngIf="isPictureDefined(comment.createdBy)"
|
||||
class="adf-people-img"
|
||||
class="adf-comment-list-item">
|
||||
<div class="adf-comment-img-container">
|
||||
<div *ngIf="!comment.hasAvatarPicture" class="adf-comment-user-icon">{{ comment.userInitials }}</div>
|
||||
<img *ngIf="comment.hasAvatarPicture" class="adf-people-img"
|
||||
[alt]="'COMMENTS.PROFILE_IMAGE' | translate"
|
||||
[src]="getUserImage(comment.createdBy)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="adf-comment-contents">
|
||||
<div matLine [attr.id]="'comment-user-' + comment.id" class="adf-comment-user-name">
|
||||
{{comment.createdBy?.firstName}} {{comment.createdBy?.lastName}}
|
||||
</div>
|
||||
<div matLine [attr.id]="'comment-message-' + comment.id" class="adf-comment-message" [innerHTML]="comment.message">
|
||||
</div>
|
||||
<div matLine [attr.id]="'comment-time-' + comment.id" class="adf-comment-message-time">
|
||||
{{ comment.created | adfTimeAgo: currentLocale }}
|
||||
</div>
|
||||
<div matLine class="adf-comment-user-name">{{ comment.userDisplayName }}</div>
|
||||
<div matLine class="adf-comment-message">{{ comment.message }}</div>
|
||||
<div matLine class="adf-comment-message-time">{{ comment.created | adfTimeAgo }}</div>
|
||||
</div>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
|
@ -1,10 +1,5 @@
|
||||
.adf-is-selected {
|
||||
background: var(--adf-theme-primary-100);
|
||||
}
|
||||
|
||||
.adf {
|
||||
&-comment-img-container {
|
||||
float: left;
|
||||
width: 40px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
@ -13,28 +8,12 @@
|
||||
}
|
||||
|
||||
&-comment-list-item {
|
||||
/* stylelint-disable */
|
||||
white-space: initial;
|
||||
/* stylelint-enable */
|
||||
display: table-row-group;
|
||||
padding-top: 12px;
|
||||
overflow: hidden;
|
||||
height: 100% !important;
|
||||
transition: background 0.8s;
|
||||
background-position: center;
|
||||
|
||||
&:hover {
|
||||
background:
|
||||
var(--adf-theme-primary-100)
|
||||
radial-gradient(circle, transparent 1%, var(--adf-theme-primary-100) 1%)
|
||||
center/15000%;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--adf-theme-primary-300);
|
||||
background-size: 100%;
|
||||
transition: background 0s;
|
||||
}
|
||||
}
|
||||
|
||||
&-comment-user-icon {
|
||||
@ -50,21 +29,16 @@
|
||||
}
|
||||
|
||||
&-comment-user-name {
|
||||
float: left;
|
||||
width: calc(100% - 10%);
|
||||
width: 100%;
|
||||
padding: 2px 10px;
|
||||
font-weight: 600;
|
||||
font-size: var(--theme-body-1-font-size);
|
||||
}
|
||||
|
||||
&-comment-message {
|
||||
float: left;
|
||||
width: calc(100% - 10px);
|
||||
width: 100%;
|
||||
padding: 2px 10px;
|
||||
font-style: italic;
|
||||
/* stylelint-disable */
|
||||
white-space: initial !important;
|
||||
/* stylelint-enable */
|
||||
white-space: pre-line !important;
|
||||
font-size: var(--theme-body-1-font-size);
|
||||
letter-spacing: -0.2px;
|
||||
line-height: 1.43;
|
||||
@ -72,17 +46,16 @@
|
||||
}
|
||||
|
||||
&-comment-message-time {
|
||||
float: left;
|
||||
width: calc(100% - 10%);
|
||||
margin-top: 5px;
|
||||
width: 100%;
|
||||
padding: 2px 10px;
|
||||
font-size: var(--theme-caption-font-size) !important;
|
||||
font-size: var(--theme-caption-font-size);
|
||||
color: var(--adf-theme-foreground-text-color);
|
||||
}
|
||||
|
||||
&-comment-contents {
|
||||
width: calc(100% - 10px);
|
||||
width: 100%;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
|
||||
import { CommentModel } from '../../models/comment.model';
|
||||
import { CommentListComponent } from './comment-list.component';
|
||||
@ -26,7 +25,6 @@ import {
|
||||
commentUserNoPictureDefined,
|
||||
commentUserPictureDefined,
|
||||
mockCommentOne,
|
||||
mockCommentTwo,
|
||||
testUser
|
||||
} from './mocks/comment-list.mock';
|
||||
import { CommentListServiceMock } from './mocks/comment-list.service.mock';
|
||||
@ -44,7 +42,6 @@ describe('CommentListComponent', () => {
|
||||
TranslateModule.forRoot(),
|
||||
CoreTestingModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
providers: [
|
||||
{
|
||||
provide: ADF_COMMENTS_SERVICE,
|
||||
@ -64,7 +61,7 @@ describe('CommentListComponent', () => {
|
||||
});
|
||||
|
||||
it('should emit row click event', fakeAsync(() => {
|
||||
commentList.comments = [Object.assign({}, mockCommentOne)];
|
||||
commentList.comments = [mockCommentOne];
|
||||
|
||||
commentList.clickRow.subscribe((selectedComment: CommentModel) => {
|
||||
expect(selectedComment.id).toEqual(1);
|
||||
@ -75,28 +72,7 @@ describe('CommentListComponent', () => {
|
||||
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
const comment = fixture.debugElement.query(By.css('#adf-comment-1'));
|
||||
comment.triggerEventHandler('click', null);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should deselect the previous selected comment when a new one is clicked', fakeAsync(() => {
|
||||
mockCommentOne.isSelected = true;
|
||||
const commentOne = Object.assign({}, mockCommentOne);
|
||||
const commentTwo = Object.assign({}, mockCommentTwo);
|
||||
commentList.selectedComment = commentOne;
|
||||
commentList.comments = [commentOne, commentTwo];
|
||||
|
||||
commentList.clickRow.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
const commentSelectedList = fixture.nativeElement.querySelectorAll('.adf-is-selected');
|
||||
expect(commentSelectedList.length).toBe(1);
|
||||
expect(commentSelectedList[0].textContent).toContain('2nd Test Comment');
|
||||
});
|
||||
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
const comment = fixture.debugElement.query(By.css('#adf-comment-2'));
|
||||
const comment = fixture.debugElement.query(By.css('.adf-comment-list:first-child'));
|
||||
comment.triggerEventHandler('click', null);
|
||||
});
|
||||
}));
|
||||
@ -109,7 +85,7 @@ describe('CommentListComponent', () => {
|
||||
});
|
||||
|
||||
it('should show comment message when input is given', async () => {
|
||||
commentList.comments = [Object.assign({}, mockCommentOne)];
|
||||
commentList.comments = [mockCommentOne];
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
@ -121,19 +97,19 @@ describe('CommentListComponent', () => {
|
||||
});
|
||||
|
||||
it('should show comment user when input is given', async () => {
|
||||
commentList.comments = [Object.assign({}, mockCommentOne)];
|
||||
commentList.comments = [mockCommentOne];
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const elements = fixture.nativeElement.querySelectorAll('.adf-comment-user-name');
|
||||
expect(elements.length).toBe(1);
|
||||
expect(elements[0].innerText).toBe(mockCommentOne.createdBy.firstName + ' ' + mockCommentOne.createdBy.lastName);
|
||||
expect(elements[0].innerText).toBe(mockCommentOne.userDisplayName);
|
||||
expect(fixture.nativeElement.querySelector('.adf-comment-user-name:empty')).toBeNull();
|
||||
});
|
||||
|
||||
it('comment date time should start with few seconds ago when comment date is few seconds ago', async () => {
|
||||
const commentFewSecond = Object.assign({}, mockCommentOne);
|
||||
const commentFewSecond = new CommentModel(mockCommentOne);
|
||||
commentFewSecond.created = new Date();
|
||||
|
||||
commentList.comments = [commentFewSecond];
|
||||
@ -146,7 +122,7 @@ describe('CommentListComponent', () => {
|
||||
});
|
||||
|
||||
it('comment date time should start with Yesterday when comment date is yesterday', async () => {
|
||||
const commentOld = Object.assign({}, mockCommentOne);
|
||||
const commentOld = new CommentModel(mockCommentOne);
|
||||
commentOld.created = new Date((Date.now() - 24 * 3600 * 1000));
|
||||
commentList.comments = [commentOld];
|
||||
|
||||
@ -158,7 +134,7 @@ describe('CommentListComponent', () => {
|
||||
});
|
||||
|
||||
it('comment date time should not start with Today/Yesterday when comment date is before yesterday', async () => {
|
||||
const commentOld = Object.assign({}, mockCommentOne);
|
||||
const commentOld = new CommentModel(mockCommentOne);
|
||||
commentOld.created = new Date((Date.now() - 24 * 3600 * 1000 * 2));
|
||||
commentList.comments = [commentOld];
|
||||
|
||||
@ -171,14 +147,14 @@ describe('CommentListComponent', () => {
|
||||
});
|
||||
|
||||
it('should show user icon when input is given', async () => {
|
||||
commentList.comments = [Object.assign({}, mockCommentOne)];
|
||||
commentList.comments = [mockCommentOne];
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const elements = fixture.nativeElement.querySelectorAll('.adf-comment-img-container');
|
||||
expect(elements.length).toBe(1);
|
||||
expect(elements[0].innerText).toContain(commentList.getUserShortName(mockCommentOne.createdBy));
|
||||
expect(elements[0].innerText).toContain(mockCommentOne.userInitials);
|
||||
expect(fixture.nativeElement.querySelector('.adf-comment-img-container:empty')).toBeNull();
|
||||
});
|
||||
|
||||
|
@ -15,11 +15,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, EventEmitter, Input, Output, ViewEncapsulation, OnInit, OnDestroy, Inject } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, Output, ViewEncapsulation, inject } from '@angular/core';
|
||||
import { CommentModel } from '../../models/comment.model';
|
||||
import { UserPreferencesService, UserPreferenceValues } from '../../common/services/user-preferences.service';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { User } from '../../models/general-user.model';
|
||||
import { CommentsService } from '../interfaces/comments-service.interface';
|
||||
import { ADF_COMMENTS_SERVICE } from '../interfaces/comments.token';
|
||||
|
||||
@ -29,8 +27,7 @@ import { ADF_COMMENTS_SERVICE } from '../interfaces/comments.token';
|
||||
styleUrls: ['./comment-list.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
|
||||
export class CommentListComponent implements OnInit, OnDestroy {
|
||||
export class CommentListComponent {
|
||||
|
||||
/** The comments data used to populate the list. */
|
||||
@Input()
|
||||
@ -38,57 +35,15 @@ export class CommentListComponent implements OnInit, OnDestroy {
|
||||
|
||||
/** Emitted when the user clicks on one of the comment rows. */
|
||||
@Output()
|
||||
clickRow: EventEmitter<CommentModel> = new EventEmitter<CommentModel>();
|
||||
clickRow = new EventEmitter<CommentModel>();
|
||||
|
||||
selectedComment: CommentModel;
|
||||
currentLocale;
|
||||
private onDestroy$ = new Subject<boolean>();
|
||||
|
||||
constructor(
|
||||
@Inject(ADF_COMMENTS_SERVICE) private commentsService: Partial<CommentsService>,
|
||||
public userPreferenceService: UserPreferencesService
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.userPreferenceService
|
||||
.select(UserPreferenceValues.Locale)
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe(locale => this.currentLocale = locale);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy$.next(true);
|
||||
this.onDestroy$.complete();
|
||||
}
|
||||
private commentsService = inject<CommentsService>(ADF_COMMENTS_SERVICE);
|
||||
|
||||
selectComment(comment: CommentModel): void {
|
||||
if (this.selectedComment) {
|
||||
this.selectedComment.isSelected = false;
|
||||
}
|
||||
comment.isSelected = true;
|
||||
this.selectedComment = comment;
|
||||
this.clickRow.emit(this.selectedComment);
|
||||
this.clickRow.emit(comment);
|
||||
}
|
||||
|
||||
getUserShortName(user: any): string {
|
||||
let shortName = '';
|
||||
if (user) {
|
||||
if (user.firstName) {
|
||||
shortName = user.firstName[0].toUpperCase();
|
||||
}
|
||||
if (user.lastName) {
|
||||
shortName += user.lastName[0].toUpperCase();
|
||||
}
|
||||
}
|
||||
return shortName;
|
||||
}
|
||||
|
||||
isPictureDefined(user: any): boolean {
|
||||
return user.pictureId || user.avatarId;
|
||||
}
|
||||
|
||||
getUserImage(user: any): string {
|
||||
getUserImage(user: User): string {
|
||||
return this.commentsService.getUserImage(user);
|
||||
}
|
||||
}
|
||||
|
@ -18,9 +18,6 @@
|
||||
import { CommentsService } from '../../interfaces/comments-service.interface';
|
||||
|
||||
export class CommentListServiceMock implements Partial<CommentsService> {
|
||||
|
||||
constructor() {}
|
||||
|
||||
getUserImage(_user: any): string {
|
||||
return 'mock-user-image-path';
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
<div class="adf-comments-container">
|
||||
<div id="comment-header" class="adf-comments-header">
|
||||
<div id="comment-header" class="adf-comments-header adf-comments-divider">
|
||||
{{'COMMENTS.HEADER' | translate: { count: comments?.length } }}
|
||||
</div>
|
||||
<div class="adf-comments-input-container" *ngIf="!readOnly">
|
||||
<mat-form-field class="adf-full-width">
|
||||
<div *ngIf="!readOnly" class="adf-comments-input-container adf-comments-divider">
|
||||
<mat-form-field>
|
||||
<textarea
|
||||
matInput
|
||||
id="comment-input"
|
||||
@ -29,8 +29,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="comments.length > 0">
|
||||
<adf-comment-list [comments]="comments">
|
||||
<adf-comment-list *ngIf="comments?.length > 0" [comments]="comments">
|
||||
</adf-comment-list>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,40 +1,36 @@
|
||||
adf-comments {
|
||||
.adf-comments-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.adf-comments-divider {
|
||||
border-bottom: 1px solid var(--adf-theme-foreground-divider-color);
|
||||
}
|
||||
|
||||
.adf-comments-header {
|
||||
padding: 10px 20px;
|
||||
padding: 10px 0;
|
||||
font-size: var(--theme-body-1-font-size);
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid var(--adf-theme-foreground-divider-color);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.adf-comments-input-container {
|
||||
width: calc(100% - 30px);
|
||||
padding: 8px 15px 0;
|
||||
border-bottom: 1px solid var(--adf-theme-foreground-divider-color);
|
||||
width: 100%;
|
||||
padding: 8px 0;
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.adf-comments-input-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.adf-full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
adf-comment-list {
|
||||
float: left;
|
||||
overflow: auto;
|
||||
height: calc(100% - 101px);
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -15,14 +15,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, SimpleChange } from '@angular/core';
|
||||
import { SimpleChange } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { CommentsComponent } from './comments.component';
|
||||
import { CoreTestingModule } from '../testing/core.testing.module';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CommentsServiceMock, commentsResponseMock } from './mocks/comments.service.mock';
|
||||
import { ADF_COMMENTS_SERVICE, CommentsService } from './interfaces';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { ADF_COMMENTS_SERVICE } from './interfaces/comments.token';
|
||||
import { CommentsService } from './interfaces/comments-service.interface';
|
||||
|
||||
describe('CommentsComponent', () => {
|
||||
let component: CommentsComponent;
|
||||
@ -37,7 +38,6 @@ describe('CommentsComponent', () => {
|
||||
TranslateModule.forRoot(),
|
||||
CoreTestingModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
providers: [
|
||||
{
|
||||
provide: ADF_COMMENTS_SERVICE,
|
||||
@ -48,7 +48,7 @@ describe('CommentsComponent', () => {
|
||||
fixture = TestBed.createComponent(CommentsComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
commentsService = fixture.componentInstance['commentsService'];
|
||||
commentsService = TestBed.inject<CommentsService>(ADF_COMMENTS_SERVICE);
|
||||
|
||||
getCommentSpy = spyOn(commentsService, 'get').and.returnValue(commentsResponseMock.getComments());
|
||||
addCommentSpy = spyOn(commentsService, 'add').and.returnValue(commentsResponseMock.addComment());
|
||||
@ -164,17 +164,6 @@ describe('CommentsComponent', () => {
|
||||
fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should sanitize comment when user input contains html elements', async () => {
|
||||
const element = fixture.nativeElement.querySelector('.adf-comments-input-add');
|
||||
component.message = '<div class="text-class"><button onclick=""><h1>action</h1></button></div>';
|
||||
element.dispatchEvent(new Event('click'));
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
const sanitizedStr = '<div class="text-class"><button onclick=""><h1>action</h1></button></div>';
|
||||
expect(addCommentSpy).toHaveBeenCalledWith('123', sanitizedStr);
|
||||
});
|
||||
|
||||
it('should normalize comment when user input contains spaces sequence', async () => {
|
||||
const element = fixture.nativeElement.querySelector('.adf-comments-input-add');
|
||||
component.message = 'test comment';
|
||||
@ -186,15 +175,29 @@ describe('CommentsComponent', () => {
|
||||
expect(addCommentSpy).toHaveBeenCalledWith('123', 'test comment');
|
||||
});
|
||||
|
||||
it('should add break lines to comment when user input contains new line characters', async () => {
|
||||
const element = fixture.nativeElement.querySelector('.adf-comments-input-add');
|
||||
component.message = 'these\nare\nparagraphs\n';
|
||||
element.dispatchEvent(new Event('click'));
|
||||
it('should support multiline comments with HTML', async () => {
|
||||
const commentText: string = [
|
||||
`<form action="/action_page.php">`,
|
||||
`First name: <input type="text" name="fname"><br>`,
|
||||
`Last name: <input type="text" name="lname"><br>`,
|
||||
`<input type="submit" value="Submit">`,
|
||||
`</form>`
|
||||
].join('\n');
|
||||
|
||||
getCommentSpy.and.returnValue(of([]));
|
||||
addCommentSpy.and.returnValue(commentsResponseMock.addComment(commentText));
|
||||
|
||||
component.message = commentText;
|
||||
const addButton = fixture.nativeElement.querySelector('.adf-comments-input-add');
|
||||
addButton.dispatchEvent(new Event('click'));
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(addCommentSpy).toHaveBeenCalledWith('123', 'these<br/>are<br/>paragraphs');
|
||||
expect(addCommentSpy).toHaveBeenCalledWith('123', commentText);
|
||||
|
||||
const messageElement = fixture.nativeElement.querySelector('.adf-comment-message');
|
||||
expect(messageElement.innerText).toBe(commentText);
|
||||
});
|
||||
|
||||
it('should call service to add a comment when add button is pressed', async () => {
|
||||
|
@ -19,15 +19,13 @@ import { CommentModel } from '../models/comment.model';
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Inject,
|
||||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import { Observable, Observer } from 'rxjs';
|
||||
import { share } from 'rxjs/operators';
|
||||
import { ADF_COMMENTS_SERVICE } from './interfaces/comments.token';
|
||||
import { CommentsService } from './interfaces/comments-service.interface';
|
||||
|
||||
@ -49,27 +47,13 @@ export class CommentsComponent implements OnChanges {
|
||||
|
||||
/** Emitted when an error occurs while displaying/adding a comment. */
|
||||
@Output()
|
||||
error: EventEmitter<any> = new EventEmitter<any>();
|
||||
error = new EventEmitter<any>();
|
||||
|
||||
comments: CommentModel[] = [];
|
||||
|
||||
message: string;
|
||||
|
||||
beingAdded: boolean = false;
|
||||
|
||||
private commentObserver: Observer<CommentModel>;
|
||||
comment$: Observable<CommentModel>;
|
||||
|
||||
constructor(@Inject(ADF_COMMENTS_SERVICE) private commentsService: CommentsService) {
|
||||
this.comment$ = new Observable<CommentModel>((observer) => this.commentObserver = observer)
|
||||
.pipe(
|
||||
share()
|
||||
);
|
||||
|
||||
this.comment$.subscribe((comment: CommentModel) => {
|
||||
this.comments.push(comment);
|
||||
});
|
||||
}
|
||||
private commentsService = inject<CommentsService>(ADF_COMMENTS_SERVICE);
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.id = null;
|
||||
@ -97,8 +81,7 @@ export class CommentsComponent implements OnChanges {
|
||||
}
|
||||
|
||||
comments = this.sortedComments(comments);
|
||||
this.addCommentsToObserver(comments);
|
||||
|
||||
this.comments.push(...comments);
|
||||
},
|
||||
(err) => {
|
||||
this.error.emit(err);
|
||||
@ -111,11 +94,9 @@ export class CommentsComponent implements OnChanges {
|
||||
return;
|
||||
}
|
||||
|
||||
const comment: string = this.sanitize(this.message);
|
||||
|
||||
this.beingAdded = true;
|
||||
|
||||
this.commentsService.add(this.id, comment)
|
||||
this.commentsService.add(this.id, this.message)
|
||||
.subscribe(
|
||||
(res: CommentModel) => {
|
||||
this.addToComments(res);
|
||||
@ -164,20 +145,7 @@ export class CommentsComponent implements OnChanges {
|
||||
});
|
||||
}
|
||||
|
||||
private addCommentsToObserver(comments: CommentModel[]): void {
|
||||
comments.forEach((currentComment: CommentModel) => {
|
||||
this.commentObserver.next(currentComment);
|
||||
});
|
||||
}
|
||||
|
||||
private resetComments(): void {
|
||||
this.comments = [];
|
||||
}
|
||||
|
||||
private sanitize(input: string): string {
|
||||
return input.replace(/^\s+|\s+$|\s+(?=\s)/g, '')
|
||||
.replace(/&/g, '&').replace(/</g, '<')
|
||||
.replace(/>/g, '>').replace(/"/g, '"')
|
||||
.replace(/'/g, ''').replace(/\r?\n/g, '<br/>');
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './public-api';
|
@ -1,19 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './comments-service.interface';
|
||||
export * from './comments.token';
|
@ -17,7 +17,7 @@
|
||||
|
||||
import { CommentModel, User } from '../../models';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { CommentsService } from '../interfaces';
|
||||
import { CommentsService } from '../interfaces/comments-service.interface';
|
||||
|
||||
export class CommentsServiceMock implements Partial<CommentsService> {
|
||||
|
||||
@ -33,7 +33,7 @@ export class CommentsServiceMock implements Partial<CommentsService> {
|
||||
|
||||
export const commentsResponseMock = {
|
||||
getComments: () => of([
|
||||
{
|
||||
new CommentModel({
|
||||
id: 1,
|
||||
message: 'Test Comment',
|
||||
created: new Date(),
|
||||
@ -59,8 +59,8 @@ export const commentsResponseMock = {
|
||||
isAdmin: () => false
|
||||
} as User,
|
||||
isSelected: false
|
||||
} as CommentModel,
|
||||
{
|
||||
}),
|
||||
new CommentModel({
|
||||
id: 2,
|
||||
message: 'Test Comment',
|
||||
created: new Date(),
|
||||
@ -86,8 +86,8 @@ export const commentsResponseMock = {
|
||||
isAdmin: () => false
|
||||
} as User,
|
||||
isSelected: false
|
||||
} as CommentModel,
|
||||
{
|
||||
}),
|
||||
new CommentModel({
|
||||
id: 3,
|
||||
message: 'Test Comment',
|
||||
created: new Date(),
|
||||
@ -113,9 +113,10 @@ export const commentsResponseMock = {
|
||||
isAdmin: () => false
|
||||
} as User,
|
||||
isSelected: false
|
||||
} as CommentModel
|
||||
})
|
||||
]),
|
||||
addComment: (message = 'test comment') => of({
|
||||
addComment: (message = 'test comment') => of(
|
||||
new CommentModel({
|
||||
id: 1,
|
||||
message,
|
||||
created: new Date(),
|
||||
@ -141,5 +142,6 @@ export const commentsResponseMock = {
|
||||
isAdmin: () => false
|
||||
} as User,
|
||||
isSelected: false
|
||||
} as CommentModel)
|
||||
})
|
||||
)
|
||||
};
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
import { CommentModel } from '../../models';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { CommentsService } from '../interfaces';
|
||||
import { CommentsService } from '../interfaces/comments-service.interface';
|
||||
import { testUser } from './comments.stories.mock';
|
||||
import { UserLike } from '../../pipes/user-like.interface';
|
||||
|
||||
@ -35,7 +35,7 @@ export class CommentsServiceStoriesMock implements Partial<CommentsService> {
|
||||
|
||||
export const commentsResponseMock = {
|
||||
getComments: () => of([
|
||||
{
|
||||
new CommentModel({
|
||||
id: 1,
|
||||
message: 'Test Comment',
|
||||
created: new Date(),
|
||||
@ -61,8 +61,8 @@ export const commentsResponseMock = {
|
||||
isAdmin: () => false
|
||||
} as UserLike,
|
||||
isSelected: false
|
||||
} as CommentModel,
|
||||
{
|
||||
}),
|
||||
new CommentModel({
|
||||
id: 2,
|
||||
message: 'Test Comment',
|
||||
created: new Date(),
|
||||
@ -88,8 +88,8 @@ export const commentsResponseMock = {
|
||||
isAdmin: () => false
|
||||
} as UserLike,
|
||||
isSelected: false
|
||||
} as CommentModel,
|
||||
{
|
||||
}),
|
||||
new CommentModel({
|
||||
id: 3,
|
||||
message: 'Test Comment',
|
||||
created: new Date(),
|
||||
@ -115,13 +115,15 @@ export const commentsResponseMock = {
|
||||
isAdmin: () => false
|
||||
} as UserLike,
|
||||
isSelected: false
|
||||
} as CommentModel
|
||||
})
|
||||
]),
|
||||
addComment: (message: string) => of({
|
||||
addComment: (message: string) => of(
|
||||
new CommentModel({
|
||||
id: 1,
|
||||
message,
|
||||
created: new Date(),
|
||||
createdBy: testUser,
|
||||
isSelected: false
|
||||
} as CommentModel)
|
||||
})
|
||||
)
|
||||
};
|
||||
|
@ -74,94 +74,94 @@ export const testUser: any = {
|
||||
|
||||
|
||||
export const commentsStoriesData: CommentModel[] = [
|
||||
{
|
||||
new CommentModel({
|
||||
id: 1,
|
||||
message: `I've done this task, what's next?`,
|
||||
created: getDateXMinutesAgo(30),
|
||||
createdBy: johnDoe,
|
||||
isSelected: false
|
||||
},
|
||||
{
|
||||
}),
|
||||
new CommentModel({
|
||||
id: 2,
|
||||
message: `I've assigned you another one 🤠`,
|
||||
created: getDateXMinutesAgo(15),
|
||||
createdBy: janeEod,
|
||||
isSelected: false
|
||||
},
|
||||
{
|
||||
}),
|
||||
new CommentModel({
|
||||
id: 3,
|
||||
message: '+1',
|
||||
created: getDateXMinutesAgo(12),
|
||||
createdBy: robertSmith,
|
||||
isSelected: false
|
||||
},
|
||||
{
|
||||
}),
|
||||
new CommentModel({
|
||||
id: 4,
|
||||
message: 'Cheers',
|
||||
created: new Date(),
|
||||
createdBy: johnDoe,
|
||||
isSelected: false
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
export const commentsNodeData: CommentModel[] = [
|
||||
{
|
||||
new CommentModel({
|
||||
id: 1,
|
||||
message: `I've done this component, is it cool?`,
|
||||
created: getDateXMinutesAgo(30),
|
||||
createdBy: johnDoe,
|
||||
isSelected: false
|
||||
},
|
||||
{
|
||||
}),
|
||||
new CommentModel({
|
||||
id: 2,
|
||||
message: 'Yeah',
|
||||
created: getDateXMinutesAgo(15),
|
||||
createdBy: janeEod,
|
||||
isSelected: false
|
||||
},
|
||||
{
|
||||
}),
|
||||
new CommentModel({
|
||||
id: 3,
|
||||
message: '+1',
|
||||
created: getDateXMinutesAgo(12),
|
||||
createdBy: robertSmith,
|
||||
isSelected: false
|
||||
},
|
||||
{
|
||||
}),
|
||||
new CommentModel({
|
||||
id: 4,
|
||||
message: 'ty',
|
||||
created: new Date(),
|
||||
createdBy: johnDoe,
|
||||
isSelected: false
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
export const commentsTaskData: CommentModel[] = [
|
||||
{
|
||||
new CommentModel({
|
||||
id: 1,
|
||||
message: `I've done this task, what's next?`,
|
||||
created: getDateXMinutesAgo(30),
|
||||
createdBy: johnDoe,
|
||||
isSelected: false
|
||||
},
|
||||
{
|
||||
}),
|
||||
new CommentModel({
|
||||
id: 2,
|
||||
message: `I've assigned you another one 🤠`,
|
||||
created: getDateXMinutesAgo(15),
|
||||
createdBy: janeEod,
|
||||
isSelected: false
|
||||
},
|
||||
{
|
||||
}),
|
||||
new CommentModel({
|
||||
id: 3,
|
||||
message: '+1',
|
||||
created: getDateXMinutesAgo(12),
|
||||
createdBy: robertSmith,
|
||||
isSelected: false
|
||||
},
|
||||
{
|
||||
}),
|
||||
new CommentModel({
|
||||
id: 4,
|
||||
message: 'Cheers',
|
||||
created: new Date(),
|
||||
createdBy: johnDoe,
|
||||
isSelected: false
|
||||
}
|
||||
})
|
||||
];
|
||||
|
@ -17,7 +17,8 @@
|
||||
|
||||
export * from './comments.component';
|
||||
|
||||
export * from './interfaces/index';
|
||||
export * from './interfaces/comments-service.interface';
|
||||
export * from './interfaces/comments.token';
|
||||
|
||||
export * from './comments.module';
|
||||
|
||||
|
@ -266,6 +266,7 @@
|
||||
}
|
||||
},
|
||||
"COMMENTS": {
|
||||
"PROFILE_IMAGE": "Profile Image",
|
||||
"NONE": "No comments",
|
||||
"ADD": "Add",
|
||||
"HEADER": "Comments ({{ count }})",
|
||||
|
@ -24,6 +24,33 @@ export class CommentModel {
|
||||
createdBy: User;
|
||||
isSelected: boolean;
|
||||
|
||||
get hasAvatarPicture(): boolean {
|
||||
return !!(this.createdBy['pictureId'] || this.createdBy['avatarId']);
|
||||
}
|
||||
|
||||
get userDisplayName(): string {
|
||||
let result = '';
|
||||
|
||||
if (this.createdBy) {
|
||||
result = `${this.createdBy.firstName || ''} ${this.createdBy.lastName || ''}`;
|
||||
}
|
||||
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
get userInitials(): string {
|
||||
let result = '';
|
||||
if (this.createdBy) {
|
||||
if (this.createdBy.firstName) {
|
||||
result = this.createdBy.firstName[0];
|
||||
}
|
||||
if (this.createdBy.lastName) {
|
||||
result += this.createdBy.lastName[0];
|
||||
}
|
||||
}
|
||||
return result.toUpperCase();
|
||||
}
|
||||
|
||||
constructor(obj?: any) {
|
||||
if (obj) {
|
||||
this.id = obj.id;
|
||||
|
Loading…
x
Reference in New Issue
Block a user