👽 Angular 14 rebase 👽 (#7769)

* fix after rebase

* new release strategy for ng next

Signed-off-by: eromano <eugenioromano16@gmail.com>

* peer dep

Signed-off-by: eromano <eugenioromano16@gmail.com>

* Angular 14

fix unit test and storybook

Signed-off-by: eromano <eugenioromano16@gmail.com>

fix after rebase

Signed-off-by: eromano <eugenioromano16@gmail.com>

update pkg.json

Signed-off-by: eromano <eugenioromano16@gmail.com>

missing dep

Signed-off-by: eromano <eugenioromano16@gmail.com>

Fix mistake and missing code

Dream....build only affected libs

Add utility run commands

* Use nx command to run affected tests

* Fix nx test core

fix content tests

Run unit with watch false

core test fixes

reduce test warnings

Fix process cloud unit

Fix adf unit test

Fix lint process cloud

Disable lint next line

Use right core path

Fix insights unit

fix linting insights

Fix process-services unit

fix the extensions test report

fix test warnings

Fix content unit

Fix bunch of content unit

* Produce an adf alpha of 14

* hopefully fixing the content

* Push back the npm publish

* Remove flaky unit

* Fix linting

* Make the branch as root

* Get rid of angualar13

* Remove the travis depth

* Fixing version for npm

* Enabling cache for unit and build

* Fix scss for core and paths

Copy i18 and asset by using ng-packager

Export the theming alias and fix path

Use ng-package to copy assets process-services-cloud

Use ng-package to copy assets process-services

Use ng-package to copy assets content-services

Use ng-package to copy assets insights

* feat: fix api secondary entry point

* fix storybook rebase

* Move dist under dist/libs from lib/dist

* Fix the webstyle

* Use only necessary nrwl deps and improve lint

* Fix unit for libs

* Convert lint.sh to targets - improve performance

* Use latest of angular

* Align alfresco-js-api

Signed-off-by: eromano <eugenioromano16@gmail.com>
Co-authored-by: eromano <eugenioromano16@gmail.com>
Co-authored-by: Mikolaj Serwicki <mikolaj.serwicki@hyland.com>
Co-authored-by: Tomasz <tomasz.gnyp@hyland.com>
This commit is contained in:
Maurizio Vitale
2022-08-25 10:50:30 +01:00
committed by GitHub
parent 53bc5aab2c
commit 1fa81962a0
1351 changed files with 26853 additions and 11958 deletions

View File

@@ -0,0 +1,29 @@
<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 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 [alt]="comment.createdBy" *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" [innerHTML]="comment.message"></div>
<div matLine id="comment-time" class="adf-comment-message-time">
{{ comment.created | adfTimeAgo: currentLocale }}
</div>
</div>
</mat-list-item>
</mat-list>

View File

@@ -0,0 +1,95 @@
.adf-is-selected {
background: var(--adf-comment-list-primary-color);
}
.adf {
&-comment-img-container {
float: left;
width: 40px;
height: 100%;
display: flex;
align-self: flex-start;
padding-top: 18px;
}
&-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-comment-list-primary-color)
radial-gradient(circle, transparent 1%, var(--adf-comment-list-primary-color) 1%)
center/15000%;
}
&:active {
background-color: var(--adf-comment-list-ripple-color);
background-size: 100%;
transition: background 0s;
}
}
&-comment-user-icon {
padding: 10px 5px;
width: 30px;
background-color: var(--theme-primary-color);
color: var(--theme-primary-color-default-contrast);
border-radius: 50%;
font-size: var(--theme-subheading-2-font-size);
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: var(--theme-body-1-font-size);
}
&-comment-message {
float: left;
width: calc(100% - 10px);
padding: 2px 10px;
font-style: italic;
/* stylelint-disable */
white-space: initial !important;
/* stylelint-enable */
font-size: var(--theme-body-1-font-size);
letter-spacing: -0.2px;
line-height: 1.43;
color: var(--theme-foreground-text-color);
}
&-comment-message-time {
float: left;
width: calc(100% - 10%);
padding: 2px 10px;
font-size: var(--theme-caption-font-size) !important;
color: var(--theme-foreground-text-color);
}
&-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,299 @@
/*!
* @license
* Copyright 2019 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 { ComponentFixture, fakeAsync, 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 '../services/ecm-user.service';
import { PeopleProcessService } from '../services/people-process.service';
import { setupTestBed } from '../testing/setup-test-bed';
import { CoreTestingModule } from '../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
const testUser = new UserProcessModel({
id: '1',
firstName: 'Test',
lastName: 'User',
email: 'tu@domain.com'
});
const processCommentOne = new CommentModel({
id: 1,
message: 'Test Comment',
created: new Date(),
createdBy: testUser
});
const processCommentTwo = new CommentModel({
id: 2,
message: '2nd Test Comment',
created: new Date(),
createdBy: testUser
});
const contentCommentUserPictureDefined = new CommentModel({
id: 2,
message: '2nd Test Comment',
created: new Date(),
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 = new CommentModel({
id: 2,
message: '2nd Test Comment',
created: new Date(),
createdBy: {
id: '1',
firstName: 'Test',
lastName: 'User',
email: 'tu@domain.com',
pictureId: '001-001-001'
}
});
const contentCommentUserNoPictureDefined = new CommentModel({
id: 2,
message: '2nd Test Comment',
created: new Date(),
createdBy: {
enabled: true,
firstName: 'some',
lastName: 'one',
email: 'some-one@somegroup.com',
emailNotificationsEnabled: true,
company: {},
id: 'fake-email@dom.com'
}
});
const processCommentUserNoPictureDefined = new CommentModel({
id: 2,
message: '2nd Test Comment',
created: new Date(),
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;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
beforeEach(() => {
ecmUserService = TestBed.inject(EcmUserService);
spyOn(ecmUserService, 'getUserProfileImage').and.returnValue('alfresco-logo.svg');
peopleProcessService = TestBed.inject(PeopleProcessService);
spyOn(peopleProcessService, 'getUserImage').and.returnValue('alfresco-logo.svg');
fixture = TestBed.createComponent(CommentListComponent);
commentList = fixture.componentInstance;
element = fixture.nativeElement;
fixture.detectChanges();
});
afterEach(() => {
fixture.destroy();
});
it('should emit row click event', fakeAsync(() => {
commentList.comments = [Object.assign({}, processCommentOne)];
commentList.clickRow.subscribe((selectedComment: CommentModel) => {
expect(selectedComment.id).toEqual(1);
expect(selectedComment.message).toEqual('Test Comment');
expect(selectedComment.createdBy).toEqual(testUser);
expect(selectedComment.isSelected).toBeTruthy();
});
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(() => {
processCommentOne.isSelected = true;
const commentOne = Object.assign({}, processCommentOne);
const commentTwo = Object.assign({}, processCommentTwo);
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'));
comment.triggerEventHandler('click', null);
});
}));
it('should not show comment list if no input is given', async () => {
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.querySelector('adf-datatable')).toBeNull();
});
it('should show comment message when input is given', async () => {
commentList.comments = [Object.assign({}, processCommentOne)];
fixture.detectChanges();
await fixture.whenStable();
const 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 = [Object.assign({}, processCommentOne)];
fixture.detectChanges();
await fixture.whenStable();
const 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('comment date time should start with few seconds ago when comment date is few seconds ago', async () => {
const commentFewSecond = Object.assign({}, processCommentOne);
commentFewSecond.created = new Date();
commentList.comments = [commentFewSecond];
fixture.detectChanges();
await fixture.whenStable();
element = fixture.nativeElement.querySelector('#comment-time');
expect(element.innerText).toContain('a few seconds ago');
});
it('comment date time should start with Yesterday when comment date is yesterday', async () => {
const commentOld = Object.assign({}, processCommentOne);
commentOld.created = new Date((Date.now() - 24 * 3600 * 1000));
commentList.comments = [commentOld];
fixture.detectChanges();
await fixture.whenStable();
element = fixture.nativeElement.querySelector('#comment-time');
expect(element.innerText).toContain('a day ago');
});
it('comment date time should not start with Today/Yesterday when comment date is before yesterday', async () => {
const commentOld = Object.assign({}, processCommentOne);
commentOld.created = new Date((Date.now() - 24 * 3600 * 1000 * 2));
commentList.comments = [commentOld];
fixture.detectChanges();
await fixture.whenStable();
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 = [Object.assign({}, processCommentOne)];
fixture.detectChanges();
await fixture.whenStable();
const 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();
await fixture.whenStable();
const elements = fixture.nativeElement.querySelectorAll('.adf-people-img');
expect(elements.length).toBe(1);
expect(fixture.nativeElement.getElementsByClassName('adf-people-img')[0].src).toContain('alfresco-logo.svg');
});
it('should return process picture when is a process user with a picture', async () => {
commentList.comments = [processCommentUserPictureDefined];
fixture.detectChanges();
await fixture.whenStable();
const elements = fixture.nativeElement.querySelectorAll('.adf-people-img');
expect(elements.length).toBe(1);
expect(fixture.nativeElement.getElementsByClassName('adf-people-img')[0].src).toContain('alfresco-logo.svg');
});
it('should return content short name when is a content user without a picture', async () => {
commentList.comments = [contentCommentUserNoPictureDefined];
fixture.detectChanges();
await fixture.whenStable();
const 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();
await fixture.whenStable();
const elements = fixture.nativeElement.querySelectorAll('.adf-comment-user-icon');
expect(elements.length).toBe(1);
});
});

View File

@@ -0,0 +1,64 @@
/*!
* @license
* Copyright 2019 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 { Meta, moduleMetadata, Story } from '@storybook/angular';
import { action } from '@storybook/addon-actions';
import { CommentModel } from '../models/comment.model';
import { CoreStoryModule } from '../testing/core.story.module';
import { CommentListComponent } from './comment-list.component';
import { CommentsModule } from './comments.module';
import { commentsTaskData, commentsNodeData } from '../mock/comment-content.mock';
import { EcmUserService } from '../services';
export default {
component: CommentListComponent,
title: 'Core/Comments/Comment List',
decorators: [
moduleMetadata({
imports: [CoreStoryModule, CommentsModule],
providers: [
{ provide: EcmUserService, useValue: { getUserProfileImage: () => '../assets/images/logo.png' } }
]
})
],
argTypes: {
comments: {
type: CommentModel,
description: 'CommentModel array',
table: {
type: { summary: 'CommentModel' }
}
}
}
} as Meta;
const template: Story<CommentListComponent> = (args: CommentListComponent) => ({
props: {
...args,
clickRow: action('clickRow')
}
});
export const taskBased = template.bind({});
taskBased.args = {
comments: commentsTaskData
};
export const nodeBased = template.bind({});
nodeBased.args = {
comments: commentsNodeData
};

View File

@@ -0,0 +1,101 @@
/*!
* @license
* Copyright 2019 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, OnInit, OnDestroy } from '@angular/core';
import { CommentModel } from '../models/comment.model';
import { EcmUserService } from '../services/ecm-user.service';
import { PeopleProcessService } from '../services/people-process.service';
import { UserPreferencesService, UserPreferenceValues } from '../services/user-preferences.service';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'adf-comment-list',
templateUrl: './comment-list.component.html',
styleUrls: ['./comment-list.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class CommentListComponent implements OnInit, OnDestroy {
/** 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;
currentLocale;
private onDestroy$ = new Subject<boolean>();
constructor(public peopleProcessService: PeopleProcessService,
public ecmUserService: EcmUserService,
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();
}
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);
}
}
private isAContentUsers(user: any): boolean {
return user.avatarId;
}
}

View File

@@ -0,0 +1,26 @@
<div class="adf-comments-container">
<div id="comment-header" class="adf-comments-header" role="heading">
{{'COMMENTS.HEADER' | translate: { count: comments?.length} }}
</div>
<div class="adf-comments-input-container" *ngIf="!isReadOnly()">
<mat-form-field class="adf-full-width">
<textarea (keydown.escape)="clear($event)" matInput id="comment-input" placeholder="{{'COMMENTS.ADD' | translate}}" [(ngModel)]="message"></textarea>
</mat-form-field>
<div class="adf-comments-input-actions">
<button mat-button
class="adf-comments-input-add"
data-automation-id="comments-input-add"
color="primary"
(click)="add()"
[disabled]="!message">
{{ 'COMMENTS.ADD' | translate }}
</button>
</div>
</div>
<div *ngIf="comments.length > 0">
<adf-comment-list [comments]="comments">
</adf-comment-list>
</div>
</div>

View File

@@ -0,0 +1,39 @@
.adf-comments-container {
height: 100%;
width: 100%;
overflow: auto;
}
.adf-comments-header {
padding: 10px 20px;
font-size: var(--theme-body-1-font-size);
font-weight: 600;
border-bottom: 1px solid var(--theme-fg-divider);
}
.adf-comments-input-container {
width: calc(100% - 30px);
padding: 8px 15px 0;
border-bottom: 1px solid var(--theme-fg-divider);
textarea {
resize: vertical;
}
}
.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%;
}

View File

@@ -0,0 +1,385 @@
/*!
* @license
* Copyright 2019 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, SimpleChange } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { of, throwError } from 'rxjs';
import { CommentProcessService } from '../services/comment-process.service';
import { CommentsComponent } from './comments.component';
import { CommentContentService } from '../services/comment-content.service';
import { setupTestBed } from '../testing/setup-test-bed';
import { CoreTestingModule } from '../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { CommentModel } from '../models/comment.model';
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;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
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(of(new CommentModel({
id: 123,
message: 'Test Comment',
createdBy: {id: '999'}
})));
getContentCommentsSpy = spyOn(commentContentService, 'getNodeComments').and.returnValue(of([
new CommentModel({message: 'Test1', created: Date.now(), createdBy: {firstName: 'Admin', lastName: 'User'}}),
new CommentModel({message: 'Test2', created: Date.now(), createdBy: {firstName: 'Admin', lastName: 'User'}}),
new CommentModel({message: 'Test3', created: Date.now(), createdBy: {firstName: 'Admin', lastName: 'User'}})
]));
getProcessCommentsSpy = spyOn(commentProcessService, 'getTaskComments').and.returnValue(of([
new CommentModel({message: 'Test1', created: Date.now(), createdBy: {firstName: 'Admin', lastName: 'User'}}),
new CommentModel({message: 'Test2', created: Date.now(), createdBy: {firstName: 'Admin', lastName: 'User'}}),
new CommentModel({message: 'Test3', created: Date.now(), createdBy: {firstName: 'Admin', lastName: 'User'}})
]));
addProcessCommentSpy = spyOn(commentProcessService, 'addTaskComment').and.returnValue(of(new CommentModel({
id: 123,
message: 'Test Comment',
createdBy: {id: '999'}
})));
});
afterEach(() => {
fixture.destroy();
});
it('should load comments when taskId specified', () => {
const change = new SimpleChange(null, '123', true);
component.ngOnChanges({taskId: change});
expect(getProcessCommentsSpy).toHaveBeenCalled();
});
it('should load comments when nodeId specified', () => {
const change = new SimpleChange(null, '123', true);
component.ngOnChanges({nodeId: change});
expect(getContentCommentsSpy).toHaveBeenCalled();
});
it('should emit an error when an error occurs loading comments', () => {
const emitSpy = spyOn(component.error, 'emit');
getProcessCommentsSpy.and.returnValue(throwError({}));
const 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 () => {
const change = new SimpleChange(null, '123', true);
component.ngOnChanges({taskId: change});
fixture.detectChanges();
await fixture.whenStable();
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 () => {
const change = new SimpleChange(null, '123', true);
component.ngOnChanges({taskId: change});
fixture.detectChanges();
await fixture.whenStable();
const element = fixture.nativeElement.querySelector('#comment-header');
expect(element.innerText).toBe('COMMENTS.HEADER');
});
it('should not display comments when the task has no comments', async () => {
component.taskId = '123';
getProcessCommentsSpy.and.returnValue(of([]));
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.querySelector('#comment-container')).toBeNull();
});
it('should display comments input by default', async () => {
const change = new SimpleChange(null, '123', true);
component.ngOnChanges({taskId: change});
fixture.detectChanges();
await fixture.whenStable();
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();
await fixture.whenStable();
expect(fixture.nativeElement.querySelector('#comment-input')).toBeNull();
});
describe('change detection taskId', () => {
const change = new SimpleChange('123', '456', true);
const nullChange = new SimpleChange('123', null, true);
beforeEach(() => {
component.taskId = '123';
fixture.detectChanges();
});
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', () => {
const change = new SimpleChange('123', '456', true);
const nullChange = new SimpleChange('123', null, true);
beforeEach(() => {
component.nodeId = '123';
fixture.detectChanges();
});
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(() => {
component.taskId = '123';
fixture.detectChanges();
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();
expect(addProcessCommentSpy).toHaveBeenCalledWith('123', 'action');
});
it('should normalize comment when user input contains spaces sequence', async () => {
const element = fixture.nativeElement.querySelector('.adf-comments-input-add');
component.message = 'test comment';
element.dispatchEvent(new Event('click'));
fixture.detectChanges();
await fixture.whenStable();
expect(addProcessCommentSpy).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'));
fixture.detectChanges();
await fixture.whenStable();
expect(addProcessCommentSpy).toHaveBeenCalledWith('123', 'these<br/>are<br/>paragraphs');
});
it('should call service to add a comment when add button is pressed', async () => {
const element = fixture.nativeElement.querySelector('.adf-comments-input-add');
component.message = 'Test Comment';
element.dispatchEvent(new Event('click'));
fixture.detectChanges();
await fixture.whenStable();
expect(addProcessCommentSpy).toHaveBeenCalled();
const 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 () => {
const element = fixture.nativeElement.querySelector('.adf-comments-input-add');
component.message = '';
element.dispatchEvent(new Event('click'));
fixture.detectChanges();
await fixture.whenStable();
expect(addProcessCommentSpy).not.toHaveBeenCalled();
});
it('should clear comment when escape key is pressed', async () => {
const event = new KeyboardEvent('keydown', {key: 'Escape'});
let element = fixture.nativeElement.querySelector('#comment-input');
element.dispatchEvent(event);
fixture.detectChanges();
await fixture.whenStable();
element = fixture.nativeElement.querySelector('#comment-input');
expect(element.value).toBe('');
});
it('should emit an error when an error occurs adding the comment', () => {
const emitSpy = spyOn(component.error, 'emit');
addProcessCommentSpy.and.returnValue(throwError({}));
component.message = 'Test comment';
component.add();
expect(emitSpy).toHaveBeenCalled();
});
});
describe('Add comment node', () => {
beforeEach(() => {
component.nodeId = '123';
fixture.detectChanges();
fixture.whenStable();
});
it('should call service to add a comment when add button is pressed', async () => {
const element = fixture.nativeElement.querySelector('.adf-comments-input-add');
component.message = 'Test Comment';
element.dispatchEvent(new Event('click'));
fixture.detectChanges();
await fixture.whenStable();
expect(addContentCommentSpy).toHaveBeenCalled();
const elements = fixture.nativeElement.querySelectorAll('#comment-message');
expect(elements.length).toBe(1);
expect(elements[0].innerText).toBe('Test Comment');
});
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();
expect(addContentCommentSpy).toHaveBeenCalledWith('123', 'action');
});
it('should normalize comment when user input contains spaces sequence', async () => {
const element = fixture.nativeElement.querySelector('.adf-comments-input-add');
component.message = 'test comment';
element.dispatchEvent(new Event('click'));
fixture.detectChanges();
await fixture.whenStable();
expect(addContentCommentSpy).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'));
fixture.detectChanges();
await fixture.whenStable();
expect(addContentCommentSpy).toHaveBeenCalledWith('123', 'these<br/>are<br/>paragraphs');
});
it('should not call service to add a comment when comment is empty', async () => {
const element = fixture.nativeElement.querySelector('.adf-comments-input-add');
component.message = '';
element.dispatchEvent(new Event('click'));
fixture.detectChanges();
await fixture.whenStable();
expect(addContentCommentSpy).not.toHaveBeenCalled();
});
it('should clear comment when escape key is pressed', async () => {
const event = new KeyboardEvent('keydown', {key: 'Escape'});
let element = fixture.nativeElement.querySelector('#comment-input');
element.dispatchEvent(event);
fixture.detectChanges();
await fixture.whenStable();
element = fixture.nativeElement.querySelector('#comment-input');
expect(element.value).toBe('');
});
it('should emit an error when an error occurs adding the comment', () => {
const emitSpy = spyOn(component.error, 'emit');
addContentCommentSpy.and.returnValue(throwError({}));
component.message = 'Test comment';
component.add();
expect(emitSpy).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,111 @@
/*!
* @license
* Copyright 2019 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 { Meta, moduleMetadata, Story } from '@storybook/angular';
import { CommentContentService, CommentProcessService, EcmUserService } from '../services';
import { CoreStoryModule } from '../testing/core.story.module';
import { CommentsComponent } from './comments.component';
import { CommentsModule } from './comments.module';
import { CommentModel } from '../models/comment.model';
import { CommentContentServiceMock } from '../mock/comment-content-service.mock';
import { CommentProcessServiceMock } from '../mock/comment-process-service.mock';
import { commentsTaskData, commentsNodeData } from '../mock/comment-content.mock';
export default {
component: CommentsComponent,
title: 'Core/Comments/Comment',
decorators: [
moduleMetadata({
imports: [CoreStoryModule, CommentsModule],
providers: [
{ provide: CommentContentService, useClass: CommentContentServiceMock },
{ provide: CommentProcessService, useClass: CommentProcessServiceMock },
{ provide: EcmUserService, useValue: { getUserProfileImage: () => '../assets/images/logo.png' } }
]
})
],
argTypes: {
comments: {
type: CommentModel,
description: 'CommentModel array',
table: { type: { summary: 'CommentModel' } }
},
readOnly: {
control: 'boolean',
description: 'Displays input area to add new comment',
defaultValue: false,
table: {
type: { summary: 'boolean' },
defaultValue: { summary: 'false'}
}
},
nodeId: {
control: 'text',
description: 'Necessary in order to add a new Node comment',
defaultValue: undefined,
table: {
type: { summary: 'string' },
defaultValue: { summary: 'undefined' }
},
if: { arg: 'taskId', exists: false }
},
taskId: {
control: 'text',
description: 'Necessary in order to add a new Task comment',
defaultValue: undefined,
table: {
type: { summary: 'string' },
defaultValue: { summary: 'undefined' }
},
if: { arg: 'nodeId', exists: false }
}
}
} as Meta;
const template: Story<CommentsComponent> = (args: CommentsComponent) => ({
props: args
});
export const singleCommentWithAvatar = template.bind({});
singleCommentWithAvatar.args = {
comments: [commentsNodeData[0]],
readOnly: true
};
export const singleCommentWithoutAvatar = template.bind({});
singleCommentWithoutAvatar.args = {
comments: [commentsTaskData[1]],
readOnly: true
};
export const noComments = template.bind({});
noComments.args = {
comments: [],
readOnly: true
};
export const nodeComments = template.bind({});
nodeComments.args = {
comments: commentsNodeData,
nodeId: '-fake-'
};
export const taskComments = template.bind({});
taskComments.args = {
comments: commentsTaskData,
taskId: '-fake-'
};

View File

@@ -0,0 +1,191 @@
/*!
* @license
* Copyright 2019 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, ViewEncapsulation } from '@angular/core';
import { Observable, Observer } from 'rxjs';
import { share } from 'rxjs/operators';
@Component({
selector: 'adf-comments',
templateUrl: './comments.component.html',
styleUrls: ['./comments.component.scss'],
encapsulation: ViewEncapsulation.None
})
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)
.pipe(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(
(comments: CommentModel[]) => {
if (comments && comments instanceof Array) {
comments = comments.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;
});
comments.forEach((currentComment) => {
this.commentObserver.next(currentComment);
});
}
},
(err) => {
this.error.emit(err);
}
);
}
if (this.isANode()) {
this.commentContentService.getNodeComments(this.nodeId).subscribe(
(comments: CommentModel[]) => {
if (comments && comments instanceof Array) {
comments = comments.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;
});
comments.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) {
const comment = this.sanitize(this.message);
this.beingAdded = true;
if (this.isATask()) {
this.commentProcessService.addTaskComment(this.taskId, comment)
.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, comment)
.subscribe(
(res: CommentModel) => {
this.comments.unshift(res);
this.message = '';
this.beingAdded = false;
},
(err) => {
this.error.emit(err);
this.beingAdded = false;
}
);
}
}
}
clear(event: Event): void {
event.stopPropagation();
this.message = '';
}
isReadOnly(): boolean {
return this.readOnly;
}
isATask(): boolean {
return !!this.taskId;
}
isANode(): boolean {
return !!this.nodeId;
}
private sanitize(input: string): string {
return input.replace(/<[^>]+>/g, '')
.replace(/^\s+|\s+$|\s+(?=\s)/g, '')
.replace(/\r?\n/g, '<br/>');
}
}

View File

@@ -0,0 +1,54 @@
/*!
* @license
* Copyright 2019 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 { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { MatLineModule } from '@angular/material/core';
import { FormsModule } from '@angular/forms';
import { PipeModule } from '../pipes/pipe.module';
import { CommentListComponent } from './comment-list.component';
import { CommentsComponent } from './comments.component';
@NgModule({
imports: [
PipeModule,
FormsModule,
CommonModule,
TranslateModule,
MatButtonModule,
MatFormFieldModule,
MatInputModule,
MatListModule,
MatLineModule
],
declarations: [
CommentListComponent,
CommentsComponent
],
exports: [
CommentListComponent,
CommentsComponent
]
})
export class CommentsModule {
}

View File

@@ -0,0 +1,18 @@
/*!
* @license
* Copyright 2019 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,21 @@
/*!
* @license
* Copyright 2019 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';
export * from './comments.module';