From 09022e18901d9ac20d0c05f3b2c0729d5a1fd6cd Mon Sep 17 00:00:00 2001 From: Peter Chapman Date: Mon, 20 Jan 2025 12:30:24 +1300 Subject: [PATCH] SF-3083 Resolve icon should change when editing resolved note --- .../biblical-terms.component.spec.ts | 50 ++++++++++++++++++- .../biblical-terms.component.ts | 19 +++++-- .../translate/editor/editor.component.spec.ts | 48 ++++++++++++++++++ .../app/translate/editor/editor.component.ts | 8 ++- .../note-dialog/note-dialog.component.spec.ts | 23 ++++++++- .../note-dialog/note-dialog.component.ts | 1 + .../src/assets/i18n/non_checking_en.json | 1 + 7 files changed, 141 insertions(+), 9 deletions(-) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/biblical-terms/biblical-terms.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/biblical-terms/biblical-terms.component.spec.ts index c3c675f08c..3cdcad0c3f 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/biblical-terms/biblical-terms.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/biblical-terms/biblical-terms.component.spec.ts @@ -367,6 +367,7 @@ describe('BiblicalTermsComponent', () => { env.setupProjectData('en'); env.wait(); + // SUT env.biblicalTermsNotesButton.click(); env.wait(); env.mockNoteDialogRef.close({ status: NoteStatus.Resolved }); @@ -374,8 +375,55 @@ describe('BiblicalTermsComponent', () => { verify(mockedMatDialog.open(NoteDialogComponent, anything())).once(); const noteThread: NoteThread = env.getNoteThreadDoc(projectId, 'threadId01').data!; - expect(noteThread.status).toEqual(NoteStatus.Resolved); + expect(noteThread.status).toBe(NoteStatus.Resolved); expect(noteThread.notes[1].content).toBeUndefined(); + expect(noteThread.notes[1].status).toBe(NoteStatus.Resolved); + })); + + it('can resolve and edit a note for a biblical term', fakeAsync(async () => { + const projectId = 'project01'; + const newContent = 'Updated Note Content'; + const env = new TestEnvironment(projectId, 1, 1); + env.setupProjectData('en'); + env.wait(); + + // Make the note editable + const noteThreadDoc: NoteThreadDoc = env.getNoteThreadDoc(projectId, 'threadId01'); + await noteThreadDoc.submitJson0Op(op => op.set(nt => nt.notes[0].editable, true)); + + // SUT + env.biblicalTermsNotesButton.click(); + env.wait(); + env.mockNoteDialogRef.close({ status: NoteStatus.Resolved, noteContent: newContent, noteDataId: 'note01' }); + env.wait(); + + verify(mockedMatDialog.open(NoteDialogComponent, anything())).once(); + const noteThread: NoteThread = env.getNoteThreadDoc(projectId, 'threadId01').data!; + expect(noteThread.status).toBe(NoteStatus.Resolved); + expect(noteThread.notes[0].content).toBe(newContent); + expect(noteThread.notes[0].status).toBe(NoteStatus.Resolved); + })); + + it('cannot resolve a non-editable note for a biblical term', fakeAsync(() => { + const projectId = 'project01'; + const env = new TestEnvironment(projectId, 1, 1); + env.setupProjectData('en'); + env.wait(); + + // Stub the error message display + const dialogMessage = spyOn((env.component as any).dialogService, 'message').and.stub(); + + // SUT + env.biblicalTermsNotesButton.click(); + env.wait(); + env.mockNoteDialogRef.close({ status: NoteStatus.Resolved, noteDataId: 'note01' }); + env.wait(); + + verify(mockedMatDialog.open(NoteDialogComponent, anything())).once(); + const noteThread: NoteThread = env.getNoteThreadDoc(projectId, 'threadId01').data!; + expect(noteThread.status).toEqual(NoteStatus.Todo); + expect(noteThread.notes.length).toBe(1); + expect(dialogMessage).toHaveBeenCalledTimes(1); })); it('should show the not found message if no messages were found', fakeAsync(() => { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/biblical-terms/biblical-terms.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/biblical-terms/biblical-terms.component.ts index 8c2a98556f..076c0b39f8 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/biblical-terms/biblical-terms.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/biblical-terms/biblical-terms.component.ts @@ -550,6 +550,7 @@ export class BiblicalTermsComponent extends DataLoadingComponent implements OnDe const currentDate: string = new Date().toJSON(); const threadId = `BT_${biblicalTermDoc.data.termId}`; const noteContent: string | undefined = params.content == null ? undefined : XmlUtils.encodeForXml(params.content); + const noteStatus: NoteStatus = params.status ?? NoteStatus.Todo; // Configure the note const note: Note = { dateCreated: currentDate, @@ -561,7 +562,7 @@ export class BiblicalTermsComponent extends DataLoadingComponent implements OnDe content: noteContent, conflictType: NoteConflictType.DefaultValue, type: NoteType.Normal, - status: params.status ?? NoteStatus.Todo, + status: noteStatus, deleted: false, editable: true, versionNumber: 1 @@ -597,10 +598,18 @@ export class BiblicalTermsComponent extends DataLoadingComponent implements OnDe ); const noteIndex: number = threadDoc.data!.notes.findIndex(n => n.dataId === params.dataId); if (noteIndex >= 0) { - await threadDoc!.submitJson0Op(op => { - op.set(t => t.notes[noteIndex].content, noteContent); - op.set(t => t.notes[noteIndex].dateModified, currentDate); - }); + // updated the existing note + if (threadDoc.data?.notes[noteIndex].editable === true) { + await threadDoc!.submitJson0Op(op => { + op.set(t => t.notes[noteIndex].content, noteContent); + op.set(t => t.notes[noteIndex].dateModified, currentDate); + op.set(t => t.notes[noteIndex].status, noteStatus); + // also set the status of the thread to be the status of the note + op.set(t => t.status, noteStatus); + }); + } else { + this.dialogService.message('biblical_terms.cannot_edit_note_paratext'); + } } else { note.threadId = threadDoc.data!.threadId; await threadDoc.submitJson0Op(op => { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.spec.ts index 6d8ca1e31c..28bf3f86fd 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.spec.ts @@ -3025,6 +3025,54 @@ describe('EditorComponent', () => { env.dispose(); })); + it('allows editing and resolving a note', fakeAsync(async () => { + const projectId: string = 'project01'; + const threadDataId: string = 'dataid01'; + const content: string = 'This thread is resolved.'; + const env = new TestEnvironment(); + let noteThread: NoteThreadDoc = env.getNoteThreadDoc(projectId, threadDataId); + expect(noteThread.data!.notes.length).toEqual(3); + // Mark the note as editable + await noteThread.submitJson0Op(op => op.set(nt => nt.notes[0].editable, true)); + env.setProjectUserConfig(); + env.wait(); + let noteThreadIconElem: HTMLElement | null = env.getNoteThreadIconElement('verse_1_1', threadDataId); + noteThreadIconElem!.click(); + verify(mockedMatDialog.open(NoteDialogComponent, anything())).once(); + env.mockNoteDialogRef.close({ noteContent: content, status: NoteStatus.Resolved, noteDataId: 'thread01_note0' }); + env.wait(); + noteThread = env.getNoteThreadDoc(projectId, threadDataId); + expect(noteThread.data!.notes.length).toEqual(3); + expect(noteThread.data!.notes[0].content).toEqual(content); + expect(noteThread.data!.notes[0].status).toEqual(NoteStatus.Resolved); + + // the icon should be hidden from the editor + noteThreadIconElem = env.getNoteThreadIconElement('verse_1_1', threadDataId); + expect(noteThreadIconElem).toBeNull(); + env.dispose(); + })); + + it('does not allow editing and resolving a non-editable note', fakeAsync(() => { + const projectId: string = 'project01'; + const threadDataId: string = 'dataid01'; + const content: string = 'This thread is resolved.'; + const env = new TestEnvironment(); + const dialogMessage = spyOn((env.component as any).dialogService, 'message').and.stub(); + let noteThread: NoteThreadDoc = env.getNoteThreadDoc(projectId, threadDataId); + expect(noteThread.data!.notes.length).toEqual(3); + env.setProjectUserConfig(); + env.wait(); + let noteThreadIconElem: HTMLElement | null = env.getNoteThreadIconElement('verse_1_1', threadDataId); + noteThreadIconElem!.click(); + verify(mockedMatDialog.open(NoteDialogComponent, anything())).once(); + env.mockNoteDialogRef.close({ noteContent: content, status: NoteStatus.Resolved, noteDataId: 'thread01_note0' }); + env.wait(); + noteThread = env.getNoteThreadDoc(projectId, threadDataId); + expect(noteThread.data!.notes.length).toEqual(3); + expect(dialogMessage).toHaveBeenCalledTimes(1); + env.dispose(); + })); + it('can open dialog of the second note on the verse', fakeAsync(() => { const env = new TestEnvironment(); env.setProjectUserConfig(); diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.ts index 904eddee57..fa8062da4d 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.ts @@ -1366,6 +1366,7 @@ export class EditorComponent extends DataLoadingComponent implements OnDestroy, const tagId: number | undefined = params.threadDataId == null ? this.projectDoc?.data?.translateConfig.defaultNoteTagId : undefined; const noteContent: string | undefined = params.content == null ? undefined : XmlUtils.encodeForXml(params.content); + const noteStatus: NoteStatus = params.status ?? NoteStatus.Todo; // Configure the note const note: Note = { dateCreated: currentDate, @@ -1377,7 +1378,7 @@ export class EditorComponent extends DataLoadingComponent implements OnDestroy, content: noteContent, conflictType: NoteConflictType.DefaultValue, type: NoteType.Normal, - status: params.status ?? NoteStatus.Todo, + status: noteStatus, deleted: false, editable: true, versionNumber: 1 @@ -1411,9 +1412,12 @@ export class EditorComponent extends DataLoadingComponent implements OnDestroy, await threadDoc!.submitJson0Op(op => { op.set(t => t.notes[noteIndex].content, noteContent); op.set(t => t.notes[noteIndex].dateModified, currentDate); + op.set(t => t.notes[noteIndex].status, noteStatus); + // also set the status of the thread to be the status of the note + op.set(t => t.status, noteStatus); }); } else { - this.dialogService.message(this.i18n.translate('editor.cannot_edit_note_paratext')); + this.dialogService.message('editor.cannot_edit_note_paratext'); } } else { note.threadId = threadDoc.data!.threadId; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/note-dialog/note-dialog.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/note-dialog/note-dialog.component.spec.ts index 150b7206ab..b4a11b6987 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/note-dialog/note-dialog.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/note-dialog/note-dialog.component.spec.ts @@ -400,6 +400,27 @@ describe('NoteDialogComponent', () => { expect(env.dialogResult).toEqual({ noteContent: content, noteDataId: 'note05' }); })); + it('allows user to resolve the last note in the thread', fakeAsync(() => { + env = new TestEnvironment({ noteThread: TestEnvironment.getNoteThread(undefined, undefined, true) }); + // note03 is marked as deleted + expect(env.notes.length).toEqual(4); + const noteThread: NoteThreadDoc = env.getNoteThreadDoc('dataid01'); + expect(noteThread.data!.notes[4].content).toEqual('note05'); + const noteNumbers = [1, 2, 3]; + noteNumbers.forEach(n => expect(env.noteHasEditActions(n)).toBe(false)); + expect(env.noteHasEditActions(4)).toBe(true); + env.clickEditNote(); + expect(env.noteInputElement).toBeTruthy(); + expect(env.notes.length).toEqual(3); + noteNumbers.forEach(n => expect(env.noteHasEditActions(n)).toBe(false)); + expect(env.component.currentNoteContent).toEqual('note05'); + const content = 'note 05 edited content'; + env.enterNoteContent(content); + env.selectResolveOption(); + env.submit(); + expect(env.dialogResult).toEqual({ noteContent: content, noteDataId: 'note05', status: NoteStatus.Resolved }); + })); + it('does not allow user to edit the last note in the thread if it is not editable', fakeAsync(() => { env = new TestEnvironment({ noteThread: TestEnvironment.getNoteThread() }); // note03 is marked as deleted @@ -494,7 +515,7 @@ describe('NoteDialogComponent', () => { env.enterNoteContent(content); env.selectResolveOption(); env.submit(); - expect(env.dialogResult).toEqual({ status: NoteStatus.Resolved, noteContent: content }); + expect(env.dialogResult).toEqual({ status: NoteStatus.Resolved, noteContent: content, noteDataId: undefined }); })); it('allows changing save option', fakeAsync(() => { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/note-dialog/note-dialog.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/note-dialog/note-dialog.component.ts index 7ed32e50d2..349f56c78f 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/note-dialog/note-dialog.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/note-dialog/note-dialog.component.ts @@ -359,6 +359,7 @@ export class NoteDialogComponent implements OnInit { const content: NoteDialogResult = { status: NoteStatus.Resolved }; if (this.currentNoteContent.trim().length > 0) { content.noteContent = this.currentNoteContent; + content.noteDataId = this.noteIdBeingEdited; } this.dialogRef.close(content); } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/non_checking_en.json b/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/non_checking_en.json index 00bbb2272c..c554b77ad0 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/non_checking_en.json +++ b/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/non_checking_en.json @@ -28,6 +28,7 @@ "all": "All", "biblical_terms_renderings": "Biblical Terms Renderings", "blank": "(Blank)", + "cannot_edit_note_paratext": "You cannot edit this note as it has been edited in Paratext.", "category": "Category", "current_book": "Current Book", "current_chapter": "Current Chapter",