From 670685bea27336e1f201449e4edd8271981d27d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Thu, 13 Jul 2023 11:40:42 +0200 Subject: [PATCH 1/3] Improve disconnected handling --- .../ace/code-editor-ace.component.ts | 54 ++++++++-------- .../code-editor-container.component.ts | 13 +++- .../service/code-editor-repository.service.ts | 2 +- src/main/webapp/i18n/de/editor.json | 13 ++-- src/main/webapp/i18n/en/editor.json | 13 ++-- .../code-editor-ace.component.spec.ts | 63 ++++++++++++++++--- .../code-editor-container.integration.spec.ts | 14 ++++- 7 files changed, 121 insertions(+), 51 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.component.ts b/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.component.ts index da4c10c45923..e24540c1c83e 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.component.ts @@ -31,11 +31,10 @@ import { ViewChildren, ViewEncapsulation, } from '@angular/core'; -import { Subscription, fromEvent, of } from 'rxjs'; -import { catchError, tap } from 'rxjs/operators'; +import { Subscription, fromEvent } from 'rxjs'; import { CommitState, CreateFileChange, DeleteFileChange, EditorState, FileChange, RenameFileChange } from 'app/exercises/programming/shared/code-editor/model/code-editor.model'; import { CodeEditorFileService } from 'app/exercises/programming/shared/code-editor/service/code-editor-file.service'; -import { CodeEditorRepositoryFileService } from 'app/exercises/programming/shared/code-editor/service/code-editor-repository.service'; +import { CodeEditorRepositoryFileService, ConnectionError } from 'app/exercises/programming/shared/code-editor/service/code-editor-repository.service'; import { RepositoryFileService } from 'app/exercises/shared/result/repository.service'; import { TextChange } from 'app/entities/text-change.model'; import { LocalStorageService } from 'ngx-webstorage'; @@ -107,7 +106,7 @@ export class CodeEditorAceComponent implements AfterViewInit, OnChanges, OnDestr isLoading = false; annotationsArray: Array = []; annotationChange: Subscription; - fileSession: { [fileName: string]: { code: string; cursor: { column: number; row: number } } } = {}; + fileSession: { [fileName: string]: { code: string; cursor: { column: number; row: number }; loadingError: boolean } } = {}; // Inline feedback variables fileFeedbacks: Feedback[]; fileFeedbackPerLine: { [line: number]: Feedback } = {}; @@ -175,7 +174,7 @@ export class CodeEditorAceComponent implements AfterViewInit, OnChanges, OnDestr ) { // Current file has changed // Only load the file from server if there is nothing stored in the editorFileSessions - if (this.selectedFile && !this.fileSession[this.selectedFile]) { + if ((this.selectedFile && !this.fileSession[this.selectedFile]) || this.fileSession[this.selectedFile].loadingError) { this.loadFile(this.selectedFile); } else { this.initEditorAfterFileChange(); @@ -261,24 +260,29 @@ export class CodeEditorAceComponent implements AfterViewInit, OnChanges, OnDestr */ loadFile(fileName: string) { this.isLoading = true; - /** Query the repositoryFileService for the specified file in the repository */ - this.repositoryFileService - .getFile(fileName) - .pipe( - tap((fileObj) => { - this.fileSession[fileName] = { code: fileObj.fileContent, cursor: { column: 0, row: 0 } }; - // It is possible that the selected file has changed - in this case don't update the editor. - if (this.selectedFile === fileName) { - this.initEditorAfterFileChange(); - } - }), - catchError(() => { - return of(undefined); - }), - ) - .subscribe(() => { - this.isLoading = false; - }); + this.repositoryFileService.getFile(fileName).subscribe({ + next: (fileObj) => { + this.fileSession[fileName] = { code: fileObj.fileContent, cursor: { column: 0, row: 0 }, loadingError: false }; + this.finalizeLoading(fileName); + }, + error: (error) => { + this.fileSession[fileName] = { code: '', cursor: { column: 0, row: 0 }, loadingError: true }; + if (error.message === ConnectionError.message) { + this.onError.emit('loadingFailed' + error.message); + } else { + this.onError.emit('loadingFailed'); + } + this.finalizeLoading(fileName); + }, + }); + } + + finalizeLoading(fileName: string) { + // It is possible that the selected file has changed - in this case don't update the editor. + if (this.selectedFile === fileName) { + this.initEditorAfterFileChange(); + } + this.isLoading = false; } /** @@ -300,7 +304,7 @@ export class CodeEditorAceComponent implements AfterViewInit, OnChanges, OnDestr if (this.selectedFile && this.fileSession[this.selectedFile]) { if (this.fileSession[this.selectedFile].code !== code) { const cursor = this.editor.getEditor().getCursorPosition(); - this.fileSession[this.selectedFile] = { code, cursor }; + this.fileSession[this.selectedFile] = { code, cursor, loadingError: false }; this.onFileContentChange.emit({ file: this.selectedFile, fileContent: code }); } } @@ -413,7 +417,7 @@ export class CodeEditorAceComponent implements AfterViewInit, OnChanges, OnDestr this.annotationsArray = this.annotationsArray.filter((a) => a.fileName === fileChange.fileName); this.storeAnnotations([fileChange.fileName]); } else if (fileChange instanceof CreateFileChange && this.selectedFile === fileChange.fileName) { - this.fileSession = { ...this.fileSession, [fileChange.fileName]: { code: '', cursor: { row: 0, column: 0 } } }; + this.fileSession = { ...this.fileSession, [fileChange.fileName]: { code: '', cursor: { row: 0, column: 0 }, loadingError: false } }; this.initEditorAfterFileChange(); } this.displayAnnotations(); diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.ts b/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.ts index 0f8503d7891a..bca3973043db 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.ts @@ -25,6 +25,7 @@ import { Participation } from 'app/entities/participation/participation.model'; import { CodeEditorInstructionsComponent } from 'app/exercises/programming/shared/code-editor/instructions/code-editor-instructions.component'; import { Feedback } from 'app/entities/feedback.model'; import { Course } from 'app/entities/course.model'; +import { ConnectionError } from 'app/exercises/programming/shared/code-editor/service/code-editor-repository.service'; export enum CollapsableCodeEditorElement { FileBrowser, @@ -216,10 +217,18 @@ export class CodeEditorContainerComponent implements ComponentCanDeactivate { /** * Show an error as an alert in the top of the editor html. * Used by other components to display errors. - * The error must already be provided translated by the emitting component. + * @param error the translation key of the error that should be displayed */ onError(error: any) { - this.alertService.error(`artemisApp.editor.errors.${error as string}`); + let errorTranslationKey: string; + const translationParams = { connectionIssue: '' }; + if (typeof error !== 'string' || !error.includes(ConnectionError.message)) { + errorTranslationKey = error as string; + } else { + translationParams.connectionIssue = this.translateService.instant(`artemisApp.editor.errors.${ConnectionError.message}`); + errorTranslationKey = error.replaceAll(ConnectionError.message, ''); + } + this.alertService.error(`artemisApp.editor.errors.${errorTranslationKey}`, translationParams); } /** diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-repository.service.ts b/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-repository.service.ts index ef2f08721c13..83ac85b4648c 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-repository.service.ts +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-repository.service.ts @@ -70,7 +70,7 @@ const handleErrorResponse = (conflictService: CodeEditorConflictStateService) if (err.status === 409) { conflictService.notifyConflictState(GitConflictState.CHECKOUT_CONFLICT); } - if (err.status === 0) { + if (err.status === 0 || err.status === 504) { return throwError(() => new ConnectionError()); } return throwError(() => err); diff --git a/src/main/webapp/i18n/de/editor.json b/src/main/webapp/i18n/de/editor.json index db3f07a7c426..393ed12a1f4a 100644 --- a/src/main/webapp/i18n/de/editor.json +++ b/src/main/webapp/i18n/de/editor.json @@ -77,9 +77,10 @@ "errors": { "participationNotFound": "Deine Teilnahme konnte nicht gefunden werden.", "exerciseNotFound": "Die Übung konnte nicht gefunden werden.", - "saveFailed": "Eine oder mehrere Dateien konnte nicht gespeichert werden.", - "submitFailed": "Eine oder mehrere Dateien konnte nicht abgesendet werden.", - "refreshFailed": "Die Aktualisierung ist fehlgeschlagen", + "saveFailed": "Speichern ist fehlgeschlagen. {{ connectionIssue }}", + "submitFailed": "Absenden ist fehlgeschlagen. {{ connectionIssue }}", + "refreshFailed": "Aktualisieren ist fehlgeschlagen. {{ connectionIssue }}", + "loadingFailed": "Das Laden der Datei ist fehlgeschlagen. {{ connectionIssue }}", "noPermissions": "Du verfügst nicht über die notwendigen Berechtigungen.", "checkoutFailed": "Dein Git-Repository konnte nicht ausgecheckt werden.", "fileExists": "Datei/Verzeichnis Name existiert bereits. Bitte wähle einen anderen Namen.", @@ -92,13 +93,11 @@ "failedToLoadBuildLogs": "Die Buildlogs konnten nicht geladen werden.", "repositoryInConflict": "Dein Repository befindet sich in einem Konfliktzustand.", "notAllowedExam": "Du kannst (nicht mehr) abgeben", - "saveFailedInternetDisconnected": "Speichern ist fehlgeschlagen. Bitte stelle eine gute Internetverbindung sicher und versuche es nochmal.", - "submitFailedInternetDisconnected": "Absenden ist fehlgeschlagen. Bitte stelle eine gute Internetverbindung sicher und versuche es nochmal.", - "refreshFailedInternetDisconnected": "Aktualisieren ist fehlgeschlagen. Bitte stelle eine gute Internetverbindung sicher und versuche es nochmal.", "resetFailed": "Dein Repository konnte nicht zurückgesetzt werden.", "submitBeforeStartDate": "Du kannst vor dem Startdatum keine Abgaben einreichen.", "submitAfterDueDate": "Du kannst nach der Abgabefrist keine weiteren Abgaben einreichen.", - "submitAfterReachingSubmissionLimit": "Du hast das Abgabelimit erreicht und kannst keine weiteren Abgaben einreichen." + "submitAfterReachingSubmissionLimit": "Du hast das Abgabelimit erreicht und kannst keine weiteren Abgaben einreichen.", + "InternetDisconnected": "Bitte stelle eine gute Internetverbindung sicher und versuche es nochmal." }, "testStatusLabels": { "noResult": "Keine Ergebnisse", diff --git a/src/main/webapp/i18n/en/editor.json b/src/main/webapp/i18n/en/editor.json index 43dd6d3329a2..b1fff67f2580 100644 --- a/src/main/webapp/i18n/en/editor.json +++ b/src/main/webapp/i18n/en/editor.json @@ -78,9 +78,10 @@ "errors": { "participationNotFound": "Your participation could not be found.", "exerciseNotFound": "The exercise could not be found.", - "saveFailed": "One or more files could not be updated.", - "submitFailed": "One or more files could not be submitted.", - "refreshFailed": "Refresh has failed", + "saveFailed": "Saving failed. {{ connectionIssue }}", + "submitFailed": "Submitting failed. {{ connectionIssue }}", + "refreshFailed": "Refresh failed. {{ connectionIssue }}", + "loadingFailed": "Loading file failed. {{ connectionIssue }}", "noPermissions": "You don't have the necessary permissions.", "checkoutFailed": "The checkout of your git repository failed.", "fileExists": "File/Directory name already exists. Please choose a different name.", @@ -93,13 +94,11 @@ "failedToLoadBuildLogs": "The build logs could not be retrieved.", "repositoryInConflict": "Your repository has entered a conflict state.", "notAllowedExam": "You may not submit (anymore)", - "saveFailedInternetDisconnected": "Saving failed. Please make sure you have a stable internet connection and try again.", - "submitFailedInternetDisconnected": "Submit failed. Please make sure you have a stable internet connection and try again.", - "refreshFailedInternetDisconnected": "Refresh failed. Please make sure you have a stable internet connection and try again.", "resetFailed": "Your repository could not be reset.", "submitBeforeStartDate": "You cannot submit before the start date of the exercise.", "submitAfterDueDate": "You cannot submit after the due date of the exercise.", - "submitAfterReachingSubmissionLimit": "You reached the submission limit and cannot participate anymore." + "submitAfterReachingSubmissionLimit": "You reached the submission limit and cannot participate anymore.", + "InternetDisconnected": "Please make sure you have a stable internet connection and try again." }, "testStatusLabels": { "noResult": "No results", diff --git a/src/test/javascript/spec/component/code-editor/code-editor-ace.component.spec.ts b/src/test/javascript/spec/component/code-editor/code-editor-ace.component.spec.ts index e3388c9b30a9..baafe679e8a6 100644 --- a/src/test/javascript/spec/component/code-editor/code-editor-ace.component.spec.ts +++ b/src/test/javascript/spec/component/code-editor/code-editor-ace.component.spec.ts @@ -5,9 +5,9 @@ import { NgModel } from '@angular/forms'; import { AceEditorModule } from 'app/shared/markdown-editor/ace-editor/ace-editor.module'; import { Subject } from 'rxjs'; import { ArtemisTestModule } from '../../test.module'; -import { CreateFileChange, FileType, RenameFileChange } from 'app/exercises/programming/shared/code-editor/model/code-editor.model'; +import { CreateFileChange, EditorState, FileType, RenameFileChange } from 'app/exercises/programming/shared/code-editor/model/code-editor.model'; import { triggerChanges } from '../../helpers/utils/general.utils'; -import { CodeEditorRepositoryFileService } from 'app/exercises/programming/shared/code-editor/service/code-editor-repository.service'; +import { CodeEditorRepositoryFileService, ConnectionError } from 'app/exercises/programming/shared/code-editor/service/code-editor-repository.service'; import { CodeEditorFileService } from 'app/exercises/programming/shared/code-editor/service/code-editor-file.service'; import { CodeEditorAceComponent } from 'app/exercises/programming/shared/code-editor/ace/code-editor-ace.component'; import { MockCodeEditorRepositoryFileService } from '../../helpers/mocks/service/mock-code-editor-repository-file.service'; @@ -126,9 +126,38 @@ describe('CodeEditorAceComponent', () => { fixture.detectChanges(); expect(comp.isLoading).toBeFalse(); + expect(comp.fileSession).toEqual({ dummy: { code: 'lorem ipsum', cursor: { column: 0, row: 0 }, loadingError: false } }); expect(initEditorAfterFileChangeSpy).toHaveBeenCalledWith(); }); + it.each([ + [new ConnectionError(), 'loadingFailedInternetDisconnected'], + [new Error(), 'loadingFailed'], + ])('should correctly init editor after file change in case of error', (error: Error, errorCode: string) => { + const selectedFile = 'dummy'; + const fileSession = {}; + const loadFileSubject = new Subject(); + const initEditorAfterFileChangeSpy = jest.spyOn(comp, 'initEditorAfterFileChange'); + const onErrorSpy = jest.spyOn(comp.onError, 'emit'); + loadRepositoryFileStub.mockReturnValue(loadFileSubject); + comp.selectedFile = selectedFile; + comp.fileSession = fileSession; + + triggerChanges(comp, { property: 'selectedFile', currentValue: selectedFile }); + fixture.detectChanges(); + + expect(comp.isLoading).toBeTrue(); + expect(loadRepositoryFileStub).toHaveBeenCalledWith(selectedFile); + expect(initEditorAfterFileChangeSpy).not.toHaveBeenCalled(); + loadFileSubject.error(error); + fixture.detectChanges(); + + expect(comp.isLoading).toBeFalse(); + expect(comp.fileSession).toEqual({ dummy: { code: '', cursor: { column: 0, row: 0 }, loadingError: true } }); + expect(initEditorAfterFileChangeSpy).toHaveBeenCalledWith(); + expect(onErrorSpy).toHaveBeenCalledWith(errorCode); + }); + it('should discard all new feedback after a re-init because of a file change', () => { const getInlineFeedbackNodeStub = jest.spyOn(comp, 'getInlineFeedbackNode'); getInlineFeedbackNodeStub.mockReturnValue(undefined); @@ -139,7 +168,7 @@ describe('CodeEditorAceComponent', () => { it('should not load the file from server on selected file change if the file is already in session', () => { const selectedFile = 'dummy'; - const fileSession = { [selectedFile]: { code: 'lorem ipsum', cursor: { column: 0, row: 0 } } }; + const fileSession = { [selectedFile]: { code: 'lorem ipsum', cursor: { column: 0, row: 0 }, loadingError: false } }; const initEditorAfterFileChangeSpy = jest.spyOn(comp, 'initEditorAfterFileChange'); const loadFileSpy = jest.spyOn(comp, 'loadFile'); comp.selectedFile = selectedFile; @@ -152,11 +181,29 @@ describe('CodeEditorAceComponent', () => { expect(loadFileSpy).not.toHaveBeenCalled(); }); + it('should load the file from server on selected file change if the file is already in session but there was a loading error', () => { + const selectedFile = 'dummy'; + const fileSession = { [selectedFile]: { code: 'lorem ipsum', cursor: { column: 0, row: 0 }, loadingError: true } }; + const initEditorAfterFileChangeSpy = jest.spyOn(comp, 'initEditorAfterFileChange'); + const loadFileSpy = jest.spyOn(comp, 'loadFile'); + comp.selectedFile = selectedFile; + comp.fileSession = fileSession; + + triggerChanges(comp, { property: 'selectedFile', currentValue: selectedFile }); + fixture.detectChanges(); + + expect(initEditorAfterFileChangeSpy).not.toHaveBeenCalled(); + expect(loadFileSpy).toHaveBeenCalledOnce(); + }); + it('should update file session references on file rename', () => { const selectedFile = 'file'; const newFileName = 'newFilename'; const fileChange = new RenameFileChange(FileType.FILE, selectedFile, newFileName); - const fileSession = { [selectedFile]: { code: 'lorem ipsum', cursor: { column: 0, row: 0 } }, anotherFile: { code: 'lorem ipsum 2', cursor: { column: 0, row: 0 } } }; + const fileSession = { + [selectedFile]: { code: 'lorem ipsum', cursor: { column: 0, row: 0 }, loadingError: false }, + anotherFile: { code: 'lorem ipsum 2', cursor: { column: 0, row: 0 }, loadingError: false }, + }; comp.selectedFile = newFileName; comp.fileSession = fileSession; @@ -168,7 +215,7 @@ describe('CodeEditorAceComponent', () => { it('should init editor on newly created file if selected', () => { const selectedFile = 'file'; const fileChange = new CreateFileChange(FileType.FILE, selectedFile); - const fileSession = { anotherFile: { code: 'lorem ipsum 2', cursor: { column: 0, row: 0 } } }; + const fileSession = { anotherFile: { code: 'lorem ipsum 2', cursor: { column: 0, row: 0 }, loadingError: false } }; const initEditorAfterFileChangeSpy = jest.spyOn(comp, 'initEditorAfterFileChange'); comp.selectedFile = selectedFile; comp.fileSession = fileSession; @@ -176,7 +223,7 @@ describe('CodeEditorAceComponent', () => { comp.onFileChange(fileChange); expect(initEditorAfterFileChangeSpy).toHaveBeenCalledWith(); - expect(comp.fileSession).toEqual({ anotherFile: fileSession.anotherFile, [fileChange.fileName]: { code: '', cursor: { row: 0, column: 0 } } }); + expect(comp.fileSession).toEqual({ anotherFile: fileSession.anotherFile, [fileChange.fileName]: { code: '', cursor: { row: 0, column: 0 }, loadingError: false } }); }); it('should not do anything on file content change if the code has not changed', () => { @@ -185,7 +232,7 @@ describe('CodeEditorAceComponent', () => { const onFileContentChangeSpy = jest.spyOn(comp.onFileContentChange, 'emit'); const selectedFile = 'file'; - const fileSession = { [selectedFile]: { code: 'lorem ipsum', cursor: { column: 0, row: 0 } } }; + const fileSession = { [selectedFile]: { code: 'lorem ipsum', cursor: { column: 0, row: 0 }, loadingError: false } }; comp.selectedFile = selectedFile; comp.fileSession = fileSession; @@ -199,7 +246,7 @@ describe('CodeEditorAceComponent', () => { const selectedFile = 'file'; const newFileContent = 'lorem ipsum new'; - const fileSession = { [selectedFile]: { code: 'lorem ipsum', cursor: { column: 0, row: 0 } } }; + const fileSession = { [selectedFile]: { code: 'lorem ipsum', cursor: { column: 0, row: 0 }, loadingError: false } }; const annotations = [{ fileName: selectedFile, row: 5, column: 4, text: 'error', type: 'error', timestamp: 0 }]; const editorChange = { start: { row: 1, column: 1 }, end: { row: 2, column: 1 }, action: 'remove' }; diff --git a/src/test/javascript/spec/integration/code-editor/code-editor-container.integration.spec.ts b/src/test/javascript/spec/integration/code-editor/code-editor-container.integration.spec.ts index 03dc878a48af..e393997af0b9 100644 --- a/src/test/javascript/spec/integration/code-editor/code-editor-container.integration.spec.ts +++ b/src/test/javascript/spec/integration/code-editor/code-editor-container.integration.spec.ts @@ -45,7 +45,7 @@ import { CodeEditorContainerComponent } from 'app/exercises/programming/shared/c import { omit } from 'lodash-es'; import { ProgrammingLanguage, ProjectType } from 'app/entities/programming-exercise.model'; import { CodeEditorGridComponent } from 'app/exercises/programming/shared/code-editor/layout/code-editor-grid.component'; -import { MockComponent, MockDirective, MockModule, MockPipe } from 'ng-mocks'; +import { MockComponent, MockDirective, MockModule, MockPipe, MockProvider } from 'ng-mocks'; import { CodeEditorActionsComponent } from 'app/exercises/programming/shared/code-editor/actions/code-editor-actions.component'; import { CodeEditorFileBrowserComponent } from 'app/exercises/programming/shared/code-editor/file-browser/code-editor-file-browser.component'; import { CodeEditorAceComponent } from 'app/exercises/programming/shared/code-editor/ace/code-editor-ace.component'; @@ -66,6 +66,7 @@ import { TreeviewComponent } from 'app/exercises/programming/shared/code-editor/ import { TreeviewItemComponent } from 'app/exercises/programming/shared/code-editor/treeview/components/treeview-item/treeview-item.component'; import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { CodeEditorHeaderComponent } from 'app/exercises/programming/shared/code-editor/header/code-editor-header.component'; +import { AlertService } from 'app/core/util/alert.service'; describe('CodeEditorContainerIntegration', () => { // needed to make sure ace is defined @@ -120,6 +121,7 @@ describe('CodeEditorContainerIntegration', () => { providers: [ ChangeDetectorRef, CodeEditorConflictStateService, + MockProvider(AlertService), { provide: ActivatedRoute, useClass: MockActivatedRouteWithSubjects }, { provide: JhiWebsocketService, useClass: MockWebsocketService }, { provide: ParticipationWebsocketService, useClass: MockParticipationWebsocketService }, @@ -550,4 +552,14 @@ describe('CodeEditorContainerIntegration', () => { containerFixture.destroy(); flush(); })); + + it.each([ + ['loadingFailed', 'artemisApp.editor.errors.loadingFailed', { connectionIssue: '' }], + ['loadingFailedInternetDisconnected', 'artemisApp.editor.errors.loadingFailed', { connectionIssue: 'artemisApp.editor.errors.InternetDisconnected' }], + ])('onError should handle disconnectedInternet', (error: string, errorKey: string, translationParams: { connectionIssue: string }) => { + const alertService = TestBed.inject(AlertService); + const alertServiceSpy = jest.spyOn(alertService, 'error'); + container.onError(error); + expect(alertServiceSpy).toHaveBeenCalledWith(errorKey, translationParams); + }); }); From e7ee5ce3359912633e1328a4d25c6bbcf5041f29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Thu, 13 Jul 2023 11:57:42 +0200 Subject: [PATCH 2/3] Make file with error read only --- .../code-editor/ace/code-editor-ace.component.html | 2 +- .../code-editor/code-editor-ace.component.spec.ts | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.component.html b/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.component.html index c662957d3296..1e430e9caa7c 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.component.html +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.component.html @@ -24,7 +24,7 @@ #editor id="ace-code-editor" [mode]="editorMode ? editorMode : 'java'" - [readOnly]="isLoading || disableActions" + [readOnly]="isLoading || disableActions || !!fileSession[selectedFile]?.loadingError" [hidden]="!selectedFile || isLoading" [autoUpdateContent]="true" [durationBeforeCallback]="200" diff --git a/src/test/javascript/spec/component/code-editor/code-editor-ace.component.spec.ts b/src/test/javascript/spec/component/code-editor/code-editor-ace.component.spec.ts index baafe679e8a6..fe8096113a70 100644 --- a/src/test/javascript/spec/component/code-editor/code-editor-ace.component.spec.ts +++ b/src/test/javascript/spec/component/code-editor/code-editor-ace.component.spec.ts @@ -5,7 +5,7 @@ import { NgModel } from '@angular/forms'; import { AceEditorModule } from 'app/shared/markdown-editor/ace-editor/ace-editor.module'; import { Subject } from 'rxjs'; import { ArtemisTestModule } from '../../test.module'; -import { CreateFileChange, EditorState, FileType, RenameFileChange } from 'app/exercises/programming/shared/code-editor/model/code-editor.model'; +import { CreateFileChange, FileType, RenameFileChange } from 'app/exercises/programming/shared/code-editor/model/code-editor.model'; import { triggerChanges } from '../../helpers/utils/general.utils'; import { CodeEditorRepositoryFileService, ConnectionError } from 'app/exercises/programming/shared/code-editor/service/code-editor-repository.service'; import { CodeEditorFileService } from 'app/exercises/programming/shared/code-editor/service/code-editor-file.service'; @@ -272,6 +272,15 @@ describe('CodeEditorAceComponent', () => { expect(displayFeedbacksSpy).toHaveBeenCalledOnce(); }); + it('should be in readonly mode when the file could not be loaded', () => { + comp.selectedFile = 'asdf'; + comp.fileSession = { asdf: { code: '', cursor: { column: 0, row: 0 }, loadingError: true } }; + + fixture.detectChanges(); + + expect(comp.editor.getEditor().getReadOnly()).toBeTrue(); + }); + it('should setup inline comment buttons in gutter', () => { comp.isTutorAssessment = true; comp.readOnlyManualFeedback = false; From 4bfb3b855d7327c085d7237cce7478089f3c3ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Sat, 15 Jul 2023 01:06:48 +0200 Subject: [PATCH 3/3] Make Lucas happy --- .../shared/code-editor/ace/code-editor-ace.component.ts | 3 ++- .../code-editor/container/code-editor-container.component.ts | 4 ++-- src/main/webapp/i18n/de/editor.json | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.component.ts b/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.component.ts index e24540c1c83e..9596c71460be 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.component.ts @@ -45,6 +45,7 @@ import { faCircleNotch, faPlusSquare } from '@fortawesome/free-solid-svg-icons'; import { CodeEditorTutorAssessmentInlineFeedbackComponent } from 'app/exercises/programming/assess/code-editor-tutor-assessment-inline-feedback.component'; export type Annotation = { fileName: string; row: number; column: number; text: string; type: string; timestamp: number; hash?: string }; +export type FileSession = { [fileName: string]: { code: string; cursor: { column: number; row: number }; loadingError: boolean } }; @Component({ selector: 'jhi-code-editor-ace', @@ -106,7 +107,7 @@ export class CodeEditorAceComponent implements AfterViewInit, OnChanges, OnDestr isLoading = false; annotationsArray: Array = []; annotationChange: Subscription; - fileSession: { [fileName: string]: { code: string; cursor: { column: number; row: number }; loadingError: boolean } } = {}; + fileSession: FileSession = {}; // Inline feedback variables fileFeedbacks: Feedback[]; fileFeedbackPerLine: { [line: number]: Feedback } = {}; diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.ts b/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.ts index bca3973043db..15868e86dbc1 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.ts @@ -222,8 +222,8 @@ export class CodeEditorContainerComponent implements ComponentCanDeactivate { onError(error: any) { let errorTranslationKey: string; const translationParams = { connectionIssue: '' }; - if (typeof error !== 'string' || !error.includes(ConnectionError.message)) { - errorTranslationKey = error as string; + if (!error.includes(ConnectionError.message)) { + errorTranslationKey = error; } else { translationParams.connectionIssue = this.translateService.instant(`artemisApp.editor.errors.${ConnectionError.message}`); errorTranslationKey = error.replaceAll(ConnectionError.message, ''); diff --git a/src/main/webapp/i18n/de/editor.json b/src/main/webapp/i18n/de/editor.json index 393ed12a1f4a..eb94710c7b3f 100644 --- a/src/main/webapp/i18n/de/editor.json +++ b/src/main/webapp/i18n/de/editor.json @@ -97,7 +97,7 @@ "submitBeforeStartDate": "Du kannst vor dem Startdatum keine Abgaben einreichen.", "submitAfterDueDate": "Du kannst nach der Abgabefrist keine weiteren Abgaben einreichen.", "submitAfterReachingSubmissionLimit": "Du hast das Abgabelimit erreicht und kannst keine weiteren Abgaben einreichen.", - "InternetDisconnected": "Bitte stelle eine gute Internetverbindung sicher und versuche es nochmal." + "InternetDisconnected": "Bitte stelle eine stabile Internetverbindung sicher und versuche es nochmal." }, "testStatusLabels": { "noResult": "Keine Ergebnisse",