mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
👽 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:
29
lib/core/src/lib/comments/comment-list.component.html
Normal file
29
lib/core/src/lib/comments/comment-list.component.html
Normal 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>
|
95
lib/core/src/lib/comments/comment-list.component.scss
Normal file
95
lib/core/src/lib/comments/comment-list.component.scss
Normal 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;
|
||||
}
|
||||
}
|
299
lib/core/src/lib/comments/comment-list.component.spec.ts
Normal file
299
lib/core/src/lib/comments/comment-list.component.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
64
lib/core/src/lib/comments/comment-list.component.stories.ts
Normal file
64
lib/core/src/lib/comments/comment-list.component.stories.ts
Normal 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
|
||||
};
|
101
lib/core/src/lib/comments/comment-list.component.ts
Normal file
101
lib/core/src/lib/comments/comment-list.component.ts
Normal 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;
|
||||
}
|
||||
}
|
26
lib/core/src/lib/comments/comments.component.html
Normal file
26
lib/core/src/lib/comments/comments.component.html
Normal 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>
|
39
lib/core/src/lib/comments/comments.component.scss
Normal file
39
lib/core/src/lib/comments/comments.component.scss
Normal 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%;
|
||||
}
|
385
lib/core/src/lib/comments/comments.component.spec.ts
Normal file
385
lib/core/src/lib/comments/comments.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
111
lib/core/src/lib/comments/comments.component.stories.ts
Normal file
111
lib/core/src/lib/comments/comments.component.stories.ts
Normal 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-'
|
||||
};
|
191
lib/core/src/lib/comments/comments.component.ts
Normal file
191
lib/core/src/lib/comments/comments.component.ts
Normal 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/>');
|
||||
}
|
||||
}
|
54
lib/core/src/lib/comments/comments.module.ts
Normal file
54
lib/core/src/lib/comments/comments.module.ts
Normal 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 {
|
||||
}
|
18
lib/core/src/lib/comments/index.ts
Normal file
18
lib/core/src/lib/comments/index.ts
Normal 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';
|
21
lib/core/src/lib/comments/public-api.ts
Normal file
21
lib/core/src/lib/comments/public-api.ts
Normal 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';
|
Reference in New Issue
Block a user