[ADF-2588] make comment component compatible with content (#3128)

This commit is contained in:
Mario Romano
2018-03-30 11:13:38 +01:00
committed by Eugenio Romano
parent f985dd11d5
commit 653a510a5c
46 changed files with 1186 additions and 462 deletions

View File

@@ -0,0 +1,32 @@
<mat-list class="adf-comment-list">
<mat-list-item *ngFor="let comment of comments"
(click)="selectComment(comment)"
class="adf-comment-list-item"
[class.is-selected]="comment.isSelected"
id="adf-comment-{{comment?.id}}">
<div id="comment-user-icon" class="adf-comment-img-container">
<div
*ngIf="!isPictureDefined(comment.createdBy)"
class="adf-comment-user-icon">
{{getUserShortName(comment.createdBy)}}
</div>
<div>
<img *ngIf="isPictureDefined(comment.createdBy)"
class="adf-people-img"
[src]="getUserImage(comment.createdBy)"
/>
</div>
</div>
<div class="adf-comment-contents">
<div matLine id="comment-user" class="adf-comment-user-name">
{{comment.createdBy?.firstName}} {{comment.createdBy?.lastName}}
</div>
<div matLine id="comment-message" class="adf-comment-message">
{{comment.message}}
</div>
<div matLine id="comment-time" class="adf-comment-message-time">
{{transformDate(comment.created)}}
</div>
</div>
</mat-list-item>
</mat-list>

View File

@@ -0,0 +1,94 @@
@mixin adf-task-list-comment-list-theme($theme) {
$primary: map-get($theme, primary);
$primaryColor: mat-color($primary, 100);
$rippleColor: mat-color($primary, 300);
.is-selected {
background: mat-color($primary, 100);
}
.adf {
&-comment-img-container {
float: left;
width: 40px;
height: 100%;
display: flex;
align-self: flex-start;
padding-top: 18px;
}
&-comment-list-item {
white-space: initial;
display: table-row-group;
padding-top: 12px;
overflow: hidden;
height: 100% !important;
transition: background 0.8s;
background-position: center;
&:hover {
background: $primaryColor radial-gradient(circle, transparent 1%, $primaryColor 1%) center/15000%;
}
&:active {
background-color: $rippleColor;
background-size: 100%;
transition: background 0s;
}
}
&-comment-user-icon {
padding: 10px 5px;
width: 30px;
background-color: mat-color($primary);
border-radius: 50%;
font-size: 16px;
text-align: center;
height: 20px;
background-size: cover;
}
&-comment-user-name {
float: left;
width: calc(100% - 10%);
padding: 2px 10px;
font-weight: 600;
font-size: 14px;
}
&-comment-message {
float: left;
width: calc(100% - 10px);
padding: 2px 10px;
font-style: italic;
white-space: initial !important;
font-size: 14px;
letter-spacing: -0.2px;
line-height: 1.43;
opacity: 0.54;
}
&-comment-message-time {
float: left;
width: calc(100% - 10%);
padding: 2px 10px;
font-size: 12px !important;
opacity: 0.54;
}
&-comment-contents {
width: calc(100% - 10px);
padding-top: 12px;
padding-bottom: 12px;
padding-left: 5px;
}
&-people-img {
border-radius: 90%;
width: 40px;
height: 40px;
vertical-align: middle;
}
}
}

View File

@@ -0,0 +1,306 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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.
*/
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { DatePipe } from '@angular/common';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CommentModel, UserProcessModel } from '../models';
import { CommentListComponent } from './comment-list.component';
import { By } from '@angular/platform-browser';
import { EcmUserService } from '../userinfo/services/ecm-user.service';
import { PeopleProcessService } from '../services/people-process.service';
const testUser: UserProcessModel = new UserProcessModel({
id: '1',
firstName: 'Test',
lastName: 'User',
email: 'tu@domain.com'
});
const testDate = new Date();
const processCommentOne: CommentModel = new CommentModel({
id: 1,
message: 'Test Comment',
created: testDate.toDateString(),
createdBy: testUser
});
const processCommentTwo: CommentModel = new CommentModel({
id: 2,
message: '2nd Test Comment',
created: new Date().toDateString(),
createdBy: testUser
});
const contentCommentUserPictureDefined: CommentModel = new CommentModel({
id: 2,
message: '2nd Test Comment',
created: new Date().toDateString(),
createdBy: {
enabled: true,
firstName: 'some',
lastName: 'one',
email: 'some-one@somegroup.com',
emailNotificationsEnabled: true,
company: {},
id: 'fake-email@dom.com',
avatarId: '001-001-001'
}
});
const processCommentUserPictureDefined: CommentModel = new CommentModel({
id: 2,
message: '2nd Test Comment',
created: new Date().toDateString(),
createdBy: {
id: '1',
firstName: 'Test',
lastName: 'User',
email: 'tu@domain.com',
pictureId: '001-001-001'
}
});
const contentCommentUserNoPictureDefined: CommentModel = new CommentModel({
id: 2,
message: '2nd Test Comment',
created: new Date().toDateString(),
createdBy: {
enabled: true,
firstName: 'some',
lastName: 'one',
email: 'some-one@somegroup.com',
emailNotificationsEnabled: true,
company: {},
id: 'fake-email@dom.com'
}
});
const processCommentUserNoPictureDefined: CommentModel = new CommentModel({
id: 2,
message: '2nd Test Comment',
created: new Date().toDateString(),
createdBy: {
id: '1',
firstName: 'Test',
lastName: 'User',
email: 'tu@domain.com'
}
});
describe('CommentListComponent', () => {
let commentList: CommentListComponent;
let fixture: ComponentFixture<CommentListComponent>;
let element: HTMLElement;
let ecmUserService: EcmUserService;
let peopleProcessService: PeopleProcessService;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
CommentListComponent
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
providers: [
DatePipe,
PeopleProcessService,
EcmUserService
]
}).compileComponents().then(() => {
fixture = TestBed.createComponent(CommentListComponent);
ecmUserService = TestBed.get(EcmUserService);
peopleProcessService = TestBed.get(PeopleProcessService);
commentList = fixture.componentInstance;
element = fixture.nativeElement;
fixture.detectChanges();
});
}));
beforeEach(() => {
spyOn(ecmUserService, 'getUserProfileImage').and.returnValue('content-user-image');
spyOn(peopleProcessService, 'getUserImage').and.returnValue('process-user-image');
});
it('should emit row click event', async(() => {
commentList.comments = [processCommentOne];
commentList.clickRow.subscribe(selectedComment => {
expect(selectedComment.id).toEqual(1);
expect(selectedComment.message).toEqual('Test Comment');
expect(selectedComment.createdBy).toEqual(testUser);
expect(selectedComment.created).toEqual(testDate.toDateString());
expect(selectedComment.isSelected).toBeTruthy();
});
fixture.detectChanges();
fixture.whenStable().then(() => {
let 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', async(() => {
processCommentOne.isSelected = true;
commentList.selectedComment = processCommentOne;
commentList.comments = [processCommentOne, processCommentTwo];
commentList.clickRow.subscribe(selectedComment => {
fixture.detectChanges();
let commentSelectedList = fixture.nativeElement.querySelectorAll('.is-selected');
expect(commentSelectedList.length).toBe(1);
expect(commentSelectedList[0].textContent).toContain('2nd Test Comment');
});
fixture.detectChanges();
fixture.whenStable().then(() => {
let comment = fixture.debugElement.query(By.css('#adf-comment-2'));
comment.triggerEventHandler('click', null);
});
}));
it('should not show comment list if no input is given', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(fixture.nativeElement.querySelector('adf-datatable')).toBeNull();
});
}));
it('should show comment message when input is given', async(() => {
commentList.comments = [processCommentOne];
fixture.detectChanges();
fixture.whenStable().then(() => {
let elements = fixture.nativeElement.querySelectorAll('#comment-message');
expect(elements.length).toBe(1);
expect(elements[0].innerText).toBe(processCommentOne.message);
expect(fixture.nativeElement.querySelector('#comment-message:empty')).toBeNull();
});
}));
it('should show comment user when input is given', async(() => {
commentList.comments = [processCommentOne];
fixture.detectChanges();
fixture.whenStable().then(() => {
let elements = fixture.nativeElement.querySelectorAll('#comment-user');
expect(elements.length).toBe(1);
expect(elements[0].innerText).toBe(processCommentOne.createdBy.firstName + ' ' + processCommentOne.createdBy.lastName);
expect(fixture.nativeElement.querySelector('#comment-user:empty')).toBeNull();
});
}));
it('should show comment date time when input is given', async(() => {
commentList.comments = [processCommentOne];
fixture.detectChanges();
fixture.whenStable().then(() => {
let elements = fixture.nativeElement.querySelectorAll('#comment-time');
expect(elements.length).toBe(1);
expect(elements[0].innerText).toBe(commentList.transformDate(testDate.toDateString()));
expect(fixture.nativeElement.querySelector('#comment-time:empty')).toBeNull();
});
}));
it('comment date time should start with Today when comment date is today', async(() => {
commentList.comments = [processCommentOne];
fixture.detectChanges();
fixture.whenStable().then(() => {
element = fixture.nativeElement.querySelector('#comment-time');
expect(element.innerText).toContain('Today');
});
}));
it('comment date time should start with Yesterday when comment date is yesterday', async(() => {
processCommentOne.created = new Date((Date.now() - 24 * 3600 * 1000));
commentList.comments = [processCommentOne];
fixture.detectChanges();
fixture.whenStable().then(() => {
element = fixture.nativeElement.querySelector('#comment-time');
expect(element.innerText).toContain('Yesterday');
});
}));
it('comment date time should not start with Today/Yesterday when comment date is before yesterday', async(() => {
processCommentOne.created = new Date((Date.now() - 24 * 3600 * 1000 * 2));
commentList.comments = [processCommentOne];
fixture.detectChanges();
fixture.whenStable().then(() => {
element = fixture.nativeElement.querySelector('#comment-time');
expect(element.innerText).not.toContain('Today');
expect(element.innerText).not.toContain('Yesterday');
});
}));
it('should show user icon when input is given', async(() => {
commentList.comments = [processCommentOne];
fixture.detectChanges();
fixture.whenStable().then(() => {
let elements = fixture.nativeElement.querySelectorAll('#comment-user-icon');
expect(elements.length).toBe(1);
expect(elements[0].innerText).toContain(commentList.getUserShortName(processCommentOne.createdBy));
expect(fixture.nativeElement.querySelector('#comment-user-icon:empty')).toBeNull();
});
}));
it('should return content picture when is a content user with a picture', async(() => {
commentList.comments = [contentCommentUserPictureDefined];
fixture.detectChanges();
fixture.whenStable().then(() => {
let elements = fixture.nativeElement.querySelectorAll('.adf-people-img');
expect(elements.length).toBe(1);
expect(fixture.nativeElement.getElementsByClassName('adf-people-img')[0].src).toContain('content-user-image');
});
}));
it('should return process picture when is a process user with a picture', async(() => {
commentList.comments = [processCommentUserPictureDefined];
fixture.detectChanges();
fixture.whenStable().then(() => {
let elements = fixture.nativeElement.querySelectorAll('.adf-people-img');
expect(elements.length).toBe(1);
expect(fixture.nativeElement.getElementsByClassName('adf-people-img')[0].src).toContain('process-user-image');
});
}));
it('should return content short name when is a content user without a picture', async(() => {
commentList.comments = [contentCommentUserNoPictureDefined];
fixture.detectChanges();
fixture.whenStable().then(() => {
let elements = fixture.nativeElement.querySelectorAll('.adf-comment-user-icon');
expect(elements.length).toBe(1);
});
}));
it('should return process short name when is a process user without a picture', async(() => {
commentList.comments = [processCommentUserNoPictureDefined];
fixture.detectChanges();
fixture.whenStable().then(() => {
let elements = fixture.nativeElement.querySelectorAll('.adf-comment-user-icon');
expect(elements.length).toBe(1);
});
}));
});

View File

@@ -0,0 +1,101 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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.
*/
import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
import { CommentModel } from '../models/comment.model';
import { EcmUserService } from '../userinfo/services/ecm-user.service';
import { PeopleProcessService } from '../services/people-process.service';
import { DatePipe } from '@angular/common';
@Component({
selector: 'adf-comment-list',
templateUrl: './comment-list.component.html',
styleUrls: ['./comment-list.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class CommentListComponent {
/** The comments data used to populate the list. */
@Input()
comments: CommentModel[];
/** Emitted when the user clicks on one of the comment rows. */
@Output()
clickRow: EventEmitter<CommentModel> = new EventEmitter<CommentModel>();
selectedComment: CommentModel;
constructor(private datePipe: DatePipe, public peopleProcessService: PeopleProcessService,
public ecmUserService: EcmUserService) {
}
selectComment(comment: CommentModel): void {
if (this.selectedComment) {
this.selectedComment.isSelected = false;
}
comment.isSelected = true;
this.selectedComment = comment;
this.clickRow.emit(this.selectedComment);
}
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 {
if (this.isAContentUsers(user)) {
return this.ecmUserService.getUserProfileImage(user.avatarId);
} else {
return this.peopleProcessService.getUserImage(user);
}
}
transformDate(aDate: string): string {
let formattedDate: string;
let givenDate = Number.parseInt(this.datePipe.transform(aDate, 'yMMdd'));
let today = Number.parseInt(this.datePipe.transform(Date.now(), 'yMMdd'));
if (givenDate === today) {
formattedDate = 'Today, ' + this.datePipe.transform(aDate, 'hh:mm a');
} else {
let yesterday = Number.parseInt(this.datePipe.transform(Date.now() - 24 * 3600 * 1000, 'yMMdd'));
if (givenDate === yesterday) {
formattedDate = 'Yesterday, ' + this.datePipe.transform(aDate, 'hh:mm a');
} else {
formattedDate = this.datePipe.transform(aDate, 'MMM dd y, hh:mm a');
}
}
return formattedDate;
}
private isAContentUsers(user: any): boolean {
return user.avatarId;
}
}

View File

@@ -0,0 +1,15 @@
<div class="adf-comments-container">
<div id="comment-header" class="adf-comments-header">
{{'ADF_TASK_LIST.DETAILS.COMMENTS.HEADER' | translate: { count: comments?.length} }}
</div>
<div class="adf-comments-input-container" *ngIf="!isReadOnly()">
<mat-form-field class="adf-full-width">
<input matInput id="comment-input" placeholder="{{'ADF_TASK_LIST.DETAILS.COMMENTS.ADD' | translate}}" [(ngModel)]="message" (keyup.enter)="add()" (keyup.esc)="clear()">
</mat-form-field>
</div>
<div *ngIf="comments.length > 0">
<adf-comment-list [comments]="comments">
</adf-comment-list>
</div>
</div>

View File

@@ -0,0 +1,36 @@
@mixin adf-task-list-comment-theme($theme) {
$foreground: map-get($theme, foreground);
$header-border: 1px solid mat-color($foreground, divider);
.adf-comments-container {
height: 100%;
width: 100%;
overflow: auto;
}
.adf-comments-header {
padding: 10px 20px;
font-size: 14px;
font-weight: 600;
border-bottom: $header-border;
}
.adf-comments-input-container {
padding: 0 15px;
width: calc(100% - 30px);
padding-top: 8px;
border-bottom: $header-border;
}
.adf-full-width {
width: 100%;
}
adf-comment-list {
float: left;
overflow: auto;
height: calc(100% - 101px);
width: 100%;
}
}

View File

@@ -0,0 +1,344 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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.
*/
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { SimpleChange } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import { CommentProcessService } from '../services/comment-process.service';
import { DatePipe } from '@angular/common';
import { PeopleProcessService } from '../services/people-process.service';
import { CommentListComponent } from './comment-list.component';
import { CommentsComponent } from './comments.component';
import { CommentContentService } from '../services/comment-content.service';
import { EcmUserService } from '../userinfo/services/ecm-user.service';
describe('CommentsComponent', () => {
let component: CommentsComponent;
let fixture: ComponentFixture<CommentsComponent>;
let getProcessCommentsSpy: jasmine.Spy;
let addProcessCommentSpy: jasmine.Spy;
let addContentCommentSpy: jasmine.Spy;
let getContentCommentsSpy: jasmine.Spy;
let commentProcessService: CommentProcessService;
let commentContentService: CommentContentService;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
],
declarations: [
CommentsComponent,
CommentListComponent
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
providers: [
DatePipe,
PeopleProcessService,
CommentProcessService,
CommentContentService,
EcmUserService
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CommentsComponent);
component = fixture.componentInstance;
commentProcessService = fixture.debugElement.injector.get(CommentProcessService);
commentContentService = fixture.debugElement.injector.get(CommentContentService);
addContentCommentSpy = spyOn(commentContentService, 'addNodeComment').and.returnValue(Observable.of({
id: 123,
message: 'Test Comment',
createdBy: {id: '999'}
}));
getContentCommentsSpy = spyOn(commentContentService, 'getNodeComments').and.returnValue(Observable.of([
{message: 'Test1', created: Date.now(), createdBy: {firstName: 'Admin', lastName: 'User'}},
{message: 'Test2', created: Date.now(), createdBy: {firstName: 'Admin', lastName: 'User'}},
{message: 'Test3', created: Date.now(), createdBy: {firstName: 'Admin', lastName: 'User'}}
]));
getProcessCommentsSpy = spyOn(commentProcessService, 'getTaskComments').and.returnValue(Observable.of([
{message: 'Test1', created: Date.now(), createdBy: {firstName: 'Admin', lastName: 'User'}},
{message: 'Test2', created: Date.now(), createdBy: {firstName: 'Admin', lastName: 'User'}},
{message: 'Test3', created: Date.now(), createdBy: {firstName: 'Admin', lastName: 'User'}}
]));
addProcessCommentSpy = spyOn(commentProcessService, 'addTaskComment').and.returnValue(Observable.of({
id: 123,
message: 'Test Comment',
createdBy: {id: '999'}
}));
});
it('should load comments when taskId specified', () => {
let change = new SimpleChange(null, '123', true);
component.ngOnChanges({'taskId': change});
expect(getProcessCommentsSpy).toHaveBeenCalled();
});
it('should load comments when nodeId specified', () => {
let change = new SimpleChange(null, '123', true);
component.ngOnChanges({'nodeId': change});
expect(getContentCommentsSpy).toHaveBeenCalled();
});
it('should emit an error when an error occurs loading comments', () => {
let emitSpy = spyOn(component.error, 'emit');
getProcessCommentsSpy.and.returnValue(Observable.throw({}));
let change = new SimpleChange(null, '123', true);
component.ngOnChanges({'taskId': change});
expect(emitSpy).toHaveBeenCalled();
});
it('should not load comments when no taskId is specified', () => {
fixture.detectChanges();
expect(getProcessCommentsSpy).not.toHaveBeenCalled();
});
it('should display comments when the task has comments', async(() => {
let change = new SimpleChange(null, '123', true);
component.ngOnChanges({'taskId': change});
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.nativeElement.querySelectorAll('#comment-message').length).toBe(3);
expect(fixture.nativeElement.querySelector('#comment-message:empty')).toBeNull();
});
}));
it('should display comments count when the task has comments', async(() => {
let change = new SimpleChange(null, '123', true);
component.ngOnChanges({'taskId': change});
fixture.whenStable().then(() => {
fixture.detectChanges();
let element = fixture.nativeElement.querySelector('#comment-header');
expect(element.innerText).toBe('ADF_TASK_LIST.DETAILS.COMMENTS.HEADER');
});
}));
it('should not display comments when the task has no comments', async(() => {
component.taskId = '123';
getProcessCommentsSpy.and.returnValue(Observable.of([]));
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('#comment-container')).toBeNull();
});
}));
it('should display comments input by default', async(() => {
let change = new SimpleChange(null, '123', true);
component.ngOnChanges({'taskId': change});
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('#comment-input')).not.toBeNull();
});
}));
it('should not display comments input when the task is readonly', async(() => {
component.readOnly = true;
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('#comment-input')).toBeNull();
});
}));
describe('change detection taskId', () => {
let change = new SimpleChange('123', '456', true);
let nullChange = new SimpleChange('123', null, true);
beforeEach(async(() => {
component.taskId = '123';
fixture.detectChanges();
fixture.whenStable().then(() => {
getProcessCommentsSpy.calls.reset();
});
}));
it('should fetch new comments when taskId changed', () => {
component.ngOnChanges({'taskId': change});
expect(getProcessCommentsSpy).toHaveBeenCalledWith('456');
});
it('should not fetch new comments when empty changeset made', () => {
component.ngOnChanges({});
expect(getProcessCommentsSpy).not.toHaveBeenCalled();
});
it('should not fetch new comments when taskId changed to null', () => {
component.ngOnChanges({'taskId': nullChange});
expect(getProcessCommentsSpy).not.toHaveBeenCalled();
});
});
describe('change detection node', () => {
let change = new SimpleChange('123', '456', true);
let nullChange = new SimpleChange('123', null, true);
beforeEach(async(() => {
component.nodeId = '123';
fixture.detectChanges();
fixture.whenStable().then(() => {
getContentCommentsSpy.calls.reset();
});
}));
it('should fetch new comments when nodeId changed', () => {
component.ngOnChanges({'nodeId': change});
expect(getContentCommentsSpy).toHaveBeenCalledWith('456');
});
it('should not fetch new comments when empty changeset made', () => {
component.ngOnChanges({});
expect(getContentCommentsSpy).not.toHaveBeenCalled();
});
it('should not fetch new comments when nodeId changed to null', () => {
component.ngOnChanges({'nodeId': nullChange});
expect(getContentCommentsSpy).not.toHaveBeenCalled();
});
});
describe('Add comment task', () => {
beforeEach(async(() => {
component.taskId = '123';
fixture.detectChanges();
fixture.whenStable();
}));
it('should call service to add a comment when enter key is pressed', async(() => {
let event = new KeyboardEvent('keyup', {'key': 'Enter'});
let element = fixture.nativeElement.querySelector('#comment-input');
component.message = 'Test Comment';
element.dispatchEvent(event);
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(addProcessCommentSpy).toHaveBeenCalled();
let elements = fixture.nativeElement.querySelectorAll('#comment-message');
expect(elements.length).toBe(1);
expect(elements[0].innerText).toBe('Test Comment');
});
}));
it('should not call service to add a comment when comment is empty', async(() => {
let event = new KeyboardEvent('keyup', {'key': 'Enter'});
let element = fixture.nativeElement.querySelector('#comment-input');
component.message = '';
element.dispatchEvent(event);
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(addProcessCommentSpy).not.toHaveBeenCalled();
});
}));
it('should clear comment when escape key is pressed', async(() => {
let event = new KeyboardEvent('keyup', {'key': 'Escape'});
let element = fixture.nativeElement.querySelector('#comment-input');
component.message = 'Test comment';
element.dispatchEvent(event);
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
element = fixture.nativeElement.querySelector('#comment-input');
expect(element.value).toBe('');
});
}));
it('should emit an error when an error occurs adding the comment', () => {
let emitSpy = spyOn(component.error, 'emit');
addProcessCommentSpy.and.returnValue(Observable.throw({}));
component.message = 'Test comment';
component.add();
expect(emitSpy).toHaveBeenCalled();
});
});
describe('Add comment node', () => {
beforeEach(async(() => {
component.nodeId = '123';
fixture.detectChanges();
fixture.whenStable();
}));
it('should call service to add a comment when enter key is pressed', async(() => {
let event = new KeyboardEvent('keyup', {'key': 'Enter'});
let element = fixture.nativeElement.querySelector('#comment-input');
component.message = 'Test Comment';
element.dispatchEvent(event);
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(addContentCommentSpy).toHaveBeenCalled();
let elements = fixture.nativeElement.querySelectorAll('#comment-message');
expect(elements.length).toBe(1);
expect(elements[0].innerText).toBe('Test Comment');
});
}));
it('should not call service to add a comment when comment is empty', async(() => {
let event = new KeyboardEvent('keyup', {'key': 'Enter'});
let element = fixture.nativeElement.querySelector('#comment-input');
component.message = '';
element.dispatchEvent(event);
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(addContentCommentSpy).not.toHaveBeenCalled();
});
}));
it('should clear comment when escape key is pressed', async(() => {
let event = new KeyboardEvent('keyup', {'key': 'Escape'});
let element = fixture.nativeElement.querySelector('#comment-input');
component.message = 'Test comment';
element.dispatchEvent(event);
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
element = fixture.nativeElement.querySelector('#comment-input');
expect(element.value).toBe('');
});
}));
it('should emit an error when an error occurs adding the comment', () => {
let emitSpy = spyOn(component.error, 'emit');
addContentCommentSpy.and.returnValue(Observable.throw({}));
component.message = 'Test comment';
component.add();
expect(emitSpy).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,179 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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.
*/
import { CommentProcessService } from '../services/comment-process.service';
import { CommentContentService } from '../services/comment-content.service';
import { CommentModel } from '../models/comment.model';
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Observer } from 'rxjs/Observer';
@Component({
selector: 'adf-comments',
templateUrl: './comments.component.html',
styleUrls: ['./comments.component.scss']
})
export class CommentsComponent implements OnChanges {
/** The numeric ID of the task. */
@Input()
taskId: string;
/** The numeric ID of the node. */
@Input()
nodeId: string;
/** Are the comments read only? */
@Input()
readOnly: boolean = false;
/** Emitted when an error occurs while displaying/adding a comment. */
@Output()
error: EventEmitter<any> = new EventEmitter<any>();
comments: CommentModel [] = [];
private commentObserver: Observer<CommentModel>;
comment$: Observable<CommentModel>;
message: string;
beingAdded: boolean = false;
constructor(private commentProcessService: CommentProcessService, private commentContentService: CommentContentService) {
this.comment$ = new Observable<CommentModel>(observer => this.commentObserver = observer).share();
this.comment$.subscribe((comment: CommentModel) => {
this.comments.push(comment);
});
}
ngOnChanges(changes: SimpleChanges) {
this.taskId = null;
this.nodeId = null;
this.taskId = changes['taskId'] ? changes['taskId'].currentValue : null;
this.nodeId = changes['nodeId'] ? changes['nodeId'].currentValue : null;
if (this.taskId || this.nodeId) {
this.getComments();
} else {
this.resetComments();
}
}
private getComments(): void {
this.resetComments();
if (this.isATask()) {
this.commentProcessService.getTaskComments(this.taskId).subscribe(
(res: CommentModel[]) => {
if (res && res instanceof Array) {
res = res.sort((comment1: CommentModel, comment2: CommentModel) => {
let date1 = new Date(comment1.created);
let date2 = new Date(comment2.created);
return date1 > date2 ? -1 : date1 < date2 ? 1 : 0;
});
res.forEach((comment) => {
this.commentObserver.next(comment);
});
}
},
(err) => {
this.error.emit(err);
}
);
}
if (this.isANode()) {
this.commentContentService.getNodeComments(this.nodeId).subscribe(
(res: CommentModel[]) => {
if (res && res instanceof Array) {
res = res.sort((comment1: CommentModel, comment2: CommentModel) => {
const date1 = new Date(comment1.created);
const date2 = new Date(comment2.created);
return date1 > date2 ? -1 : date1 < date2 ? 1 : 0;
});
res.forEach((comment) => {
this.commentObserver.next(comment);
});
}
},
(err) => {
this.error.emit(err);
}
);
}
}
private resetComments(): void {
this.comments = [];
}
add(): void {
if (this.message && this.message.trim() && !this.beingAdded) {
this.beingAdded = true;
if (this.isATask()) {
this.commentProcessService.addTaskComment(this.taskId, this.message)
.subscribe(
(res: CommentModel) => {
this.comments.unshift(res);
this.message = '';
this.beingAdded = false;
},
(err) => {
this.error.emit(err);
this.beingAdded = false;
}
);
}
if (this.isANode()) {
this.commentContentService.addNodeComment(this.nodeId, this.message)
.subscribe(
(res: CommentModel) => {
this.comments.unshift(res);
this.message = '';
this.beingAdded = false;
},
(err) => {
this.error.emit(err);
this.beingAdded = false;
}
);
}
}
}
clear(): void {
this.message = '';
}
isReadOnly(): boolean {
return this.readOnly;
}
isATask(): boolean {
return this.taskId ? true : false;
}
isANode(): boolean {
return this.nodeId ? true : false;
}
}

View File

@@ -0,0 +1,49 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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.
*/
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { MaterialModule } from '../material.module';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { DataColumnModule } from '../data-column/data-column.module';
import { DataTableModule } from '../datatable/datatable.module';
import { CommentListComponent } from './comment-list.component';
import { CommentsComponent } from './comments.component';
@NgModule({
imports: [
DataColumnModule,
DataTableModule,
FormsModule,
ReactiveFormsModule,
MaterialModule,
CommonModule,
TranslateModule
],
declarations: [
CommentListComponent,
CommentsComponent
],
exports: [
CommentListComponent,
CommentsComponent
]
})
export class CommentsModule {
}

View File

@@ -0,0 +1,18 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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';

View File

@@ -0,0 +1,19 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 './comment-list.component';
export * from './comments.component';