diff --git a/src/components/comments/index.test.ts b/src/components/comments/index.test.ts index c6ea51a3..09364a2c 100644 --- a/src/components/comments/index.test.ts +++ b/src/components/comments/index.test.ts @@ -24,14 +24,12 @@ describe('CommentsComponent', () => { realtime: ABLY_REALTIME_MOCK, localParticipant: MOCK_LOCAL_PARTICIPANT, group: MOCK_GROUP, - config: { - ...MOCK_CONFIG, - apiUrl: 'https://dev.nodeapi.superviz.com', - }, + config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, }); commentsComponent['element'].addAnnotation = jest.fn().mockImplementation(() => []); + commentsComponent['element'].addComment = jest.fn().mockImplementation(() => []); }); afterEach(() => { @@ -72,20 +70,20 @@ describe('CommentsComponent', () => { }); it('should call apiService when fetch annotation', async () => { - const result = jest.spyOn(ApiService, 'fetchAnnotation'); + const spy = jest.spyOn(ApiService, 'fetchAnnotation'); - expect(result).toHaveBeenCalledWith( - expect.any(String), - expect.any(String), + expect(spy).toHaveBeenCalledWith( + MOCK_CONFIG.apiUrl, + MOCK_CONFIG.apiKey, { - roomId: expect.any(String), + roomId: MOCK_CONFIG.roomId, url: expect.any(String), }, ); }); it('should call apiService when create annotation', async () => { - const result = jest.spyOn(ApiService, 'createAnnotations'); + const spy = jest.spyOn(ApiService, 'createAnnotations'); commentsComponent['element'].dispatchEvent(new CustomEvent('create-annotation', { detail: { @@ -97,11 +95,11 @@ describe('CommentsComponent', () => { }, })); - expect(result).toHaveBeenCalledWith( - expect.any(String), - expect.any(String), + expect(spy).toHaveBeenCalledWith( + MOCK_CONFIG.apiUrl, + MOCK_CONFIG.apiKey, { - roomId: expect.any(String), + roomId: MOCK_CONFIG.roomId, url: expect.any(String), userId: expect.any(String), position: expect.any(String), @@ -110,7 +108,23 @@ describe('CommentsComponent', () => { }); it('should call apiService when resolve annotation', async () => { - const result = jest.spyOn(ApiService, 'resolveAnnotation'); + const spy = jest.spyOn(ApiService, 'resolveAnnotation'); + + commentsComponent['element'].dispatchEvent(new CustomEvent('resolve-annotation', { + detail: { + uuid: 'test', + }, + })); + + expect(spy).toHaveBeenCalledWith( + MOCK_CONFIG.apiUrl, + MOCK_CONFIG.apiKey, + 'test', + ); + }); + + it('should call apiService when resolve annotation', async () => { + const spy = jest.spyOn(ApiService, 'resolveAnnotation'); commentsComponent['element'].dispatchEvent(new CustomEvent('resolve-annotation', { detail: { @@ -118,10 +132,31 @@ describe('CommentsComponent', () => { }, })); - expect(result).toHaveBeenCalledWith( - expect.any(String), - expect.any(String), + expect(spy).toHaveBeenCalledWith( + MOCK_CONFIG.apiUrl, + MOCK_CONFIG.apiKey, 'test', ); }); + + it('should call apiService when create a new comment', async () => { + const spy = jest.spyOn(ApiService, 'createComment'); + + commentsComponent['element'].dispatchEvent(new CustomEvent('create-comment', { + detail: { + uuid: 'uuid-test', + text: 'text-test', + }, + })); + + expect(spy).toHaveBeenCalledWith( + MOCK_CONFIG.apiUrl, + MOCK_CONFIG.apiKey, + { + annotationId: 'uuid-test', + userId: expect.any(String), + text: 'text-test', + }, + ); + }); }); diff --git a/src/components/comments/index.ts b/src/components/comments/index.ts index a54ec120..c3276d46 100644 --- a/src/components/comments/index.ts +++ b/src/components/comments/index.ts @@ -55,6 +55,7 @@ export class CommentsComponent extends BaseComponent { private addListeners(): void { this.element.addEventListener('create-annotation', this.createAnnotation); this.element.addEventListener('resolve-annotation', this.resolveAnnotation); + this.element.addEventListener('create-comment', ({ detail }: CustomEvent) => this.createComment(detail.uuid, detail.text, true)); } /** @@ -65,17 +66,18 @@ export class CommentsComponent extends BaseComponent { private destroyListeners(): void { this.element.removeEventListener('create-annotation', this.createAnnotation); this.element.removeEventListener('resolve-annotation', this.createAnnotation); + this.element.removeEventListener('create-comment', ({ detail }: CustomEvent) => this.createComment(detail.uuid, detail.text, true)); } /** * @function createAnnotation * @description Creates a new annotation and comment and adds them to the Comments component - * @param {CustomEvent} e - The event object containing the annotation text and position + * @param {CustomEvent} event - The event object containing the annotation text and position * @returns {Promise} */ - private createAnnotation = async (e: CustomEvent): Promise => { + private createAnnotation = async ({ detail }: CustomEvent): Promise => { try { - const { text, position } = e.detail; + const { text, position } = detail; const { url } = this; const annotation: Annotation = await ApiService.createAnnotations(config.get('apiUrl'), config.get('apiKey'), { @@ -104,13 +106,21 @@ export class CommentsComponent extends BaseComponent { * @param {string} text - The text content of the comment * @returns {Promise} - A promise that resolves with the created comment object */ - private async createComment(annotationId: string, text: string): Promise { + private async createComment( + annotationId: string, + text: string, + addComment = false, + ): Promise { try { - return await ApiService.createComment(config.get('apiUrl'), config.get('apiKey'), { + const comment: Comment = await ApiService.createComment(config.get('apiUrl'), config.get('apiKey'), { annotationId, userId: this.localParticipant.id, text, }); + + if (addComment) this.addComment(annotationId, comment); + + return comment; } catch (error) { this.logger.log('error when creating comment', error); throw error; @@ -123,10 +133,21 @@ export class CommentsComponent extends BaseComponent { * @param {Annotation[]} annotation - An array of annotation objects to add to the component * @returns {void} */ - private addAnnotation(annotation: Annotation[]): void { + addAnnotation(annotation: Annotation[]): void { this.element.addAnnotation(annotation); } + /** + * @function addComment + * @description Adds a new comment to an annotation in the Comments component + * @param {string} annotationId - The ID of the annotation to add the comment to + * @param {Comment} comment - The comment object to add to the annotation + * @returns {void} + */ + addComment(annotationId: string, comment: Comment): void { + this.element.addComment(annotationId, comment); + } + /** * @function fetchAnnotations * @description Fetches annotations from the API and adds them to the Comments component @@ -157,9 +178,9 @@ export class CommentsComponent extends BaseComponent { * @param {CustomEvent} e - The custom event containing the UUID of the annotation to resolve * @returns {Promise} */ - private async resolveAnnotation(e: CustomEvent): Promise { + private async resolveAnnotation({ detail }: CustomEvent): Promise { try { - const { uuid } = e.detail; + const { uuid } = detail; await ApiService.resolveAnnotation( config.get('apiUrl'), config.get('apiKey'), diff --git a/src/web-components/comments/comments.ts b/src/web-components/comments/comments.ts index 7f71237d..bfa6deca 100644 --- a/src/web-components/comments/comments.ts +++ b/src/web-components/comments/comments.ts @@ -1,7 +1,7 @@ import { CSSResultGroup, LitElement, html } from 'lit'; import { customElement } from 'lit/decorators.js'; -import { Annotation } from '../../components/comments/types'; +import { Annotation, Comment } from '../../components/comments/types'; import { WebComponentsBase } from '../base'; import { commentsStyle } from './css'; @@ -33,6 +33,26 @@ export class Comments extends WebComponentsBaseElement { ]; } + addComment(annotationId: string, comment: Comment) { + const annotationIndex = this.annotations + .findIndex((annotation) => annotation.uuid === annotationId); + + if (annotationIndex === -1) return; + + const annotation = this.annotations[annotationIndex]; + + annotation.comments = [ + ...annotation.comments, + comment, + ]; + + this.annotations = [ + ...this.annotations.slice(0, annotationIndex), + annotation, + ...this.annotations.slice(annotationIndex + 1), + ]; + } + updateAnnotations(data: Annotation[]) { this.annotations = data; } diff --git a/src/web-components/comments/components/annotation-item.ts b/src/web-components/comments/components/annotation-item.ts index 562fcffd..10591e1f 100644 --- a/src/web-components/comments/components/annotation-item.ts +++ b/src/web-components/comments/components/annotation-item.ts @@ -45,6 +45,15 @@ export class CommentsAnnotationItem extends WebComponentsBaseElement { this.emitEvent('select-annotation', { uuid }); }; + private createComment({ detail }: CustomEvent) { + const { text } = detail; + + this.emitEvent('create-comment', { + uuid: this.annotation.uuid, + text, + }); + } + protected render() { const replies = this.annotation.comments.length; @@ -84,13 +93,13 @@ export class CommentsAnnotationItem extends WebComponentsBaseElement { `; }; - const resolveAnnotation = (e: CustomEvent) => { + const resolveAnnotation = ({ detail }: CustomEvent) => { const { uuid } = this.annotation; - const { resolved } = e.detail; + const { resolved } = detail; this.emitEvent('resolve-annotation', { uuid, - resolved: resolved === 'true', + resolved, }); this.options.resolved = resolved; @@ -119,6 +128,7 @@ export class CommentsAnnotationItem extends WebComponentsBaseElement {
${this.annotation.comments.map(expandedComments)}
diff --git a/src/web-components/comments/components/annotations.ts b/src/web-components/comments/components/annotations.ts index f4b2a812..cda74aa6 100644 --- a/src/web-components/comments/components/annotations.ts +++ b/src/web-components/comments/components/annotations.ts @@ -17,10 +17,10 @@ export class CommentsAnnotations extends WebComponentsBaseElement { open: { type: Boolean }, }; - private createAnnotation(e: CustomEvent) { + private createAnnotation({ detail }: CustomEvent) { this.emitEvent('create-annotation', { position: {}, - text: e.detail.text, + text: detail.text, }); } @@ -29,8 +29,8 @@ export class CommentsAnnotations extends WebComponentsBaseElement {
Click anywhere to add a comment
diff --git a/src/web-components/comments/components/comment-input.ts b/src/web-components/comments/components/comment-input.ts index 23cf5e83..fefe8300 100644 --- a/src/web-components/comments/components/comment-input.ts +++ b/src/web-components/comments/components/comment-input.ts @@ -70,7 +70,14 @@ export class CommentsCommentInput extends WebComponentsBaseElement { const sendBtn = this.getSendBtn(); const text = input.value; - this.emitEvent(this.eventType, { text }); + this.emitEvent( + this.eventType, + { text }, + { + composed: false, + bubbles: false, + }, + ); input.value = ''; sendBtn.disabled = true; diff --git a/src/web-components/comments/components/comment-item.ts b/src/web-components/comments/components/comment-item.ts index fbc30d87..63b4ab4a 100644 --- a/src/web-components/comments/components/comment-item.ts +++ b/src/web-components/comments/components/comment-item.ts @@ -1,4 +1,4 @@ -import { CSSResultGroup, LitElement, html } from 'lit'; +import { CSSResultGroup, LitElement, PropertyValueMap, html } from 'lit'; import { customElement } from 'lit/decorators.js'; import { DateTime } from 'luxon'; @@ -12,13 +12,17 @@ const styles: CSSResultGroup[] = [WebComponentsBaseElement.styles, commentItemSt @customElement('superviz-comments-comment-item') export class CommentsCommentItem extends WebComponentsBaseElement { + constructor() { + super(); + this.resolved = false; + } + static styles = styles; declare avatar: string; declare username: string; declare text: string; - declare resolved: string; - declare resolvable: boolean; + declare resolved: boolean; declare createdAt: string; declare options: AnnotationOptions; @@ -26,12 +30,17 @@ export class CommentsCommentItem extends WebComponentsBaseElement { avatar: { type: String }, username: { type: String }, text: { type: String }, - resolved: { type: String }, - resolvable: { type: Boolean }, + resolved: { type: Boolean }, createdAt: { type: String }, options: { type: Object }, }; + updated = (changedProperties: PropertyValueMap) => { + if (changedProperties.has('options')) { + this.resolved = this.options?.resolved; + } + }; + protected render() { const humanizeDate = (date: string) => { return DateTime.fromISO(date).toFormat('yyyy-dd-MM'); @@ -39,11 +48,13 @@ export class CommentsCommentItem extends WebComponentsBaseElement { const isResolvable = this.options?.resolvable ? 'comment-item__resolve' : 'hidden'; - const iconResolve = this.options?.resolved ? 'resolved' : 'unresolved'; + const iconResolve = this.resolved ? 'resolved' : 'unresolved'; const resolveAnnotation = () => { + this.resolved = !this.resolved; + this.emitEvent('resolve-annotation', { - resolved: !this.options.resolved, + resolved: this.resolved, }, { composed: false, bubbles: false }); }; diff --git a/src/web-components/comments/components/content.ts b/src/web-components/comments/components/content.ts index 1da0ce8c..3ac94ec4 100644 --- a/src/web-components/comments/components/content.ts +++ b/src/web-components/comments/components/content.ts @@ -30,8 +30,8 @@ export class CommentsContent extends WebComponentsBaseElement { return this.annotations.length === index + 1 ? 'hidden' : ''; }; - const selectAnnotation = (e: CustomEvent) => { - const { uuid } = e.detail; + const selectAnnotation = ({ detail }: CustomEvent) => { + const { uuid } = detail; this.selectedAnnotation = uuid; }; diff --git a/src/web-components/comments/tests/comments.test.ts b/src/web-components/comments/tests/comments.test.ts index ef0683f8..cdeea1ab 100644 --- a/src/web-components/comments/tests/comments.test.ts +++ b/src/web-components/comments/tests/comments.test.ts @@ -91,4 +91,48 @@ describe('comments', () => { expect(element['annotations']).toEqual([annotationUpdated]); }); + + test('should add comment', async () => { + const annotation = { + ...MOCK_ANNOTATION, + position: 'any_position', + }; + + element['addAnnotation']([annotation]); + + const comment = { + uuid: 'teste', + username: 'any_username', + avatar: 'any_avatar', + text: 'any_text', + createdAt: new Date().toISOString(), + }; + + element['addComment'](annotation.uuid, comment); + + const lastComment = element['annotations'][0].comments.at(-1); + + expect(lastComment).toEqual(comment); + }); + + test('should return void when annotation is not found', async () => { + const annotation = { + ...MOCK_ANNOTATION, + position: 'any_position', + }; + + element['addAnnotation']([annotation]); + + const comment = { + uuid: 'teste', + username: 'any_username', + avatar: 'any_avatar', + text: 'any_text', + createdAt: new Date().toISOString(), + }; + + element['addComment']('other_annotation_id', comment); + + expect(element['annotations'][0].comments.length).toEqual(2); + }); }); diff --git a/src/web-components/comments/tests/components/annotation-item.test.ts b/src/web-components/comments/tests/components/annotation-item.test.ts index 994363fd..79be2ac4 100644 --- a/src/web-components/comments/tests/components/annotation-item.test.ts +++ b/src/web-components/comments/tests/components/annotation-item.test.ts @@ -55,11 +55,11 @@ describe('CommentsAnnotationItem', () => { element = await createElement(MOCK_ANNOTATION); const commentItem = element.shadowRoot!.querySelector('superviz-comments-comment-item'); - commentItem!.dispatchEvent(new CustomEvent('resolve-annotation', { detail: { resolved: 'true' } })); + commentItem!.dispatchEvent(new CustomEvent('resolve-annotation', { detail: { resolved: true } })); await sleep(); - expect(element['options']['resolved']).toBe('true'); + expect(element['options']['resolved']).toBe(true); }); test('resolves the annotation when the comment is resolved', async () => { @@ -70,10 +70,33 @@ describe('CommentsAnnotationItem', () => { expect(element['options']['resolved']).toBe(true); const commentItem = element.shadowRoot!.querySelector('superviz-comments-comment-item'); - commentItem!.dispatchEvent(new CustomEvent('resolve-annotation', { detail: { resolved: 'false' } })); + commentItem!.dispatchEvent(new CustomEvent('resolve-annotation', { detail: { resolved: false } })); + + await sleep(); + + expect(element['options']['resolved']).toBe(false); + }); + + test('should create a new comment in annotation', async () => { + element = await createElement(MOCK_ANNOTATION); + element['dispatchEvent'] = jest.fn(); + const commentInput = element.shadowRoot!.querySelector('superviz-comments-comment-input') as HTMLElement; + + commentInput!.dispatchEvent(new CustomEvent('create-comment', { + detail: { + text: 'new comment', + }, + })); await sleep(); - expect(element['options']['resolved']).toBe('false'); + expect(element['dispatchEvent']).toHaveBeenCalledWith( + new CustomEvent('create-comment', { + detail: { + text: 'new comment', + uuid: MOCK_ANNOTATION.uuid, + }, + }), + ); }); }); diff --git a/src/web-components/comments/tests/components/annotations.test.ts b/src/web-components/comments/tests/components/annotations.test.ts index 1a0e0917..1dcf88a6 100644 --- a/src/web-components/comments/tests/components/annotations.test.ts +++ b/src/web-components/comments/tests/components/annotations.test.ts @@ -29,7 +29,7 @@ describe('CommentsAnnotations', () => { const commentInput = element.shadowRoot!.querySelector('superviz-comments-comment-input') as HTMLElement; const eventSpy = jest.fn(); element.addEventListener('create-annotation', eventSpy); - commentInput.dispatchEvent(new CustomEvent('create-comment', { + commentInput.dispatchEvent(new CustomEvent('create-annotation', { detail: { text: 'test', }, diff --git a/src/web-components/comments/tests/components/comment-item.test.ts b/src/web-components/comments/tests/components/comment-item.test.ts index a8dbe071..da8716a6 100644 --- a/src/web-components/comments/tests/components/comment-item.test.ts +++ b/src/web-components/comments/tests/components/comment-item.test.ts @@ -49,5 +49,6 @@ describe('CommentsCommentItem', () => { { detail: { resolved: true } }, ), ); + expect(element['resolved']).toEqual(true); }); });