[AAE-10768] Make the comments reusable (#7983)

* [AAE-10768] Refactor core comments

* [AAE-10768] Add models export in core comments refactor

* [AAE-10768] Add comments implementation inside process-services package

* [AAE-10768] Add task comments module to process module

* [AAE-10768] Add node comments module to content services

* [AAE-10768] Add id check to getComments and addComments in adf-core comments component

* [AAE-10768] Remove unused service files in process-comments module

* [AAE-10768] Remove unused service files in process-comments module

* [AAE-10768] Add testing logic to spec files

* [AAE-10768] Add comments components readme

* [AAE-10768] Add a mock service to inject into the comment stories file

* [AAE-10768] Add mock data for comments stories

* [AAE-10768] Add mock service to inject into comments stories

* [AAE-10768] Rename mock service and mock data

* [AAE-10768] change taskId with id into the comments test because taskId is never used

* [AAE-10768] Resolve pr suggestions

* [AAE-10768] Resolve task-comments pr suggestions

* [AAE-10768] Resolve comments pr suggestions

* [AAE-10768] Fix merge error in comments.component.html

* [AAE-10768] Add missing markdown files

* [AAE-10768] Remove events from md files

* [AAE-10768] Update upgrade50-60.md with renamed input property

Co-authored-by: Amedeo Lepore <amedeo.lepore@hyland.com>
This commit is contained in:
Diogo Bastos 2022-12-20 10:51:54 +00:00 committed by GitHub
parent 9077572199
commit 3864aaf9cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1667 additions and 356 deletions

View File

@ -4,7 +4,11 @@
<adf-info-drawer [title]="'APP.INFO_DRAWER.TITLE' | translate">
<adf-info-drawer-tab label="APP.INFO_DRAWER.COMMENTS">
<adf-comments [nodeId]="nodeId" [readOnly]="!isCommentEnabled"></adf-comments>
<adf-node-comments
[nodeId]="nodeId"
[readOnly]="!isCommentEnabled"
>
</adf-node-comments>
</adf-info-drawer-tab>
<adf-info-drawer-tab label="APP.INFO_DRAWER.PROPERTIES">

View File

@ -19,7 +19,7 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
import { CoreModule, InfoDrawerModule } from '@alfresco/adf-core';
import { ContentDirectiveModule, VersionManagerModule, ContentMetadataModule } from '@alfresco/adf-content-services';
import { ContentModule, ContentDirectiveModule, VersionManagerModule, ContentMetadataModule } from '@alfresco/adf-content-services';
import { FileViewComponent } from './file-view.component';
const routes: Routes = [
@ -35,6 +35,7 @@ const routes: Routes = [
RouterModule.forChild(routes),
CoreModule,
InfoDrawerModule,
ContentModule,
ContentDirectiveModule,
ContentMetadataModule,
VersionManagerModule

View File

@ -0,0 +1,29 @@
---
Title: Node Comments Component
Added: v5.1.0
Status: Active
---
# [Node Comments Component](../../../lib/content-services/src/lib/node-comments/node-comments.component.ts "Defined in node-comments.component.ts")
Displays comments from users involved in a specified content and allows an involved user to add a comment to a content.
![adf-comments](../../docassets/images/adf-comments.png)
## Basic Usage Task
```html
<adf-node-comments
[nodeId]="YOUR_NODE_ID"
[readOnly]="YOUR_READ_ONLY_FLAG">
</adf-node-comments>
```
## Class members
### Properties
| Name | Type | Default value | Description |
| ---- | ---- | ------------- | ----------- |
| nodeId | `string` | | The numeric ID of the node. |
| readOnly | `boolean` | | Are the comments read only? |

View File

@ -0,0 +1,28 @@
---
Title: Node Comments Service
Added: v6.0.0
Status: Active
Last reviewed: 2022-12-19
---
# [Node Comments service](../../../lib/content-services/src/lib/node-comments/services/node-comments.service.ts "Defined in node-comments.service.ts")
Adds and retrieves comments for nodes in Content Services.
## Class members
### Methods
- **add**(id: `string`, message: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`CommentModel`](../../../lib/core/src/lib/models/comment.model.ts)`>`<br/>
Adds a comment to a task.
- _id:_ `string` - ID of the target task
- _message:_ `string` - Text for the comment
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`CommentModel`](../../../lib/core/src/lib/models/comment.model.ts)`>` - Details about the comment
- **get**(id: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`CommentModel`](../../../lib/core/src/lib/models/comment.model.ts)`[]>`<br/>
Gets all comments that have been added to a task.
- _id:_ `string` - ID of the target task
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`CommentModel`](../../../lib/core/src/lib/models/comment.model.ts)`[]>` - Details for each comment
## See also
- [Node comments component](../../../lib/content-services/src/lib/node-comments/node-comments.component.ts)

View File

@ -0,0 +1,28 @@
---
Title: Task Comments service
Added: v6.0.0
Status: Active
Last reviewed: 2022-12-19
---
# [Task Comments service](../../../lib/process-services/src/lib/task-comments/services/task-comments.service.ts "Defined in task-comments.service.ts")
Adds and retrieves comments for task and process instances in Process Services.
## Class members
### Methods
- **get**(id: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`CommentModel`](../../../lib/core/models/comment.model.ts)`[]>`<br/>
Gets all comments that have been added to a task.
- _id:_ `string` - ID of the target task
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`CommentModel`](../../../lib/core/models/comment.model.ts)`[]>` - Details for each comment
- **add**(id: `string`, message: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`CommentModel`](../../../lib/core/models/comment.model.ts)`>`<br/>
Adds a comment to a process instance.
- _id:_ `string` - ID of the target task
- _message:_ `string` - Text for the comment
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`CommentModel`](../../../lib/core/models/comment.model.ts)`>` - Details of the comment added
## See also
- [Task comments component](../../../lib/process-services/src/lib/task-comments/task-comments.component.ts)

View File

@ -4,9 +4,9 @@ Added: v2.0.0
Status: Active
---
# [Comments Component](lib/core/src/lib/comments/comments.component.ts "Defined in comments.component.ts")
# [Comments Component](../../../lib/core/src/lib/comments/comments.component.ts "Defined in comments.component.ts")
Displays comments from users involved in a specified task or content and allows an involved user to add a comment to a task or a content.
Displays comments from users involved in a specified environment and allows an involved user to add a comment to a environment.
![adf-comments](../../docassets/images/adf-comments.png)
@ -14,16 +14,7 @@ Displays comments from users involved in a specified task or content and allows
```html
<adf-comments
[taskId]="YOUR_TASK_ID"
[readOnly]="YOUR_READ_ONLY_FLAG">
</adf-comments>
```
## Basic Usage Content
```html
<adf-comments
[nodeId]="YOUR_NODE_ID"
[id]="YOUR_ID"
[readOnly]="YOUR_READ_ONLY_FLAG">
</adf-comments>
```
@ -34,9 +25,8 @@ Displays comments from users involved in a specified task or content and allows
| Name | Type | Default value | Description |
| ---- | ---- | ------------- | ----------- |
| nodeId | `string` | | The numeric ID of the node. |
| id | `string` | | The numeric ID of the entity. |
| readOnly | `boolean` | false | Are the comments read only? |
| taskId | `string` | | The numeric ID of the task. |
### Events

View File

@ -0,0 +1,29 @@
---
Title: Task Comments Component
Added: v5.1.0
Status: Active
---
# [Task Comments Component](../../../lib/process-services/src/lib/task-comments/task-comments.component.ts "Defined in task-comments.component.ts")
Displays comments from users involved in a specified task and allows an involved user to add a comment to a task.
![adf-comments](../../docassets/images/adf-comments.png)
## Basic Usage Task
```html
<adf-task-comments
[taskId]="YOUR_TASK_ID"
[readOnly]="YOUR_READ_ONLY_FLAG">
</adf-task-comments>
```
## Class members
### Properties
| Name | Type | Default value | Description |
| ---- | ---- | ------------- | ----------- |
| taskId | `string` | | The numeric ID of the task. |
| readOnly | `boolean` | | Are the comments read only? |

View File

@ -126,7 +126,7 @@ v6.0.0 and after:
### Properties and methods
- `<adf-comments>`: The `taskId` input has now been renamed as `id`
### Component selectors

View File

@ -45,6 +45,7 @@ import { VersionCompatibilityModule } from './version-compatibility/version-comp
import { versionCompatibilityFactory } from './version-compatibility/version-compatibility-factory';
import { VersionCompatibilityService } from './version-compatibility/version-compatibility.service';
import { ContentPipeModule } from './pipes/content-pipe.module';
import { NodeCommentsModule } from './node-comments/node-comments.module';
@NgModule({
imports: [
@ -73,7 +74,8 @@ import { ContentPipeModule } from './pipes/content-pipe.module';
TreeViewModule,
ContentTypeModule,
AspectListModule,
VersionCompatibilityModule
VersionCompatibilityModule,
NodeCommentsModule
],
providers: [
{
@ -106,7 +108,8 @@ import { ContentPipeModule } from './pipes/content-pipe.module';
TreeViewModule,
AspectListModule,
ContentTypeModule,
VersionCompatibilityModule
VersionCompatibilityModule,
NodeCommentsModule
]
})
export class ContentModule {

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,209 @@
/*!
* @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 { CommentModel, EcmCompanyModel, EcmUserModel } from '@alfresco/adf-core';
export const fakeUser1 = {
enabled: true,
firstName: 'firstName',
lastName: 'lastName',
email: 'fake-email@dom.com',
emailNotificationsEnabled: true,
company: {},
id: 'fake-email@dom.com',
avatarId: '123-123-123'
};
export const fakeUser2 = {
enabled: true,
firstName: 'some',
lastName: 'one',
email: 'some-one@somegroup.com',
emailNotificationsEnabled: true,
company: {},
id: 'fake-email@dom.com',
avatarId: '001-001-001'
};
export const fakeContentComments = {
list: {
pagination: {
count: 4,
hasMoreItems: false,
totalItems: 4,
skipCount: 0,
maxItems: 100
},
entries: [{
entry: {
createdAt: '2018-03-27T10:55:45.725+0000',
createdBy: fakeUser1,
edited: false,
modifiedAt: '2018-03-27T10:55:45.725+0000',
canEdit: true,
modifiedBy: fakeUser1,
canDelete: true,
id: '35a0cea7-b6d0-4abc-9030-f4e461dd1ac7',
content: 'fake-message-1'
}
}, {
entry: {
createdAt: '2018-03-27T10:55:45.725+0000',
createdBy: fakeUser2,
edited: false,
modifiedAt: '2018-03-27T10:55:45.725+0000',
canEdit: true,
modifiedBy: fakeUser2,
canDelete: true,
id: '35a0cea7-b6d0-4abc-9030-f4e461dd1ac7',
content: 'fake-message-2'
}
}
]
}
};
export const fakeContentComment = {
entry: {
createdAt: '2018-03-29T11:49:51.735+0000',
createdBy: fakeUser1,
edited: false,
modifiedAt: '2018-03-29T11:49:51.735+0000',
canEdit: true,
modifiedBy: fakeUser1,
canDelete: true,
id: '4d07cdc5-f00c-4391-b39d-a842b12478b2',
content: 'fake-comment-message'
}
};
const fakeCompany: EcmCompanyModel = {
organization: '',
address1: '',
address2: '',
address3: '',
postcode: '',
telephone: '',
fax: '',
email: ''
};
const johnDoe: EcmUserModel = {
id: '1',
email: 'john.doe@alfresco.com',
firstName: 'John',
lastName: 'Doe',
company: fakeCompany,
enabled: true,
isAdmin: undefined,
avatarId: '001'
};
const janeEod: EcmUserModel = {
id: '2',
email: 'jane.eod@alfresco.com',
firstName: 'Jane',
lastName: 'Eod',
company: fakeCompany,
enabled: true,
isAdmin: undefined
};
const robertSmith: EcmUserModel = {
id: '3',
email: 'robert.smith@alfresco.com',
firstName: 'Robert',
lastName: 'Smith',
company: fakeCompany,
enabled: true,
isAdmin: undefined
};
export const testUser: EcmUserModel = {
id: '44',
email: 'test.user@hyland.com',
firstName: 'Test',
lastName: 'User',
company: fakeCompany,
enabled: true,
isAdmin: undefined,
avatarId: '044'
};
export const getDateXMinutesAgo = (minutes: number) => new Date(new Date().getTime() - minutes * 60000);
export const commentsNodeData: CommentModel[] = [
{
id: 1,
message: `I've done this component, is it cool?`,
created: getDateXMinutesAgo(30),
createdBy: johnDoe,
isSelected: false
},
{
id: 2,
message: 'Yeah',
created: getDateXMinutesAgo(15),
createdBy: janeEod,
isSelected: false
},
{
id: 3,
message: '+1',
created: getDateXMinutesAgo(12),
createdBy: robertSmith,
isSelected: false
},
{
id: 4,
message: 'ty',
created: new Date(),
createdBy: johnDoe,
isSelected: false
}
];
export const commentsTaskData: CommentModel[] = [
{
id: 1,
message: `I've done this task, what's next?`,
created: getDateXMinutesAgo(30),
createdBy: johnDoe,
isSelected: false
},
{
id: 2,
message: `I've assigned you another one 🤠`,
created: getDateXMinutesAgo(15),
createdBy: janeEod,
isSelected: false
},
{
id: 3,
message: '+1',
created: getDateXMinutesAgo(12),
createdBy: robertSmith,
isSelected: false
},
{
id: 4,
message: 'Cheers',
created: new Date(),
createdBy: johnDoe,
isSelected: false
}
];

View File

@ -0,0 +1,5 @@
<adf-comments
[readOnly]="readOnly"
[id]="nodeId"
>
</adf-comments>

View File

@ -0,0 +1,31 @@
/*!
* @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, Input, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'adf-node-comments',
templateUrl: './node-comments.component.html',
encapsulation: ViewEncapsulation.None
})
export class NodeCommentsComponent {
@Input()
nodeId: string;
@Input()
readOnly: boolean;
}

View File

@ -0,0 +1,38 @@
/*!
* @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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NodeCommentsComponent } from './node-comments.component';
import { ADF_COMMENTS_SERVICE, CoreModule } from '@alfresco/adf-core';
import { NodeCommentsService } from './services/node-comments.service';
@NgModule({
imports: [
CommonModule,
CoreModule
],
declarations: [NodeCommentsComponent],
exports: [NodeCommentsComponent],
providers: [
{
provide: ADF_COMMENTS_SERVICE,
useClass: NodeCommentsService
}
]
})
export class NodeCommentsModule {}

View File

@ -0,0 +1,22 @@
/*!
* @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 './node-comments.component';
export * from './services/node-comments.service';
export * from './node-comments.module';

View File

@ -0,0 +1,88 @@
/*!
* @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 { TestBed } from '@angular/core/testing';
import { CommentModel, setupTestBed, CoreTestingModule } from '@alfresco/adf-core';
import { fakeContentComment, fakeContentComments } from '../mocks/node-comments.mock';
import { TranslateModule } from '@ngx-translate/core';
import { NodeCommentsService } from './node-comments.service';
declare let jasmine: any;
describe('NodeCommentsService', () => {
let service: NodeCommentsService;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
service = TestBed.inject(NodeCommentsService);
jasmine.Ajax.install();
});
afterEach(() => {
jasmine.Ajax.uninstall();
});
describe('Node comments', () => {
it('should add a comment node ', (done) => {
service.add('999', 'fake-comment-message').subscribe(
(res: CommentModel) => {
expect(res).toBeDefined();
expect(res.id).not.toEqual(null);
expect(res.message).toEqual('fake-comment-message');
expect(res.created).not.toEqual(null);
expect(res.createdBy.email).toEqual('fake-email@dom.com');
expect(res.createdBy.firstName).toEqual('firstName');
expect(res.createdBy.lastName).toEqual('lastName');
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeContentComment)
});
});
it('should return the nodes comments ', (done) => {
service.get('999').subscribe(
(res: CommentModel[]) => {
expect(res).toBeDefined();
expect(res.length).toEqual(2);
expect(res[0].message).toEqual('fake-message-1');
expect(res[1].message).toEqual('fake-message-2');
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeContentComments)
});
});
});
});

View File

@ -0,0 +1,102 @@
/*!
* @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 { AlfrescoApiService, LogService, CommentModel } from '@alfresco/adf-core';
import { CommentEntry, CommentsApi, Comment } from '@alfresco/js-api';
import { Injectable } from '@angular/core';
import { Observable, from, throwError } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class NodeCommentsService {
private _commentsApi: CommentsApi;
get commentsApi(): CommentsApi {
this._commentsApi = this._commentsApi ?? new CommentsApi(this.apiService.getInstance());
return this._commentsApi;
}
constructor(
private apiService: AlfrescoApiService,
private logService: LogService
) {}
/**
* Gets all comments that have been added to a task.
*
* @param id ID of the target task
* @returns Details for each comment
*/
get(id: string): Observable<CommentModel[]> {
return from(this.commentsApi.listComments(id))
.pipe(
map((response) => {
const comments: CommentModel[] = [];
response.list.entries.forEach((comment: CommentEntry) => {
this.addToComments(comments, comment);
});
return comments;
}),
catchError(
(err: any) => this.handleError(err)
)
);
}
/**
* Adds a comment to a task.
*
* @param id ID of the target task
* @param message Text for the comment
* @returns Details about the comment
*/
add(id: string, message: string): Observable<CommentModel> {
return from(this.commentsApi.createComment(id, { content: message }))
.pipe(
map(
(response: CommentEntry) => this.newCommentModel(response.entry)
),
catchError(
(err: any) => this.handleError(err)
)
);
}
private addToComments(comments: CommentModel[], comment: CommentEntry): void {
const newComment: Comment = comment.entry;
comments.push(this.newCommentModel(newComment));
}
private newCommentModel(comment: Comment): CommentModel {
return new CommentModel({
id: comment.id,
message: comment.content,
created: comment.createdAt,
createdBy: comment.createdBy
});
}
private handleError(error: any) {
this.logService.error(error);
return throwError(error || 'Server error');
}
}

View File

@ -35,6 +35,7 @@ export * from './lib/tree-view/index';
export * from './lib/group/index';
export * from './lib/aspect-list/index';
export * from './lib/content-type/index';
export * from './lib/node-comments/index';
export * from './lib/new-version-uploader';
export * from './lib/interfaces/index';
export * from './lib/version-compatibility/index';

View File

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

View File

@ -17,100 +17,74 @@
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';
import { CommentsServiceMock, commentsResponseMock } from './mocks/comments.service.mock';
import { ADF_COMMENTS_SERVICE, CommentsService } from './interfaces';
import { of, throwError } from 'rxjs';
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;
let getCommentSpy: jasmine.Spy;
let addCommentSpy: jasmine.Spy;
let commentsService: CommentsService;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
schemas: [CUSTOM_ELEMENTS_SCHEMA],
providers: [
{
provide: ADF_COMMENTS_SERVICE,
useClass: CommentsServiceMock
}
]
});
beforeEach(() => {
fixture = TestBed.createComponent(CommentsComponent);
component = fixture.componentInstance;
commentProcessService = fixture.debugElement.injector.get(CommentProcessService);
commentContentService = fixture.debugElement.injector.get(CommentContentService);
commentsService = fixture.componentInstance['commentsService'];
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'}
})));
getCommentSpy = spyOn(commentsService, 'get').and.returnValue(commentsResponseMock.getComments());
addCommentSpy = spyOn(commentsService, 'add').and.returnValue(commentsResponseMock.addComment());
});
afterEach(() => {
fixture.destroy();
});
it('should load comments when taskId specified', () => {
it('should load comments when id specified', () => {
const change = new SimpleChange(null, '123', true);
component.ngOnChanges({taskId: change});
component.ngOnChanges({id: 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();
expect(getCommentSpy).toHaveBeenCalled();
});
it('should emit an error when an error occurs loading comments', () => {
const emitSpy = spyOn(component.error, 'emit');
getProcessCommentsSpy.and.returnValue(throwError({}));
getCommentSpy.and.returnValue(throwError({}));
const change = new SimpleChange(null, '123', true);
component.ngOnChanges({taskId: change});
component.ngOnChanges({id: change});
expect(emitSpy).toHaveBeenCalled();
});
it('should not load comments when no taskId is specified', () => {
it('should not load comments when no id is specified', () => {
fixture.detectChanges();
expect(getProcessCommentsSpy).not.toHaveBeenCalled();
expect(getCommentSpy).not.toHaveBeenCalled();
});
it('should display comments when the task has comments', async () => {
it('should display comments when the entity has comments', async () => {
const change = new SimpleChange(null, '123', true);
component.ngOnChanges({taskId: change});
component.ngOnChanges({id: change});
fixture.detectChanges();
await fixture.whenStable();
@ -119,9 +93,9 @@ describe('CommentsComponent', () => {
expect(fixture.nativeElement.querySelector('.adf-comment-message:empty')).toBeNull();
});
it('should display comments count when the task has comments', async () => {
it('should display comments count when the entity has comments', async () => {
const change = new SimpleChange(null, '123', true);
component.ngOnChanges({taskId: change});
component.ngOnChanges({id: change});
fixture.detectChanges();
await fixture.whenStable();
@ -130,9 +104,9 @@ describe('CommentsComponent', () => {
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([]));
it('should not display comments when the entity has no comments', async () => {
component.id = '123';
getCommentSpy.and.returnValue(of([]));
fixture.detectChanges();
await fixture.whenStable();
@ -142,7 +116,7 @@ describe('CommentsComponent', () => {
it('should display comments input by default', async () => {
const change = new SimpleChange(null, '123', true);
component.ngOnChanges({taskId: change});
component.ngOnChanges({id: change});
fixture.detectChanges();
await fixture.whenStable();
@ -150,7 +124,7 @@ describe('CommentsComponent', () => {
expect(fixture.nativeElement.querySelector('#comment-input')).not.toBeNull();
});
it('should not display comments input when the task is readonly', async () => {
it('should not display comments input when the entity is readonly', async () => {
component.readOnly = true;
fixture.detectChanges();
@ -159,60 +133,35 @@ describe('CommentsComponent', () => {
expect(fixture.nativeElement.querySelector('#comment-input')).toBeNull();
});
describe('change detection taskId', () => {
describe('Change detection id', () => {
const change = new SimpleChange('123', '456', true);
const nullChange = new SimpleChange('123', null, true);
beforeEach(() => {
component.taskId = '123';
component.id = '123';
fixture.detectChanges();
});
it('should fetch new comments when taskId changed', () => {
component.ngOnChanges({taskId: change});
expect(getProcessCommentsSpy).toHaveBeenCalledWith('456');
it('should fetch new comments when id changed', () => {
component.ngOnChanges({id: change});
expect(getCommentSpy).toHaveBeenCalledWith('456');
});
it('should not fetch new comments when empty changeset made', () => {
component.ngOnChanges({});
expect(getProcessCommentsSpy).not.toHaveBeenCalled();
expect(getCommentSpy).not.toHaveBeenCalled();
});
it('should not fetch new comments when taskId changed to null', () => {
component.ngOnChanges({taskId: nullChange});
expect(getProcessCommentsSpy).not.toHaveBeenCalled();
it('should not fetch new comments when id changed to null', () => {
component.ngOnChanges({id: nullChange});
expect(getCommentSpy).not.toHaveBeenCalled();
});
});
describe('change detection node', () => {
const change = new SimpleChange('123', '456', true);
const nullChange = new SimpleChange('123', null, true);
describe('Add comment', () => {
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';
component.id = '123';
fixture.detectChanges();
fixture.whenStable();
});
@ -225,18 +174,18 @@ describe('CommentsComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
expect(addProcessCommentSpy).toHaveBeenCalledWith('123', 'action');
expect(addCommentSpy).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';
component.message = 'test comment';
element.dispatchEvent(new Event('click'));
fixture.detectChanges();
await fixture.whenStable();
expect(addProcessCommentSpy).toHaveBeenCalledWith('123', 'test comment');
expect(addCommentSpy).toHaveBeenCalledWith('123', 'test comment');
});
it('should add break lines to comment when user input contains new line characters', async () => {
@ -247,18 +196,21 @@ describe('CommentsComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
expect(addProcessCommentSpy).toHaveBeenCalledWith('123', 'these<br/>are<br/>paragraphs');
expect(addCommentSpy).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';
addCommentSpy.and.returnValue(commentsResponseMock.addComment(component.message));
element.dispatchEvent(new Event('click'));
fixture.detectChanges();
await fixture.whenStable();
expect(addProcessCommentSpy).toHaveBeenCalled();
expect(addCommentSpy).toHaveBeenCalled();
const elements = fixture.nativeElement.querySelectorAll('.adf-comment-message');
expect(elements.length).toBe(1);
expect(elements[0].innerText).toBe('Test Comment');
@ -272,7 +224,7 @@ describe('CommentsComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
expect(addProcessCommentSpy).not.toHaveBeenCalled();
expect(addCommentSpy).not.toHaveBeenCalled();
});
it('should clear comment when escape key is pressed', async () => {
@ -289,97 +241,34 @@ describe('CommentsComponent', () => {
it('should emit an error when an error occurs adding the comment', () => {
const emitSpy = spyOn(component.error, 'emit');
addProcessCommentSpy.and.returnValue(throwError({}));
addCommentSpy.and.returnValue(throwError({}));
component.message = 'Test comment';
component.add();
component.addComment();
expect(emitSpy).toHaveBeenCalled();
});
});
describe('Add comment node', () => {
beforeEach(() => {
component.nodeId = '123';
fixture.detectChanges();
fixture.whenStable();
it('should set beingAdded variable back to false when an error occurs adding the comment', () => {
addCommentSpy.and.returnValue(throwError({}));
component.addComment();
expect(component.beingAdded).toBeFalse();
});
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('.adf-comment-message');
expect(elements.length).toBe(1);
expect(elements[0].innerText).toBe('Test Comment');
it('should set beingAdded variable back to false on successful response when adding the comment', () => {
addCommentSpy.and.returnValue(commentsResponseMock.addComment());
component.addComment();
expect(component.beingAdded).toBeFalse();
});
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 not add comment if id is not provided', () => {
component.id = '';
component.addComment();
expect(addCommentSpy).not.toHaveBeenCalled();
});
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');
it('should not add comment if message is empty', () => {
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();
component.addComment();
expect(addCommentSpy).not.toHaveBeenCalled();
});
});
});

View File

@ -16,13 +16,13 @@
*/
import { Meta, moduleMetadata, Story } from '@storybook/angular';
import { CommentContentService, CommentProcessService, EcmUserService } from '../services';
import { EcmUserService } from '../services';
import { CoreStoryModule } from '../testing/core.story.module';
import { CommentsComponent } from './comments.component';
import { CommentsModule } from './comments.module';
import { CommentContentServiceMock } from '../mock/comment-content-service.mock';
import { CommentProcessServiceMock } from '../mock/comment-process-service.mock';
import { commentsTaskData, commentsNodeData } from '../mock/comment-content.mock';
import { ADF_COMMENTS_SERVICE } from './interfaces/comments.token';
import { commentsStoriesData } from './mocks/comments.stories.mock';
import { CommentsServiceStoriesMock } from './mocks/comments.service.stories.mock';
export default {
component: CommentsComponent,
@ -31,17 +31,16 @@ export default {
moduleMetadata({
imports: [CoreStoryModule, CommentsModule],
providers: [
{ provide: CommentContentService, useClass: CommentContentServiceMock },
{ provide: CommentProcessService, useClass: CommentProcessServiceMock },
{ provide: EcmUserService, useValue: { getUserProfileImage: () => '../assets/images/logo.png' } }
{ provide: EcmUserService, useValue: { getUserProfileImage: () => '../assets/images/logo.png' } },
{ provide: ADF_COMMENTS_SERVICE, useClass: CommentsServiceStoriesMock }
]
})
],
parameters: {
docs: {
description: {
component: `Displays comments from users involved in a specified task or node.
Allows an involved user to add a comment to a task or a node.`
component: `Displays comments from users involved in a specified environment.
Allows an involved user to add a comment to a environment.`
}
}
},
@ -60,21 +59,12 @@ export default {
defaultValue: { summary: 'false' }
}
},
nodeId: {
id: {
control: 'text',
description: 'Necessary in order to add a new Node comment',
description: 'Necessary in order to add a new comment',
table: {
type: { summary: 'string' }
},
if: { arg: 'taskId', exists: false }
},
taskId: {
control: 'text',
description: 'Necessary in order to add a new Task comment',
table: {
type: { summary: 'string' }
},
if: { arg: 'nodeId', exists: false }
}
},
error: {
action: 'error',
@ -93,13 +83,13 @@ const template: Story<CommentsComponent> = (args: CommentsComponent) => ({
export const singleCommentWithAvatar = template.bind({});
singleCommentWithAvatar.args = {
comments: [commentsNodeData[0]],
comments: [commentsStoriesData[0]],
readOnly: true
};
export const singleCommentWithoutAvatar = template.bind({});
singleCommentWithoutAvatar.args = {
comments: [commentsTaskData[1]],
comments: [commentsStoriesData[1]],
readOnly: true
};
@ -109,14 +99,9 @@ noComments.args = {
readOnly: true
};
export const nodeComments = template.bind({});
nodeComments.args = {
comments: commentsNodeData,
nodeId: '-fake-'
export const comments = template.bind({});
comments.args = {
comments: commentsStoriesData,
id: '-fake-'
};
export const taskComments = template.bind({});
taskComments.args = {
comments: commentsTaskData,
taskId: '-fake-'
};

View File

@ -15,12 +15,21 @@
* 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 {
Component,
EventEmitter,
Inject,
Input,
OnChanges,
Output,
SimpleChanges,
ViewEncapsulation
} from '@angular/core';
import { Observable, Observer } from 'rxjs';
import { share } from 'rxjs/operators';
import { ADF_COMMENTS_SERVICE } from './interfaces/comments.token';
import { CommentsService } from './interfaces/comments-service.interface';
@Component({
selector: 'adf-comments',
@ -32,11 +41,7 @@ export class CommentsComponent implements OnChanges {
/** The numeric ID of the task. */
@Input()
taskId: string;
/** The numeric ID of the node. */
@Input()
nodeId: string;
id: string;
/** Are the comments read only? */
@Input()
@ -46,143 +51,129 @@ export class CommentsComponent implements OnChanges {
@Output()
error: EventEmitter<any> = new EventEmitter<any>();
comments: CommentModel [] = [];
private commentObserver: Observer<CommentModel>;
comment$: Observable<CommentModel>;
comments: CommentModel[] = [];
message: string;
beingAdded: boolean = false;
constructor(private commentProcessService: CommentProcessService,
private commentContentService: CommentContentService) {
private commentObserver: Observer<CommentModel>;
comment$: Observable<CommentModel>;
constructor(@Inject(ADF_COMMENTS_SERVICE) private commentsService: CommentsService) {
this.comment$ = new Observable<CommentModel>((observer) => this.commentObserver = observer)
.pipe(share());
.pipe(
share()
);
this.comment$.subscribe((comment: CommentModel) => {
this.comments.push(comment);
});
}
ngOnChanges(changes: SimpleChanges) {
this.taskId = null;
this.nodeId = null;
ngOnChanges(changes: SimpleChanges): void {
this.id = null;
this.taskId = changes['taskId'] ? changes['taskId'].currentValue : null;
this.nodeId = changes['nodeId'] ? changes['nodeId'].currentValue : null;
this.id = changes['id'] ? changes['id'].currentValue : null;
if (this.taskId || this.nodeId) {
this.getComments();
if (this.id) {
this.loadComments();
} else {
this.resetComments();
}
}
private getComments(): void {
loadComments() {
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);
});
}
if (!this.hasId()) {
return;
}
this.commentsService.get(this.id).subscribe(
(comments: CommentModel[]) => {
if (!this.isArrayInstance(comments)) {
return;
}
comments = this.sortedComments(comments);
this.addCommentsToObserver(comments);
},
(err) => {
this.error.emit(err);
}
);
}
addComment() {
if (!this.canAddComment()) {
return;
}
const comment: string = this.sanitize(this.message);
this.beingAdded = true;
this.commentsService.add(this.id, comment)
.subscribe(
(res: CommentModel) => {
this.addToComments(res);
this.resetMessage();
},
(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);
() => {
this.beingAdded = false;
}
);
}
}
clearMessage(event: Event): void {
event.stopPropagation();
this.resetMessage();
}
private addToComments(comment: CommentModel): void {
this.comments.unshift(comment);
}
private resetMessage(): void {
this.message = '';
}
private canAddComment(): boolean {
return this.hasId() && this.message && this.message.trim() && !this.beingAdded;
}
private hasId(): boolean {
return !!this.id;
}
private isArrayInstance(entity: any): boolean {
return entity && entity instanceof Array;
}
private sortedComments(comments: CommentModel[]): CommentModel[] {
return 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;
});
}
private addCommentsToObserver(comments: CommentModel[]): void {
comments.forEach((currentComment: CommentModel) => {
this.commentObserver.next(currentComment);
});
}
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, '')

View File

@ -0,0 +1,24 @@
/*!
* @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 { Observable } from 'rxjs';
import { CommentModel } from '../../models/comment.model';
export interface CommentsService {
get(id: string): Observable<CommentModel[]>;
add(id: string, message: string): Observable<CommentModel>;
}

View File

@ -0,0 +1,20 @@
/*!
* @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 { InjectionToken } from '@angular/core';
export const ADF_COMMENTS_SERVICE = new InjectionToken('ADF_COMMENTS_SERVICE');

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,19 @@
/*!
* @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 './comments-service.interface';
export * from './comments.token';

View File

@ -0,0 +1,145 @@
/*!
* @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 { CommentModel, EcmUserModel } from '../../models';
import { Observable, of } from 'rxjs';
import { CommentsService } from '../interfaces';
export class CommentsServiceMock implements CommentsService {
constructor() {}
get(_id: string): Observable<CommentModel[]> {
return commentsResponseMock.getComments();
}
add(_id: string): Observable<CommentModel> {
return commentsResponseMock.addComment();
}
}
export const commentsResponseMock = {
getComments: () => of([
{
id: 1,
message: 'Test Comment',
created: new Date(),
createdBy: {
enabled: true,
firstName: 'hruser',
displayName: 'hruser',
quota: -1,
quotaUsed: 12,
emailNotificationsEnabled: true,
company: {
organization: 'test',
address1: 'test',
address2: 'test',
address3: 'test',
postcode: 'test',
telephone: 'test',
fax: 'test',
email: 'test'
},
id: 'hruser',
email: 'test',
isAdmin: () => false
} as EcmUserModel,
isSelected: false
} as CommentModel,
{
id: 2,
message: 'Test Comment',
created: new Date(),
createdBy: {
enabled: true,
firstName: 'hruser',
displayName: 'hruser',
quota: -1,
quotaUsed: 12,
emailNotificationsEnabled: true,
company: {
organization: 'test',
address1: 'test',
address2: 'test',
address3: 'test',
postcode: 'test',
telephone: 'test',
fax: 'test',
email: 'test'
},
id: 'hruser',
email: 'test',
isAdmin: () => false
} as EcmUserModel,
isSelected: false
} as CommentModel,
{
id: 3,
message: 'Test Comment',
created: new Date(),
createdBy: {
enabled: true,
firstName: 'hruser',
displayName: 'hruser',
quota: -1,
quotaUsed: 12,
emailNotificationsEnabled: true,
company: {
organization: 'test',
address1: 'test',
address2: 'test',
address3: 'test',
postcode: 'test',
telephone: 'test',
fax: 'test',
email: 'test'
},
id: 'hruser',
email: 'test',
isAdmin: () => false
} as EcmUserModel,
isSelected: false
} as CommentModel
]),
addComment: (message = 'test comment') => of({
id: 1,
message,
created: new Date(),
createdBy: {
enabled: true,
firstName: 'hruser',
displayName: 'hruser',
quota: -1,
quotaUsed: 12,
emailNotificationsEnabled: true,
company: {
organization: 'test',
address1: 'test',
address2: 'test',
address3: 'test',
postcode: 'test',
telephone: 'test',
fax: 'test',
email: 'test'
},
id: 'hruser',
email: 'test',
isAdmin: () => false
} as EcmUserModel,
isSelected: false
} as CommentModel)
};

View File

@ -0,0 +1,126 @@
/*!
* @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 { CommentModel, EcmUserModel } from '../../models';
import { Observable, of } from 'rxjs';
import { CommentsService } from '../interfaces';
import { testUser } from './comments.stories.mock';
export class CommentsServiceStoriesMock implements CommentsService {
constructor() {}
get(_id: string): Observable<CommentModel[]> {
return commentsResponseMock.getComments();
}
add(_id: string, message = 'test comment'): Observable<CommentModel> {
return commentsResponseMock.addComment(message);
}
}
export const commentsResponseMock = {
getComments: () => of([
{
id: 1,
message: 'Test Comment',
created: new Date(),
createdBy: {
enabled: true,
firstName: 'hruser',
displayName: 'hruser',
quota: -1,
quotaUsed: 12,
emailNotificationsEnabled: true,
company: {
organization: 'test',
address1: 'test',
address2: 'test',
address3: 'test',
postcode: 'test',
telephone: 'test',
fax: 'test',
email: 'test'
},
id: 'hruser',
email: 'test',
isAdmin: () => false
} as EcmUserModel,
isSelected: false
} as CommentModel,
{
id: 2,
message: 'Test Comment',
created: new Date(),
createdBy: {
enabled: true,
firstName: 'hruser',
displayName: 'hruser',
quota: -1,
quotaUsed: 12,
emailNotificationsEnabled: true,
company: {
organization: 'test',
address1: 'test',
address2: 'test',
address3: 'test',
postcode: 'test',
telephone: 'test',
fax: 'test',
email: 'test'
},
id: 'hruser',
email: 'test',
isAdmin: () => false
} as EcmUserModel,
isSelected: false
} as CommentModel,
{
id: 3,
message: 'Test Comment',
created: new Date(),
createdBy: {
enabled: true,
firstName: 'hruser',
displayName: 'hruser',
quota: -1,
quotaUsed: 12,
emailNotificationsEnabled: true,
company: {
organization: 'test',
address1: 'test',
address2: 'test',
address3: 'test',
postcode: 'test',
telephone: 'test',
fax: 'test',
email: 'test'
},
id: 'hruser',
email: 'test',
isAdmin: () => false
} as EcmUserModel,
isSelected: false
} as CommentModel
]),
addComment: (message: string) => of({
id: 1,
message,
created: new Date(),
createdBy: testUser,
isSelected: false
} as CommentModel)
};

View File

@ -0,0 +1,105 @@
/*!
* @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 { CommentModel, EcmCompanyModel, EcmUserModel } from '../../models';
const fakeCompany: EcmCompanyModel = {
organization: '',
address1: '',
address2: '',
address3: '',
postcode: '',
telephone: '',
fax: '',
email: ''
};
export const getDateXMinutesAgo = (minutes: number) => new Date(new Date().getTime() - minutes * 60000);
const johnDoe: EcmUserModel = {
id: '1',
email: 'john.doe@alfresco.com',
firstName: 'John',
lastName: 'Doe',
company: fakeCompany,
enabled: true,
isAdmin: undefined,
avatarId: '001'
};
const janeEod: EcmUserModel = {
id: '2',
email: 'jane.eod@alfresco.com',
firstName: 'Jane',
lastName: 'Eod',
company: fakeCompany,
enabled: true,
isAdmin: undefined
};
const robertSmith: EcmUserModel = {
id: '3',
email: 'robert.smith@alfresco.com',
firstName: 'Robert',
lastName: 'Smith',
company: fakeCompany,
enabled: true,
isAdmin: undefined
};
export const testUser: EcmUserModel = {
id: '44',
email: 'test.user@hyland.com',
firstName: 'Test',
lastName: 'User',
company: fakeCompany,
enabled: true,
isAdmin: undefined,
avatarId: '044'
};
export const commentsStoriesData: CommentModel[] = [
{
id: 1,
message: `I've done this task, what's next?`,
created: getDateXMinutesAgo(30),
createdBy: johnDoe,
isSelected: false
},
{
id: 2,
message: `I've assigned you another one 🤠`,
created: getDateXMinutesAgo(15),
createdBy: janeEod,
isSelected: false
},
{
id: 3,
message: '+1',
created: getDateXMinutesAgo(12),
createdBy: robertSmith,
isSelected: false
},
{
id: 4,
message: 'Cheers',
created: new Date(),
createdBy: johnDoe,
isSelected: false
}
];

View File

@ -18,4 +18,6 @@
export * from './comment-list.component';
export * from './comments.component';
export * from './interfaces/index';
export * from './comments.module';

View File

@ -38,3 +38,4 @@ export * from './node-metadata.model';
export * from './application-access.model';
export * from './user-access.model';
export * from './general-user.model';
export * from './comment.model';

View File

@ -31,6 +31,7 @@ import { PeopleModule } from './people/people.module';
import { FormModule } from './form/form.module';
import { ProcessFormRenderingService } from './form/process-form-rendering.service';
import { ProcessServicesPipeModule } from './pipes/process-services-pipe.module';
import { TaskCommentsModule } from './task-comments/task-comments.module';
@NgModule({
imports: [
@ -42,6 +43,7 @@ import { ProcessServicesPipeModule } from './pipes/process-services-pipe.module'
MaterialModule,
ProcessListModule,
TaskListModule,
TaskCommentsModule,
AppsListModule,
AttachmentModule,
PeopleModule,
@ -65,6 +67,7 @@ import { ProcessServicesPipeModule } from './pipes/process-services-pipe.module'
ReactiveFormsModule,
ProcessListModule,
TaskListModule,
TaskCommentsModule,
AppsListModule,
AttachmentModule,
PeopleModule,

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,32 @@
/*!
* @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 const fakeUser1 = { id: 1, email: 'fake-email@dom.com', firstName: 'firstName', lastName: 'lastName' };
export const fakeUser2 = { id: 1001, email: 'some-one@somegroup.com', firstName: 'some', lastName: 'one' };
export const fakeTasksComment = {
size: 2, total: 2, start: 0,
data: [
{
id: 1, message: 'fake-message-1', created: '', createdBy: fakeUser1
},
{
id: 2, message: 'fake-message-2', created: '', createdBy: fakeUser1
}
]
};

View File

@ -0,0 +1,22 @@
/*!
* @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 './task-comments.component';
export * from './services/task-comments.service';
export * from './task-comments.module';

View File

@ -0,0 +1,94 @@
/*!
* @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 { TestBed } from '@angular/core/testing';
import { CommentModel, setupTestBed, CoreTestingModule } from '@alfresco/adf-core';
import { fakeTasksComment, fakeUser1 } from '../mocks/task-comments.mock';
import { TranslateModule } from '@ngx-translate/core';
import { TaskCommentsService } from './task-comments.service';
declare let jasmine: any;
describe('TaskCommentsService', () => {
let service: TaskCommentsService;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
service = TestBed.inject(TaskCommentsService);
});
beforeEach(() => {
jasmine.Ajax.install();
});
afterEach(() => {
jasmine.Ajax.uninstall();
});
describe('Task comments', () => {
it('should add a comment task ', (done) => {
service.add('999', 'fake-comment-message').subscribe(
(res: CommentModel) => {
expect(res).toBeDefined();
expect(res.id).not.toEqual(null);
expect(res.message).toEqual('fake-comment-message');
expect(res.created).not.toEqual(null);
expect(res.createdBy.email).toEqual('fake-email@dom.com');
expect(res.createdBy.firstName).toEqual('firstName');
expect(res.createdBy.lastName).toEqual('lastName');
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify({
id: '111', message: 'fake-comment-message',
createdBy: fakeUser1,
created: '2016-07-15T11:19:17.440+0000'
})
});
});
it('should return the tasks comments ', (done) => {
service.get('999').subscribe(
(res: CommentModel[]) => {
expect(res).toBeDefined();
expect(res.length).toEqual(2);
expect(res[0].message).toEqual('fake-message-1');
expect(res[1].message).toEqual('fake-message-2');
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeTasksComment)
});
});
});
});

View File

@ -0,0 +1,107 @@
/*!
* @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 { AlfrescoApiService, CommentModel, CommentsService, UserProcessModel } from '@alfresco/adf-core';
import { ActivitiCommentsApi, CommentRepresentation } from '@alfresco/js-api';
import { Injectable } from '@angular/core';
import { from, Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class TaskCommentsService implements CommentsService {
private _commentsApi: ActivitiCommentsApi;
get commentsApi(): ActivitiCommentsApi {
this._commentsApi = this._commentsApi ?? new ActivitiCommentsApi(this.apiService.getInstance());
return this._commentsApi;
}
constructor(
private apiService: AlfrescoApiService
) {}
/**
* Gets all comments that have been added to a task.
*
* @param id ID of the target task
* @returns Details for each comment
*/
get(id: string): Observable<CommentModel[]> {
return from(this.commentsApi.getTaskComments(id))
.pipe(
map((response) => {
const comments: CommentModel[] = [];
response.data.forEach((comment: CommentRepresentation) => {
this.addToComments(comments, comment);
});
return comments;
}),
catchError(
(err: any) => this.handleError(err)
)
);
}
/**
* Adds a comment to a task.
*
* @param id ID of the target task
* @param message Text for the comment
* @returns Details about the comment
*/
add(id: string, message: string): Observable<CommentModel> {
return from(this.commentsApi.addTaskComment({ message }, id))
.pipe(
map(
(response: CommentRepresentation) => this.newCommentModel(response)
),
catchError(
(err: any) => this.handleError(err)
)
);
}
private addToComments(comments: CommentModel[], comment: CommentRepresentation): void {
const user = new UserProcessModel(comment.createdBy);
const newComment: CommentRepresentation = {
id: comment.id,
message: comment.message,
created: comment.created,
createdBy: user
};
comments.push(this.newCommentModel(newComment));
}
private newCommentModel(representation: CommentRepresentation): CommentModel {
return new CommentModel({
id: representation.id,
message: representation.message,
created: representation.created,
createdBy: representation.createdBy
});
}
private handleError(error: any) {
return throwError(error || 'Server error');
}
}

View File

@ -0,0 +1,5 @@
<adf-comments
[readOnly]="readOnly"
[id]="taskId"
>
</adf-comments>

View File

@ -0,0 +1,31 @@
/*!
* @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, Input, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'adf-task-comments',
templateUrl: './task-comments.component.html',
encapsulation: ViewEncapsulation.None
})
export class TaskCommentsComponent {
@Input()
taskId: string;
@Input()
readOnly: boolean;
}

View File

@ -0,0 +1,38 @@
/*!
* @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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TaskCommentsComponent } from './task-comments.component';
import { TaskCommentsService } from './services/task-comments.service';
import { ADF_COMMENTS_SERVICE, CoreModule } from '@alfresco/adf-core';
@NgModule({
imports: [
CommonModule,
CoreModule
],
declarations: [TaskCommentsComponent],
exports: [TaskCommentsComponent],
providers: [
{
provide: ADF_COMMENTS_SERVICE,
useClass: TaskCommentsService
}
]
})
export class TaskCommentsModule {}

View File

@ -82,10 +82,12 @@
<adf-info-drawer-tab label="ADF_TASK_LIST.DETAILS.LABELS.INFO_DRAWER_TAB_ACTIVITY_TITLE">
<mat-card *ngIf="showComments">
<mat-card-content>
<adf-comments #activitiComments
[readOnly]="isReadOnlyComment()"
[taskId]="taskDetails.id">
</adf-comments>
<adf-task-comments
#activitiComments
[readOnly]="isReadOnlyComment()"
[taskId]="taskDetails.id"
>
</adf-task-comments>
</mat-card-content>
</mat-card>
</adf-info-drawer-tab>

View File

@ -40,6 +40,7 @@ import { AttachFormComponent } from './components/attach-form.component';
import { FormModule } from '../form/form.module';
import { ClaimTaskDirective } from './components/task-form/claim-task.directive';
import { UnclaimTaskDirective } from './components/task-form/unclaim-task.directive';
import { TaskCommentsModule } from '../task-comments/task-comments.module';
@NgModule({
imports: [
@ -52,7 +53,8 @@ import { UnclaimTaskDirective } from './components/task-form/unclaim-task.direct
CoreModule,
PeopleModule,
ProcessCommentsModule,
ContentWidgetModule
ContentWidgetModule,
TaskCommentsModule
],
declarations: [
NoTaskDetailsTemplateDirective,

View File

@ -23,6 +23,7 @@ export * from './lib/process-comments/index';
export * from './lib/people/index';
export * from './lib/content-widget/index';
export * from './lib/form/index';
export * from './lib/task-comments/index';
export * from './lib/pipes/process-name.pipe';
export * from './lib/pipes/process-services-pipe.module';