From d96a43798a886b39157c0eb3d6bbf9e1d8f4003b Mon Sep 17 00:00:00 2001 From: Benjamin Schmitz <66966223+bensofficial@users.noreply.github.com> Date: Tue, 17 Dec 2024 20:23:51 +0100 Subject: [PATCH 1/5] Development: Add test server 7 to GitHub deployment (#10042) --- .github/workflows/testserver.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/testserver.yml b/.github/workflows/testserver.yml index db5b0fce1000..f99ae615b36b 100644 --- a/.github/workflows/testserver.yml +++ b/.github/workflows/testserver.yml @@ -138,12 +138,13 @@ jobs: folder: /opt/artemis host_keys: | - #- environment: artemis-test7.artemis.cit.tum.de - # label-identifier: artemis-test7 - # url: https://artemis-test7.artemis.cit.tum.de - # user: deployment - # hosts: artemis-test7.artemis.cit.tum.de - # folder: /opt/artemis + - environment: artemis-test7.artemis.cit.tum.de + label-identifier: artemis-test7 + url: https://artemis-test7.artemis.cit.tum.de + user: deployment + hosts: artemis-test7.artemis.cit.tum.de + folder: /opt/artemis + host_keys: | #- environment: artemis-test8.artemis.cit.tum.de # label-identifier: artemis-test8 From 3a6832e88ba1a3c5f44251d2f893d3bea89b1d0a Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Tue, 17 Dec 2024 20:52:06 +0100 Subject: [PATCH 2/5] Development: Update server dependencies --- build.gradle | 15 +++++++++++---- gradle.properties | 10 ++++++---- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 00f7bbb49d39..4091d7584bc7 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ plugins { id "idea" id "jacoco" id "org.springframework.boot" version "${spring_boot_version}" - id "io.spring.dependency-management" version "1.1.6" + id "io.spring.dependency-management" version "1.1.7" id "com.google.cloud.tools.jib" version "3.4.4" id "com.github.node-gradle.node" version "${gradle_node_plugin_version}" id "com.diffplug.spotless" version "6.25.0" @@ -384,6 +384,13 @@ dependencies { exclude module: "spring-boot-starter-undertow" } implementation "org.springframework.boot:spring-boot-starter-tomcat:${spring_boot_version}" + + // Avoid security issues in Tomcat 10.1.33 + implementation "org.apache.tomcat.embed:tomcat-embed-core:${tomcat_version}" + implementation "org.apache.tomcat.embed:tomcat-embed-el:${tomcat_version}" + implementation "org.apache.tomcat.embed:tomcat-embed-websocket:${tomcat_version}" + implementation "org.apache.tomcat:tomcat-annotations-api:${tomcat_version}" + implementation "org.springframework.boot:spring-boot-starter-websocket:${spring_boot_version}" implementation "org.springframework.boot:spring-boot-starter-thymeleaf:${spring_boot_version}" implementation "org.springframework.boot:spring-boot-starter-oauth2-resource-server:${spring_boot_version}" @@ -400,7 +407,7 @@ dependencies { implementation "org.springframework.cloud:spring-cloud-starter-config:${spring_cloud_version}" implementation "org.springframework.cloud:spring-cloud-commons:${spring_cloud_version}" - implementation "io.netty:netty-all:4.1.115.Final" + implementation "io.netty:netty-all:4.1.116.Final" implementation "io.projectreactor.netty:reactor-netty:1.2.1" implementation "org.springframework:spring-messaging:${spring_framework_version}" implementation "org.springframework.retry:spring-retry:2.0.11" @@ -451,7 +458,7 @@ dependencies { implementation "org.apfloat:apfloat:1.14.0" // use newest version of guava to avoid security issues through outdated dependencies - implementation "com.google.guava:guava:33.3.1-jre" + implementation "com.google.guava:guava:33.4.0-jre" implementation "com.sun.activation:jakarta.activation:2.0.1" // use newest version of gson to avoid security issues through outdated dependencies @@ -607,7 +614,7 @@ tasks.withType(Test).configureEach { } wrapper { - gradleVersion = "8.12-rc-1" + gradleVersion = "8.12-rc-2" } tasks.register("stage") { diff --git a/gradle.properties b/gradle.properties index de944d16001d..c110eb971ddc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ jhipster_dependencies_version=8.7.2 spring_boot_version=3.4.0 spring_framework_version=6.2.1 spring_cloud_version=4.2.0 -spring_security_version=6.4.1 +spring_security_version=6.4.2 # TODO: upgrading to 6.6.x currently leads to issues due to internal changes in Hibernate and potentially wrong use in Artemis server code hibernate_version=6.4.10.Final # TODO: can we update to 5.x? @@ -19,7 +19,8 @@ jwt_version=0.12.6 jaxb_runtime_version=4.0.5 hazelcast_version=5.5.0 fasterxml_version=2.18.2 -jgit_version=7.1.0.202411261347-r +# TODO: 7.1.0 includes bugs related to git diffs, therefore we cannot update +jgit_version=7.0.0.202409031743-r sshd_version=2.14.0 checkstyle_version=10.21.0 jplag_version=5.1.0 @@ -32,13 +33,14 @@ liquibase_version=4.30.0 docker_java_version=3.4.1 logback_version=1.5.12 java_parser_version=3.26.2 -byte_buddy_version=1.15.10 +byte_buddy_version=1.15.11 netty_version=4.1.115.Final +tomcat_version=10.1.34 # testing # make sure both versions are compatible junit_version=5.11.3 -junit_platform_version=1.11.3 +junit_platform_version=1.11.4 mockito_version=5.14.2 testcontainer_version=1.20.4 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9e40988550fd..fb4b1a2e2ced 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-rc-1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-rc-2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 92b2aad9cf275f9eb85f4d4e971317a15b8950f6 Mon Sep 17 00:00:00 2001 From: Florian Glombik <63976129+florian-glombik@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:06:54 +0100 Subject: [PATCH 3/5] Lectures: Add dismiss modal for unsaved changes to title or period section (#10023) --- .../exercise-title-channel-name.component.ts | 6 +- .../close-edit-lecture-modal.component.html | 34 ++++ .../close-edit-lecture-modal.component.ts | 25 +++ .../lecture/hasLectureUnsavedChanges.guard.ts | 28 ++++ .../lecture-period.component.html} | 8 +- .../lecture-period.component.ts | 23 +++ .../lecture-title-channel-name.component.ts | 5 +- .../app/lecture/lecture-update.component.html | 56 +++---- .../app/lecture/lecture-update.component.ts | 147 +++++++++++++----- src/main/webapp/app/lecture/lecture.module.ts | 4 +- src/main/webapp/app/lecture/lecture.route.ts | 2 + .../lecture-update-wizard.component.html | 2 +- .../lecture-wizard-period.component.ts | 12 -- .../title-channel-name.component.ts | 6 +- src/main/webapp/i18n/de/lecture.json | 10 +- src/main/webapp/i18n/en/lecture.json | 10 +- ...close-edit-lecture-modal.component.spec.ts | 22 +++ .../hasLectureUnsavedChanges.guard.spec.ts | 95 +++++++++++ ...ec.ts => lecture-period.component.spec.ts} | 19 +-- .../lecture/lecture-update.component.spec.ts | 133 ++++++++++++---- .../lecture-wizard-title.component.spec.ts | 20 ++- .../lecture-wizard.component.spec.ts | 26 ++-- 22 files changed, 533 insertions(+), 160 deletions(-) create mode 100644 src/main/webapp/app/lecture/close-edit-lecture-dialog/close-edit-lecture-modal.component.html create mode 100644 src/main/webapp/app/lecture/close-edit-lecture-dialog/close-edit-lecture-modal.component.ts create mode 100644 src/main/webapp/app/lecture/hasLectureUnsavedChanges.guard.ts rename src/main/webapp/app/lecture/{wizard-mode/lecture-wizard-period.component.html => lecture-period/lecture-period.component.html} (78%) create mode 100644 src/main/webapp/app/lecture/lecture-period/lecture-period.component.ts delete mode 100644 src/main/webapp/app/lecture/wizard-mode/lecture-wizard-period.component.ts create mode 100644 src/test/javascript/spec/component/lecture/close-edit-lecture-modal.component.spec.ts create mode 100644 src/test/javascript/spec/component/lecture/hasLectureUnsavedChanges.guard.spec.ts rename src/test/javascript/spec/component/lecture/{wizard-mode/lecture-wizard-period.component.spec.ts => lecture-period.component.spec.ts} (54%) diff --git a/src/main/webapp/app/exercises/shared/exercise-title-channel-name/exercise-title-channel-name.component.ts b/src/main/webapp/app/exercises/shared/exercise-title-channel-name/exercise-title-channel-name.component.ts index 5ad7384d3d17..5b53397e616e 100644 --- a/src/main/webapp/app/exercises/shared/exercise-title-channel-name/exercise-title-channel-name.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-title-channel-name/exercise-title-channel-name.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewChild, effect, inject, input, signal } from '@angular/core'; +import { Component, Input, OnChanges, SimpleChanges, ViewChild, effect, inject, input, output, signal } from '@angular/core'; import { Course, isCommunicationEnabled } from 'app/entities/course.model'; import { Exercise } from 'app/entities/exercise.model'; import { TitleChannelNameComponent } from 'app/shared/form/title-channel-name/title-channel-name.component'; @@ -22,8 +22,8 @@ export class ExerciseTitleChannelNameComponent implements OnChanges { @ViewChild(TitleChannelNameComponent) titleChannelNameComponent: TitleChannelNameComponent; - @Output() onTitleChange = new EventEmitter(); - @Output() onChannelNameChange = new EventEmitter(); + onTitleChange = output(); + onChannelNameChange = output(); private readonly exerciseService: ExerciseService = inject(ExerciseService); diff --git a/src/main/webapp/app/lecture/close-edit-lecture-dialog/close-edit-lecture-modal.component.html b/src/main/webapp/app/lecture/close-edit-lecture-dialog/close-edit-lecture-modal.component.html new file mode 100644 index 000000000000..afc317c054bc --- /dev/null +++ b/src/main/webapp/app/lecture/close-edit-lecture-dialog/close-edit-lecture-modal.component.html @@ -0,0 +1,34 @@ + + + diff --git a/src/main/webapp/app/lecture/close-edit-lecture-dialog/close-edit-lecture-modal.component.ts b/src/main/webapp/app/lecture/close-edit-lecture-dialog/close-edit-lecture-modal.component.ts new file mode 100644 index 000000000000..cd7024b6c5af --- /dev/null +++ b/src/main/webapp/app/lecture/close-edit-lecture-dialog/close-edit-lecture-modal.component.ts @@ -0,0 +1,25 @@ +import { Component, Input, inject } from '@angular/core'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'jhi-close-edit-lecture-modal', + standalone: true, + imports: [TranslateDirective, ArtemisSharedCommonModule], + templateUrl: './close-edit-lecture-modal.component.html', +}) +export class CloseEditLectureModalComponent { + protected readonly faTimes = faTimes; + + protected readonly activeModal = inject(NgbActiveModal); + + // no input signals yet as they can not be initialized with current ng-bootstrap version https://stackoverflow.com/a/79094268/16540383 + @Input() hasUnsavedChangesInTitleSection: boolean; + @Input() hasUnsavedChangesInPeriodSection: boolean; + + closeWindow(isCloseConfirmed: boolean): void { + this.activeModal.close(isCloseConfirmed); + } +} diff --git a/src/main/webapp/app/lecture/hasLectureUnsavedChanges.guard.ts b/src/main/webapp/app/lecture/hasLectureUnsavedChanges.guard.ts new file mode 100644 index 000000000000..b292d2413830 --- /dev/null +++ b/src/main/webapp/app/lecture/hasLectureUnsavedChanges.guard.ts @@ -0,0 +1,28 @@ +import { inject } from '@angular/core'; +import { CanDeactivateFn } from '@angular/router'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { LectureUpdateComponent } from 'app/lecture/lecture-update.component'; +import { Observable, from, of } from 'rxjs'; +import { CloseEditLectureModalComponent } from 'app/lecture/close-edit-lecture-dialog/close-edit-lecture-modal.component'; + +export const hasLectureUnsavedChangesGuard: CanDeactivateFn = (component: LectureUpdateComponent): Observable => { + if (!component.shouldDisplayDismissWarning || component.isShowingWizardMode) { + return of(true); + } + + if (component.isChangeMadeToTitleOrPeriodSection) { + const modalService = inject(NgbModal); + + const modalRef: NgbModalRef = modalService.open(CloseEditLectureModalComponent, { + size: 'lg', + backdrop: 'static', + animation: true, + }); + modalRef.componentInstance.hasUnsavedChangesInTitleSection = component.isChangeMadeToTitleSection(); + modalRef.componentInstance.hasUnsavedChangesInPeriodSection = component.isChangeMadeToPeriodSection(); + + return from(modalRef.result); + } + + return of(true); +}; diff --git a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-period.component.html b/src/main/webapp/app/lecture/lecture-period/lecture-period.component.html similarity index 78% rename from src/main/webapp/app/lecture/wizard-mode/lecture-wizard-period.component.html rename to src/main/webapp/app/lecture/lecture-period/lecture-period.component.html index 7e205ffa58f9..5434a21491a2 100644 --- a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-period.component.html +++ b/src/main/webapp/app/lecture/lecture-period/lecture-period.component.html @@ -1,11 +1,11 @@
-

+

(); + @Input() validateDatesFunction: () => void; + + periodSectionDatepickers = viewChildren(FormDateTimePickerComponent); + + isPeriodSectionValid: Signal = computed(() => { + for (const periodSectionDatepicker of this.periodSectionDatepickers()) { + if (!periodSectionDatepicker.isValid()) { + return false; + } + } + return true; + }); +} diff --git a/src/main/webapp/app/lecture/lecture-title-channel-name.component.ts b/src/main/webapp/app/lecture/lecture-title-channel-name.component.ts index 0bda4027f089..f2c8ffbc915e 100644 --- a/src/main/webapp/app/lecture/lecture-title-channel-name.component.ts +++ b/src/main/webapp/app/lecture/lecture-title-channel-name.component.ts @@ -1,6 +1,7 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnInit, viewChild } from '@angular/core'; import { isCommunicationEnabled } from 'app/entities/course.model'; import { Lecture } from 'app/entities/lecture.model'; +import { TitleChannelNameComponent } from 'app/shared/form/title-channel-name/title-channel-name.component'; @Component({ selector: 'jhi-lecture-title-channel-name', @@ -9,6 +10,8 @@ import { Lecture } from 'app/entities/lecture.model'; export class LectureTitleChannelNameComponent implements OnInit { @Input() lecture: Lecture; + titleChannelNameComponent = viewChild.required(TitleChannelNameComponent); + hideChannelNameInput = false; ngOnInit() { this.hideChannelNameInput = !this.requiresChannelName(this.lecture); diff --git a/src/main/webapp/app/lecture/lecture-update.component.html b/src/main/webapp/app/lecture/lecture-update.component.html index 9ba49083c37c..332e6b86f4a1 100644 --- a/src/main/webapp/app/lecture/lecture-update.component.html +++ b/src/main/webapp/app/lecture/lecture-update.component.html @@ -5,7 +5,7 @@ [toggleModeFunction]="toggleModeFunction" [saveLectureFunction]="saveLectureFunction" [validateDatesFunction]="onDatesValuesChanged" - [lecture]="lecture" + [lecture]="lecture()" [isSaving]="isSaving" /> } @@ -27,45 +27,24 @@

+

+

+
- -
-
-
- -
-
- -
-
- -
+
- @if (lecture.course) { + + @if (lecture().course) {
- +
} @@ -116,7 +95,14 @@

  -

diff --git a/src/main/webapp/app/lecture/lecture-update.component.ts b/src/main/webapp/app/lecture/lecture-update.component.ts index b72ea717afd9..c4ec628fc417 100644 --- a/src/main/webapp/app/lecture/lecture-update.component.ts +++ b/src/main/webapp/app/lecture/lecture-update.component.ts @@ -1,10 +1,9 @@ -import { Component, OnInit, ViewChild } from '@angular/core'; +import { Component, OnDestroy, OnInit, ViewChild, effect, inject, signal, viewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import { Observable, Subscription } from 'rxjs'; import { AlertService } from 'app/core/util/alert.service'; import { LectureService } from './lecture.service'; -import { CourseManagementService } from '../course/manage/course-management.service'; import { Lecture } from 'app/entities/lecture.model'; import { Course } from 'app/entities/course.model'; import { onError } from 'app/shared/util/global.utils'; @@ -12,29 +11,43 @@ import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; import { DocumentationType } from 'app/shared/components/documentation-button/documentation-button.component'; import { faBan, faHandshakeAngle, faPuzzlePiece, faQuestionCircle, faSave } from '@fortawesome/free-solid-svg-icons'; import { LectureUpdateWizardComponent } from 'app/lecture/wizard-mode/lecture-update-wizard.component'; -import { UPLOAD_FILE_EXTENSIONS } from 'app/shared/constants/file-extensions.constants'; +import { ACCEPTED_FILE_EXTENSIONS_FILE_BROWSER, ALLOWED_FILE_EXTENSIONS_HUMAN_READABLE } from 'app/shared/constants/file-extensions.constants'; import { FormulaAction } from 'app/shared/monaco-editor/model/actions/formula.action'; +import { LectureTitleChannelNameComponent } from './lecture-title-channel-name.component'; +import { LectureUpdatePeriodComponent } from 'app/lecture/lecture-period/lecture-period.component'; +import dayjs, { Dayjs } from 'dayjs'; +import { FormDateTimePickerComponent } from 'app/shared/date-time-picker/date-time-picker.component'; +import cloneDeep from 'lodash-es/cloneDeep'; @Component({ selector: 'jhi-lecture-update', templateUrl: './lecture-update.component.html', styleUrls: ['./lecture-update.component.scss'], }) -export class LectureUpdateComponent implements OnInit { +export class LectureUpdateComponent implements OnInit, OnDestroy { protected readonly documentationType: DocumentationType = 'Lecture'; protected readonly faQuestionCircle = faQuestionCircle; protected readonly faSave = faSave; protected readonly faPuzzleProcess = faPuzzlePiece; protected readonly faBan = faBan; protected readonly faHandShakeAngle = faHandshakeAngle; - // A human-readable list of allowed file extensions - protected readonly allowedFileExtensions = UPLOAD_FILE_EXTENSIONS.join(', '); - // The list of file extensions for the "accept" attribute of the file input field - protected readonly acceptedFileExtensionsFileBrowser = UPLOAD_FILE_EXTENSIONS.map((ext) => '.' + ext).join(','); + + protected readonly allowedFileExtensions = ALLOWED_FILE_EXTENSIONS_HUMAN_READABLE; + protected readonly acceptedFileExtensionsFileBrowser = ACCEPTED_FILE_EXTENSIONS_FILE_BROWSER; @ViewChild(LectureUpdateWizardComponent, { static: false }) wizardComponent: LectureUpdateWizardComponent; - lecture: Lecture; + private readonly alertService = inject(AlertService); + private readonly lectureService = inject(LectureService); + private readonly activatedRoute = inject(ActivatedRoute); + private readonly navigationUtilService = inject(ArtemisNavigationUtilService); + private readonly router = inject(Router); + + titleSection = viewChild(LectureTitleChannelNameComponent); + lecturePeriodSection = viewChild(LectureUpdatePeriodComponent); + + lecture = signal(new Lecture()); + lectureOnInit: Lecture; isSaving: boolean; isProcessing: boolean; processUnitMode: boolean; @@ -50,18 +63,41 @@ export class LectureUpdateComponent implements OnInit { toggleModeFunction = () => this.toggleWizardMode(); saveLectureFunction = () => this.save(); - constructor( - protected alertService: AlertService, - protected lectureService: LectureService, - protected courseService: CourseManagementService, - protected activatedRoute: ActivatedRoute, - private navigationUtilService: ArtemisNavigationUtilService, - private router: Router, - ) {} + isChangeMadeToTitleOrPeriodSection = false; + shouldDisplayDismissWarning = true; + + private subscriptions = new Subscription(); + + constructor() { + effect(() => { + if (this.titleSection()?.titleChannelNameComponent() && this.lecturePeriodSection()) { + this.subscriptions.add( + this.titleSection()! + .titleChannelNameComponent() + .titleChange.subscribe(() => { + this.updateIsChangesMadeToTitleOrPeriodSection(); + }), + ); + this.subscriptions.add( + this.titleSection()! + .titleChannelNameComponent() + .channelNameChange.subscribe(() => { + this.updateIsChangesMadeToTitleOrPeriodSection(); + }), + ); + this.subscriptions.add( + this.lecturePeriodSection()! + .periodSectionDatepickers() + .forEach((datepicker: FormDateTimePickerComponent) => { + datepicker.valueChange.subscribe(() => { + this.updateIsChangesMadeToTitleOrPeriodSection(); + }); + }), + ); + } + }); + } - /** - * Life cycle hook called by Angular to indicate that Angular is done creating the component - */ ngOnInit() { this.isSaving = false; this.processUnitMode = false; @@ -70,10 +106,10 @@ export class LectureUpdateComponent implements OnInit { this.activatedRoute.parent!.data.subscribe((data) => { // Create a new lecture to use unless we fetch an existing lecture const lecture = data['lecture']; - this.lecture = lecture ?? new Lecture(); + this.lecture.set(lecture ?? new Lecture()); const course = data['course']; if (course) { - this.lecture.course = course; + this.lecture().course = course; } }); @@ -82,6 +118,42 @@ export class LectureUpdateComponent implements OnInit { this.isShowingWizardMode = params.shouldBeInWizardMode; } }); + + this.lectureOnInit = cloneDeep(this.lecture()); + } + + ngOnDestroy() { + this.subscriptions.unsubscribe(); + } + + isChangeMadeToTitleSection() { + return ( + this.lecture().title !== this.lectureOnInit.title || + this.lecture().channelName !== this.lectureOnInit.channelName || + (this.lecture().description ?? '') !== (this.lectureOnInit.description ?? '') + ); + } + + isChangeMadeToPeriodSection() { + const { visibleDate, startDate, endDate } = this.lecture(); + const { visibleDate: visibleDateOnInit, startDate: startDateOnInit, endDate: endDateOnInit } = this.lectureOnInit; + + const isInvalid = (date: Dayjs | undefined) => !dayjs(date).isValid(); + const isSame = (date1: Dayjs | undefined, date2: Dayjs | undefined) => dayjs(date1).isSame(dayjs(date2)); + + const emptyVisibleDateWasCleared = !visibleDateOnInit && isInvalid(visibleDate); + const emptyStartDateWasCleared = !startDateOnInit && isInvalid(startDate); + const emptyEndDateWasCleared = !endDateOnInit && isInvalid(endDate); + + return ( + (!isSame(visibleDate, visibleDateOnInit) && !emptyVisibleDateWasCleared) || + (!isSame(startDate, startDateOnInit) && !emptyStartDateWasCleared) || + (!isSame(endDate, endDateOnInit) && !emptyEndDateWasCleared) + ); + } + + protected updateIsChangesMadeToTitleOrPeriodSection() { + this.isChangeMadeToTitleOrPeriodSection = this.isChangeMadeToTitleSection() || this.isChangeMadeToPeriodSection(); } /** @@ -90,7 +162,8 @@ export class LectureUpdateComponent implements OnInit { * Returns to the overview page if there is no previous state, and we created a new lecture */ previousState() { - this.navigationUtilService.navigateBackWithOptional(['course-management', this.lecture.course!.id!.toString(), 'lectures'], this.lecture.id?.toString()); + this.shouldDisplayDismissWarning = false; + this.navigationUtilService.navigateBackWithOptional(['course-management', this.lecture().course!.id!.toString(), 'lectures'], this.lecture().id?.toString()); } /** @@ -98,13 +171,14 @@ export class LectureUpdateComponent implements OnInit { * This function is called by pressing save after creating or editing a lecture */ save() { + this.shouldDisplayDismissWarning = false; this.isSaving = true; this.isProcessing = true; - if (this.lecture.id !== undefined) { - this.subscribeToSaveResponse(this.lectureService.update(this.lecture)); + if (this.lecture().id !== undefined) { + this.subscribeToSaveResponse(this.lectureService.update(this.lecture())); } else { // Newly created lectures must have a channel name, which cannot be undefined - this.subscribeToSaveResponse(this.lectureService.create(this.lecture)); + this.subscribeToSaveResponse(this.lectureService.create(this.lecture())); } } @@ -139,7 +213,7 @@ export class LectureUpdateComponent implements OnInit { } /** - * @callback Callback function after saving a lecture, handles appropriate action in case of error + * @callback callback after saving a lecture, handles appropriate action in case of error * @param result The Http response from the server */ protected subscribeToSaveResponse(result: Observable>) { @@ -153,11 +227,11 @@ export class LectureUpdateComponent implements OnInit { * Action on successful lecture creation or edit */ protected onSaveSuccess(lecture: Lecture) { - if (this.isShowingWizardMode && !this.lecture.id) { + if (this.isShowingWizardMode && !this.lecture().id) { this.lectureService.findWithDetails(lecture.id!).subscribe({ next: (response: HttpResponse) => { this.isSaving = false; - this.lecture = response.body!; + this.lecture.set(response.body!); this.alertService.success(`Lecture with title ${lecture.title} was successfully created.`); this.wizardComponent.onLectureCreationSucceeded(); }, @@ -165,7 +239,7 @@ export class LectureUpdateComponent implements OnInit { } else if (this.processUnitMode) { this.isSaving = false; this.isProcessing = false; - this.alertService.success(`Lecture with title ${lecture.title} was successfully ${this.lecture.id !== undefined ? 'updated' : 'created'}.`); + this.alertService.success(`Lecture with title ${lecture.title} was successfully ${this.lecture().id !== undefined ? 'updated' : 'created'}.`); this.router.navigate(['course-management', lecture.course!.id, 'lectures', lecture.id, 'unit-management', 'attachment-units', 'process'], { state: { file: this.file, fileName: this.fileName }, }); @@ -181,6 +255,7 @@ export class LectureUpdateComponent implements OnInit { */ protected onSaveError(errorRes: HttpErrorResponse) { this.isSaving = false; + if (errorRes.error && errorRes.error.title) { this.alertService.addErrorAlert(errorRes.error.title, errorRes.error.message, errorRes.error.params); } else { @@ -189,18 +264,18 @@ export class LectureUpdateComponent implements OnInit { } onDatesValuesChanged() { - const startDate = this.lecture.startDate; - const endDate = this.lecture.endDate; - const visibleDate = this.lecture.visibleDate; + const startDate = this.lecture().startDate; + const endDate = this.lecture().endDate; + const visibleDate = this.lecture().visibleDate; // Prevent endDate from being before startDate, if both dates are set if (endDate && startDate?.isAfter(endDate)) { - this.lecture.endDate = startDate.clone(); + this.lecture().endDate = startDate.clone(); } // Prevent visibleDate from being after startDate, if both dates are set if (visibleDate && startDate?.isBefore(visibleDate)) { - this.lecture.visibleDate = startDate.clone(); + this.lecture().visibleDate = startDate.clone(); } } } diff --git a/src/main/webapp/app/lecture/lecture.module.ts b/src/main/webapp/app/lecture/lecture.module.ts index 5cdad5fc5fa2..db1380abd69d 100644 --- a/src/main/webapp/app/lecture/lecture.module.ts +++ b/src/main/webapp/app/lecture/lecture.module.ts @@ -16,7 +16,6 @@ import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; import { LectureImportComponent } from 'app/lecture/lecture-import.component'; import { ArtemisCompetenciesModule } from 'app/course/competencies/competency.module'; import { LectureUpdateWizardTitleComponent } from 'app/lecture/wizard-mode/lecture-wizard-title.component'; -import { LectureUpdateWizardPeriodComponent } from 'app/lecture/wizard-mode/lecture-wizard-period.component'; import { LectureUpdateWizardAttachmentsComponent } from 'app/lecture/wizard-mode/lecture-wizard-attachments.component'; import { LectureUpdateWizardUnitsComponent } from 'app/lecture/wizard-mode/lecture-wizard-units.component'; import { LectureUpdateWizardStepComponent } from 'app/lecture/wizard-mode/lecture-update-wizard-step.component'; @@ -24,6 +23,7 @@ import { TitleChannelNameModule } from 'app/shared/form/title-channel-name/title import { LectureTitleChannelNameComponent } from 'app/lecture/lecture-title-channel-name.component'; import { DetailModule } from 'app/detail-overview-list/detail.module'; import { CompetencyFormComponent } from 'app/course/competencies/forms/competency/competency-form.component'; +import { LectureUpdatePeriodComponent } from 'app/lecture/lecture-period/lecture-period.component'; const ENTITY_STATES = [...lectureRoute]; @@ -49,7 +49,7 @@ const ENTITY_STATES = [...lectureRoute]; LectureUpdateWizardComponent, LectureAttachmentsComponent, LectureUpdateWizardTitleComponent, - LectureUpdateWizardPeriodComponent, + LectureUpdatePeriodComponent, LectureUpdateWizardAttachmentsComponent, LectureUpdateWizardUnitsComponent, LectureUpdateWizardStepComponent, diff --git a/src/main/webapp/app/lecture/lecture.route.ts b/src/main/webapp/app/lecture/lecture.route.ts index 11850d96840f..aa36380de4a1 100644 --- a/src/main/webapp/app/lecture/lecture.route.ts +++ b/src/main/webapp/app/lecture/lecture.route.ts @@ -17,6 +17,7 @@ import { CourseManagementTabBarComponent } from 'app/course/manage/course-manage import { PdfPreviewComponent } from 'app/lecture/pdf-preview/pdf-preview.component'; import { Attachment } from 'app/entities/attachment.model'; import { AttachmentService } from 'app/lecture/attachment.service'; +import { hasLectureUnsavedChangesGuard } from './hasLectureUnsavedChanges.guard'; @Injectable({ providedIn: 'root' }) export class LectureResolve implements Resolve { @@ -132,6 +133,7 @@ export const lectureRoute: Routes = [ pageTitle: 'global.generic.edit', }, canActivate: [UserRouteAccessService], + canDeactivate: [hasLectureUnsavedChangesGuard], }, ...lectureUnitRoute, ], diff --git a/src/main/webapp/app/lecture/wizard-mode/lecture-update-wizard.component.html b/src/main/webapp/app/lecture/wizard-mode/lecture-update-wizard.component.html index cb704901206e..1e228e1c0747 100644 --- a/src/main/webapp/app/lecture/wizard-mode/lecture-update-wizard.component.html +++ b/src/main/webapp/app/lecture/wizard-mode/lecture-update-wizard.component.html @@ -3,7 +3,7 @@ } @if (currentStep >= LECTURE_UPDATE_WIZARD_PERIOD_STEP) { - + } @if (currentStep >= LECTURE_UPDATE_WIZARD_ATTACHMENT_STEP) { diff --git a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-period.component.ts b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-period.component.ts deleted file mode 100644 index 7d9b48571621..000000000000 --- a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-period.component.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { Lecture } from 'app/entities/lecture.model'; - -@Component({ - selector: 'jhi-lecture-update-wizard-period', - templateUrl: './lecture-wizard-period.component.html', -}) -export class LectureUpdateWizardPeriodComponent { - @Input() currentStep: number; - @Input() lecture: Lecture; - @Input() validateDatesFunction: () => void; -} diff --git a/src/main/webapp/app/shared/form/title-channel-name/title-channel-name.component.ts b/src/main/webapp/app/shared/form/title-channel-name/title-channel-name.component.ts index 269e3a7e84e4..b7f139bc5182 100644 --- a/src/main/webapp/app/shared/form/title-channel-name/title-channel-name.component.ts +++ b/src/main/webapp/app/shared/form/title-channel-name/title-channel-name.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild, computed, effect, input, signal, viewChild } from '@angular/core'; +import { AfterViewInit, Component, Input, OnDestroy, OnInit, ViewChild, computed, effect, input, output, signal, viewChild } from '@angular/core'; import { ControlContainer, NgForm, NgModel } from '@angular/forms'; import { Subject, Subscription } from 'rxjs'; import { ProgrammingExerciseInputField } from 'app/exercises/programming/manage/update/programming-exercise-update.helper'; @@ -26,8 +26,8 @@ export class TitleChannelNameComponent implements AfterViewInit, OnDestroy, OnIn @ViewChild('field_title') field_title: NgModel; field_channel_name = viewChild('field_channel_name'); - @Output() titleChange = new EventEmitter(); - @Output() channelNameChange = new EventEmitter(); + titleChange = output(); + channelNameChange = output(); isFormValidSignal = signal(false); /** diff --git a/src/main/webapp/i18n/de/lecture.json b/src/main/webapp/i18n/de/lecture.json index e5ecd6649b38..ede4e7eaeb02 100644 --- a/src/main/webapp/i18n/de/lecture.json +++ b/src/main/webapp/i18n/de/lecture.json @@ -80,9 +80,7 @@ "attachmentsStepTitle": "Anhänge", "attachmentsStepMessage": "Lade Anhänge für die Vorlesung hoch.", "unitsStepTitle": "Vorlesungseinheiten", - "unitsStepMessage": "Füge Inhalte zur Vorlesung hinzu durch Erstellung von Vorlesungseinheiten.", - "competenciesStepTitle": "Kompetenzen", - "competenciesStepMessage": "Verknüpfe die Einheiten dieser Vorlesung mit Kompetenzen, um zu zeigen, welche Kompetenzen Studierende erreichen werden, wenn sie die Einheit abschließen." + "unitsStepMessage": "Füge Inhalte zur Vorlesung hinzu, indem du Vorlesungseinheiten erstellst." }, "newLectureUnit": "Neue Vorlesungseinheit", "editLectureUnit": "Vorlesungseinheit bearbeiten", @@ -91,6 +89,12 @@ "competencyTitle": "Titel", "competencyConnectedUnits": "Verknüpfte Einheiten", "competencyNoConnectedUnits": "Keine verknüpften Einheiten" + }, + "dismissChangesModal": { + "title": "Ungespeicherte Änderungen der Vorlesung verwerfen?", + "message": "Bist du sicher, dass du die ungespeicherten Änderungen verwerfen willst?", + "sectionsThatContainUnsavedChangesSingular": "Der folgende Abschnitt enthält ungespeicherte Änderungen:", + "sectionsThatContainUnsavedChangesPlural": "Die folgenden Abschnitte enthalten ungespeicherte Änderungen:" } }, "attachment": { diff --git a/src/main/webapp/i18n/en/lecture.json b/src/main/webapp/i18n/en/lecture.json index 284c7d83ace3..d93f7e5f5779 100644 --- a/src/main/webapp/i18n/en/lecture.json +++ b/src/main/webapp/i18n/en/lecture.json @@ -80,9 +80,7 @@ "attachmentsStepTitle": "Attachments", "attachmentsStepMessage": "Upload attachments to this lecture.", "unitsStepTitle": "Units", - "unitsStepMessage": "Add content to the lecture by creating different kinds of lecture units.", - "competenciesStepTitle": "Competencies", - "competenciesStepMessage": "Make it easily visible what knowledge students will achieve when completing the units of this lecture by connecting them to competencies." + "unitsStepMessage": "Add content to the lecture by creating different kinds of lecture units." }, "newLectureUnit": "New Lecture Unit", "editLectureUnit": "Edit Lecture Unit", @@ -91,6 +89,12 @@ "competencyTitle": "Title", "competencyConnectedUnits": "Connected Units", "competencyNoConnectedUnits": "No connected units" + }, + "dismissChangesModal": { + "title": "Discard unsaved lecture changes", + "message": "Are you sure you want to discard your unsaved changes?", + "sectionsThatContainUnsavedChangesSingular": "The following section contains unsaved changes:", + "sectionsThatContainUnsavedChangesPlural": "The following sections contain unsaved changes:" } }, "attachment": { diff --git a/src/test/javascript/spec/component/lecture/close-edit-lecture-modal.component.spec.ts b/src/test/javascript/spec/component/lecture/close-edit-lecture-modal.component.spec.ts new file mode 100644 index 000000000000..93b92f1d9627 --- /dev/null +++ b/src/test/javascript/spec/component/lecture/close-edit-lecture-modal.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArtemisTestModule } from '../../test.module'; +import { CloseEditLectureModalComponent } from '../../../../../main/webapp/app/lecture/close-edit-lecture-dialog/close-edit-lecture-modal.component'; + +describe('CloseEditLectureModalComponent', () => { + let component: CloseEditLectureModalComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ArtemisTestModule, CloseEditLectureModalComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(CloseEditLectureModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/test/javascript/spec/component/lecture/hasLectureUnsavedChanges.guard.spec.ts b/src/test/javascript/spec/component/lecture/hasLectureUnsavedChanges.guard.spec.ts new file mode 100644 index 000000000000..8c2b66b14ada --- /dev/null +++ b/src/test/javascript/spec/component/lecture/hasLectureUnsavedChanges.guard.spec.ts @@ -0,0 +1,95 @@ +import { ActivatedRouteSnapshot, GuardResult, MaybeAsync, Router, RouterStateSnapshot } from '@angular/router'; +import { hasLectureUnsavedChangesGuard } from '../../../../../main/webapp/app/lecture/hasLectureUnsavedChanges.guard'; +import { LectureUpdateComponent } from '../../../../../main/webapp/app/lecture/lecture-update.component'; +import { TestBed } from '@angular/core/testing'; +import { MockRouter } from '../../helpers/mocks/mock-router'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { Observable, firstValueFrom, of } from 'rxjs'; +import { MockNgbModalService } from '../../helpers/mocks/service/mock-ngb-modal.service'; + +describe('hasLectureUnsavedChanges', () => { + let component: LectureUpdateComponent; + let currentRoute: ActivatedRouteSnapshot; + let currentState: RouterStateSnapshot; + let nextState: RouterStateSnapshot; + let mockNgbModal: NgbModal; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + declarations: [LectureUpdateComponent], + providers: [ + { provide: Router, useClass: MockRouter }, + { provide: NgbModal, useClass: MockNgbModalService }, + { + provide: LectureUpdateComponent, + useValue: { + shouldDisplayDismissWarning: true, + isShowingWizardMode: false, + isChangeMadeToTitleSection: jest.fn().mockReturnValue(true), + isChangeMadeToPeriodSection: jest.fn().mockReturnValue(true), + isChangeMadeToTitleOrPeriodSection: true, + }, + }, + ], + }).compileComponents(); + + component = TestBed.inject(LectureUpdateComponent); + mockNgbModal = TestBed.inject(NgbModal); + const mockModalRef = { + componentInstance: {}, + result: Promise.resolve(true), + }; + jest.spyOn(mockNgbModal, 'open').mockReturnValue(mockModalRef as NgbModalRef); + + currentRoute = {} as ActivatedRouteSnapshot; + currentState = {} as RouterStateSnapshot; + nextState = {} as RouterStateSnapshot; + }); + + it('should return true if warning is not bypassed by shouldDisplayDismissWarning variable but no changes were made', async () => { + component.shouldDisplayDismissWarning = true; + component.isChangeMadeToTitleOrPeriodSection = false; + + const result = await firstValueFrom(getGuardResultAsObservable(hasLectureUnsavedChangesGuard(component, currentRoute, currentState, nextState))); + expect(result).toBeTrue(); + }); + + it('should return true if dismiss warning shall not be displayed', async () => { + component.shouldDisplayDismissWarning = false; + component.isChangeMadeToTitleOrPeriodSection = true; + + const result = await firstValueFrom(getGuardResultAsObservable(hasLectureUnsavedChangesGuard(component, currentRoute, currentState, nextState))); + expect(result).toBeTrue(); + }); + + it('should return result from modal (true, dismiss changes)', async () => { + component.shouldDisplayDismissWarning = true; + + const result = await TestBed.runInInjectionContext(() => { + return firstValueFrom(getGuardResultAsObservable(hasLectureUnsavedChangesGuard(component, currentRoute, currentState, nextState))); + }); + + expect(result).toBeTrue(); + }); + + it('should return result from modal (false, keep editing)', async () => { + component.shouldDisplayDismissWarning = true; + + const mockModalRef = { + componentInstance: {}, + result: Promise.resolve(false), + }; + jest.spyOn(mockNgbModal, 'open').mockReturnValue(mockModalRef as NgbModalRef); + + const result = await TestBed.runInInjectionContext(() => { + return firstValueFrom(getGuardResultAsObservable(hasLectureUnsavedChangesGuard(component, currentRoute, currentState, nextState))); + }); + + expect(result).toBeFalse(); + }); + + function getGuardResultAsObservable(guardResult: MaybeAsync): Observable> { + return guardResult instanceof Observable ? guardResult : of(guardResult); + } +}); diff --git a/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard-period.component.spec.ts b/src/test/javascript/spec/component/lecture/lecture-period.component.spec.ts similarity index 54% rename from src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard-period.component.spec.ts rename to src/test/javascript/spec/component/lecture/lecture-period.component.spec.ts index f06067251d59..1f6309915829 100644 --- a/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard-period.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/lecture-period.component.spec.ts @@ -1,27 +1,28 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormDateTimePickerComponent } from 'app/shared/date-time-picker/date-time-picker.component'; +import { FormDateTimePickerComponent } from '../../../../../main/webapp/app/shared/date-time-picker/date-time-picker.component'; import { MockComponent, MockPipe } from 'ng-mocks'; -import { Lecture } from 'app/entities/lecture.model'; -import { LectureUpdateWizardPeriodComponent } from 'app/lecture/wizard-mode/lecture-wizard-period.component'; -import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { Lecture } from '../../../../../main/webapp/app/entities/lecture.model'; +import { ArtemisTranslatePipe } from '../../../../../main/webapp/app/shared/pipes/artemis-translate.pipe'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { LectureUpdatePeriodComponent } from '../../../../../main/webapp/app/lecture/lecture-period/lecture-period.component'; describe('LectureWizardPeriodComponent', () => { - let wizardPeriodComponentFixture: ComponentFixture; - let wizardPeriodComponent: LectureUpdateWizardPeriodComponent; + let wizardPeriodComponentFixture: ComponentFixture; + let wizardPeriodComponent: LectureUpdatePeriodComponent; beforeEach(() => { TestBed.configureTestingModule({ imports: [ReactiveFormsModule, FormsModule], - declarations: [LectureUpdateWizardPeriodComponent, MockPipe(ArtemisTranslatePipe), MockComponent(FormDateTimePickerComponent)], + declarations: [LectureUpdatePeriodComponent, MockPipe(ArtemisTranslatePipe), MockComponent(FormDateTimePickerComponent)], providers: [], schemas: [], }) .compileComponents() .then(() => { - wizardPeriodComponentFixture = TestBed.createComponent(LectureUpdateWizardPeriodComponent); + wizardPeriodComponentFixture = TestBed.createComponent(LectureUpdatePeriodComponent); wizardPeriodComponent = wizardPeriodComponentFixture.componentInstance; - wizardPeriodComponent.lecture = new Lecture(); + + wizardPeriodComponentFixture.componentRef.setInput('lecture', new Lecture()); }); }); diff --git a/src/test/javascript/spec/component/lecture/lecture-update.component.spec.ts b/src/test/javascript/spec/component/lecture/lecture-update.component.spec.ts index 03be222b4cb4..384ec2002b9c 100644 --- a/src/test/javascript/spec/component/lecture/lecture-update.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/lecture-update.component.spec.ts @@ -14,7 +14,7 @@ import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { HtmlForMarkdownPipe } from 'app/shared/pipes/html-for-markdown.pipe'; import dayjs from 'dayjs/esm'; -import { MockComponent, MockModule, MockPipe } from 'ng-mocks'; +import { MockComponent, MockDirective, MockModule, MockPipe } from 'ng-mocks'; import { of } from 'rxjs'; import { MockRouterLinkDirective } from '../../helpers/mocks/directive/mock-router-link.directive'; import { MockRouter } from '../../helpers/mocks/mock-router'; @@ -23,6 +23,11 @@ import { ArtemisTestModule } from '../../test.module'; import { DocumentationButtonComponent } from 'app/shared/components/documentation-button/documentation-button.component'; import { LectureTitleChannelNameComponent } from 'app/lecture/lecture-title-channel-name.component'; import { MarkdownEditorMonacoComponent } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; +import { CustomNotIncludedInValidatorDirective } from '../../../../../main/webapp/app/shared/validators/custom-not-included-in-validator.directive'; +import { OwlDateTimeModule } from '@danielmoncada/angular-datetime-picker'; +import { TitleChannelNameComponent } from '../../../../../main/webapp/app/shared/form/title-channel-name/title-channel-name.component'; +import { LectureUpdatePeriodComponent } from '../../../../../main/webapp/app/lecture/lecture-period/lecture-period.component'; +import { LectureUnitManagementComponent } from '../../../../../main/webapp/app/lecture/lecture-unit/lecture-unit-management/lecture-unit-management.component'; describe('LectureUpdateComponent', () => { let lectureUpdateWizardComponentFixture: ComponentFixture; @@ -45,18 +50,22 @@ describe('LectureUpdateComponent', () => { pastLecture.endDate = yesterday; TestBed.configureTestingModule({ - imports: [ArtemisTestModule, FormsModule, MockModule(NgbTooltipModule)], + imports: [ArtemisTestModule, FormsModule, MockModule(NgbTooltipModule), MockModule(OwlDateTimeModule)], declarations: [ LectureUpdateComponent, - MockComponent(LectureTitleChannelNameComponent), + LectureTitleChannelNameComponent, + TitleChannelNameComponent, + FormDateTimePickerComponent, + LectureUpdatePeriodComponent, MockComponent(LectureUpdateWizardComponent), - MockComponent(FormDateTimePickerComponent), + MockComponent(LectureUnitManagementComponent), MockComponent(MarkdownEditorMonacoComponent), MockComponent(DocumentationButtonComponent), MockPipe(ArtemisTranslatePipe), MockPipe(ArtemisDatePipe), MockPipe(HtmlForMarkdownPipe), MockRouterLinkDirective, + MockDirective(CustomNotIncludedInValidatorDirective), ], providers: [ { provide: TranslateService, useClass: MockTranslateService }, @@ -97,8 +106,8 @@ describe('LectureUpdateComponent', () => { jest.restoreAllMocks(); }); - it('should create lecture', fakeAsync(() => { - lectureUpdateComponent.lecture = { title: 'test1', channelName: 'test1' } as Lecture; + it('should create lecture', () => { + lectureUpdateComponent.lecture.set({ title: 'test1', channelName: 'test1' } as Lecture); const navigateSpy = jest.spyOn(router, 'navigate'); const createSpy = jest.spyOn(lectureService, 'create').mockReturnValue( @@ -116,7 +125,6 @@ describe('LectureUpdateComponent', () => { ); lectureUpdateComponent.save(); - tick(); lectureUpdateComponentFixture.detectChanges(); const expectedPath = ['course-management', 1, 'lectures', 3]; @@ -124,10 +132,10 @@ describe('LectureUpdateComponent', () => { expect(createSpy).toHaveBeenCalledOnce(); expect(createSpy).toHaveBeenCalledWith({ title: 'test1', channelName: 'test1' }); - })); + }); it('should create lecture in wizard mode', () => { - lectureUpdateComponent.lecture = { title: '', channelName: '' } as Lecture; + lectureUpdateComponent.lecture.set({ title: '', channelName: '' } as Lecture); lectureUpdateComponent.isShowingWizardMode = true; lectureUpdateComponent.wizardComponent = lectureUpdateWizardComponent; @@ -173,7 +181,7 @@ describe('LectureUpdateComponent', () => { activatedRoute.parent!.data = of({ course: { id: 1 }, lecture: { id: 6 } }); lectureUpdateComponentFixture.detectChanges(); - lectureUpdateComponent.lecture = { id: 6, title: 'test1Updated', channelName: 'test1Updated' } as Lecture; + lectureUpdateComponent.lecture.set({ id: 6, title: 'test1Updated', channelName: 'test1Updated' } as Lecture); const updateSpy = jest.spyOn(lectureService, 'update').mockReturnValue( of>( @@ -249,7 +257,7 @@ describe('LectureUpdateComponent', () => { lectureUpdateComponent.file = new File([''], 'testFile.pdf', { type: 'application/pdf' }); lectureUpdateComponent.fileName = 'testFile'; lectureUpdateComponent.processUnitMode = true; - lectureUpdateComponent.lecture = { title: 'test1', channelName: 'test1' } as Lecture; + lectureUpdateComponent.lecture.set({ title: 'test1', channelName: 'test1' } as Lecture); const navigateSpy = jest.spyOn(router, 'navigate'); const createSpy = jest.spyOn(lectureService, 'create').mockReturnValue( @@ -279,7 +287,7 @@ describe('LectureUpdateComponent', () => { expect(navigateSpy).toHaveBeenCalledWith(expectedPath, { state: { file: lectureUpdateComponent.file, fileName: lectureUpdateComponent.fileName } }); })); - it('should call onFileChange on changed file', fakeAsync(() => { + it('should call onFileChange on changed file', () => { lectureUpdateComponent.processUnitMode = false; lectureUpdateComponentFixture.detectChanges(); expect(lectureUpdateComponentFixture.debugElement.nativeElement.querySelector('#fileInput')).toBeFalsy(); @@ -295,52 +303,117 @@ describe('LectureUpdateComponent', () => { expect(lectureUpdateComponentFixture.debugElement.nativeElement.querySelector('#fileInput')).toBeTruthy(); fileInput.dispatchEvent(new Event('change')); expect(onFileChangeStub).toHaveBeenCalledOnce(); - })); + }); it('should set lecture visible date, start date and end date correctly', fakeAsync(() => { activatedRoute = TestBed.inject(ActivatedRoute); activatedRoute.parent!.data = of({ course: { id: 1 }, lecture: { id: 6 } }); lectureUpdateComponentFixture.detectChanges(); - lectureUpdateComponent.lecture = { id: 6, title: 'test1Updated' } as Lecture; + lectureUpdateComponent.lecture.set({ id: 6, title: 'test1Updated' } as Lecture); const setDatesSpy = jest.spyOn(lectureUpdateComponent, 'onDatesValuesChanged'); - lectureUpdateComponent.lecture.visibleDate = dayjs().year(2022).month(3).date(7); - lectureUpdateComponent.lecture.startDate = dayjs().year(2022).month(3).date(5); - lectureUpdateComponent.lecture.endDate = dayjs().year(2022).month(3).date(1); + lectureUpdateComponent.lecture().visibleDate = dayjs().year(2022).month(3).date(7); + lectureUpdateComponent.lecture().startDate = dayjs().year(2022).month(3).date(5); + lectureUpdateComponent.lecture().endDate = dayjs().year(2022).month(3).date(1); lectureUpdateComponent.onDatesValuesChanged(); expect(setDatesSpy).toHaveBeenCalledOnce(); - expect(lectureUpdateComponent.lecture.startDate).toEqual(lectureUpdateComponent.lecture.endDate); - expect(lectureUpdateComponent.lecture.startDate).toEqual(lectureUpdateComponent.lecture.visibleDate); + expect(lectureUpdateComponent.lecture().startDate).toEqual(lectureUpdateComponent.lecture().endDate); + expect(lectureUpdateComponent.lecture().startDate).toEqual(lectureUpdateComponent.lecture().visibleDate); lectureUpdateComponentFixture.detectChanges(); tick(); - lectureUpdateComponent.lecture.startDate = undefined; - lectureUpdateComponent.lecture.endDate = undefined; - lectureUpdateComponent.lecture.visibleDate = undefined; + lectureUpdateComponent.lecture().startDate = undefined; + lectureUpdateComponent.lecture().endDate = undefined; + lectureUpdateComponent.lecture().visibleDate = undefined; lectureUpdateComponent.onDatesValuesChanged(); expect(setDatesSpy).toHaveBeenCalledTimes(2); - expect(lectureUpdateComponent.lecture.startDate).toBeUndefined(); - expect(lectureUpdateComponent.lecture.endDate).toBeUndefined(); - expect(lectureUpdateComponent.lecture.visibleDate).toBeUndefined(); + expect(lectureUpdateComponent.lecture().startDate).toBeUndefined(); + expect(lectureUpdateComponent.lecture().endDate).toBeUndefined(); + expect(lectureUpdateComponent.lecture().visibleDate).toBeUndefined(); lectureUpdateComponentFixture.detectChanges(); tick(); - lectureUpdateComponent.lecture.visibleDate = dayjs().year(2022).month(1).date(1); - lectureUpdateComponent.lecture.startDate = dayjs().year(2022).month(1).date(2); - lectureUpdateComponent.lecture.endDate = dayjs().year(2022).month(1).date(3); + lectureUpdateComponent.lecture().visibleDate = dayjs().year(2022).month(1).date(1); + lectureUpdateComponent.lecture().startDate = dayjs().year(2022).month(1).date(2); + lectureUpdateComponent.lecture().endDate = dayjs().year(2022).month(1).date(3); lectureUpdateComponent.onDatesValuesChanged(); expect(setDatesSpy).toHaveBeenCalledTimes(3); - expect(lectureUpdateComponent.lecture.visibleDate.toDate()).toBeBefore(lectureUpdateComponent.lecture.startDate.toDate()); - expect(lectureUpdateComponent.lecture.startDate.toDate()).toBeBefore(lectureUpdateComponent.lecture.endDate.toDate()); + if (lectureUpdateComponent.lecture().visibleDate && lectureUpdateComponent.lecture().startDate) { + expect(lectureUpdateComponent.lecture().visibleDate!.toDate()).toBeBefore(lectureUpdateComponent.lecture().startDate!.toDate()); + } else { + throw new Error('visibleDate and startDate should not be undefined'); + } + + if (lectureUpdateComponent.lecture().startDate && lectureUpdateComponent.lecture().endDate) { + expect(lectureUpdateComponent.lecture().startDate!.toDate()).toBeBefore(lectureUpdateComponent.lecture().endDate!.toDate()); + } else { + throw new Error('startDate and endDate should not be undefined'); + } })); + + describe('isChangeMadeToTitleSection', () => { + it('should detect changes made to the title section', () => { + lectureUpdateComponent.lecture.set({ title: 'new title', channelName: 'new channel', description: 'new description' } as Lecture); + lectureUpdateComponent.lectureOnInit = { title: 'old title', channelName: 'old channel', description: 'old description' } as Lecture; + expect(lectureUpdateComponent.isChangeMadeToTitleSection()).toBeTrue(); + + lectureUpdateComponent.lecture.set({ + title: lectureUpdateComponent.lectureOnInit.title, + channelName: lectureUpdateComponent.lectureOnInit.channelName, + description: lectureUpdateComponent.lectureOnInit.description, + } as Lecture); + expect(lectureUpdateComponent.isChangeMadeToTitleSection()).toBeFalse(); + }); + + it('should handle undefined from description properly', () => { + lectureUpdateComponent.lecture.set({ title: 'new title', channelName: 'new channel', description: 'new description' } as Lecture); + lectureUpdateComponent.lectureOnInit = { title: 'old title', channelName: 'old channel', description: undefined } as Lecture; + expect(lectureUpdateComponent.isChangeMadeToTitleSection()).toBeTrue(); + + lectureUpdateComponent.lecture.set({ + title: lectureUpdateComponent.lectureOnInit.title, + channelName: lectureUpdateComponent.lectureOnInit.channelName, + description: '', // will be an empty string if the user clears the input, but was loaded with undefined in that case + } as Lecture); + expect(lectureUpdateComponent.isChangeMadeToTitleSection()).toBeFalse(); + }); + }); + + describe('isChangeMadeToPeriodSection', () => { + it('should detect changes made to the period section', () => { + lectureUpdateComponent.lecture.set({ visibleDate: dayjs().add(1, 'day'), startDate: dayjs().add(2, 'day'), endDate: dayjs().add(3, 'day') } as Lecture); + lectureUpdateComponent.lectureOnInit = { visibleDate: dayjs(), startDate: dayjs(), endDate: dayjs() } as Lecture; + expect(lectureUpdateComponent.isChangeMadeToPeriodSection()).toBeTrue(); + + lectureUpdateComponent.lecture.set({ + visibleDate: lectureUpdateComponent.lectureOnInit.visibleDate, + startDate: lectureUpdateComponent.lectureOnInit.startDate, + endDate: lectureUpdateComponent.lectureOnInit.endDate, + } as Lecture); + expect(lectureUpdateComponent.isChangeMadeToPeriodSection()).toBeFalse(); + }); + + it('should not consider resetting an undefined date as a change', () => { + lectureUpdateComponent.lecture.set({ visibleDate: dayjs().add(1, 'day'), startDate: dayjs().add(2, 'day'), endDate: dayjs().add(3, 'day') } as Lecture); + lectureUpdateComponent.lectureOnInit = { visibleDate: undefined, startDate: undefined, endDate: undefined } as Lecture; + expect(lectureUpdateComponent.isChangeMadeToPeriodSection()).toBeTrue(); + + lectureUpdateComponent.lecture.set({ + visibleDate: dayjs('undefined'), + startDate: dayjs('undefined'), + endDate: dayjs('undefined'), + } as Lecture); + expect(lectureUpdateComponent.isChangeMadeToPeriodSection()).toBeFalse(); + }); + }); }); diff --git a/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard-title.component.spec.ts b/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard-title.component.spec.ts index 36701004edbb..de4b7fc35dc7 100644 --- a/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard-title.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard-title.component.spec.ts @@ -1,10 +1,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { LectureUpdateWizardTitleComponent } from 'app/lecture/wizard-mode/lecture-wizard-title.component'; import { Lecture } from 'app/entities/lecture.model'; -import { MockComponent } from 'ng-mocks'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { LectureTitleChannelNameComponent } from 'app/lecture/lecture-title-channel-name.component'; -import { MarkdownEditorMonacoComponent } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; +import { MockComponent, MockDirective, MockModule } from 'ng-mocks'; +import { FormsModule } from '@angular/forms'; +import { MarkdownEditorMonacoComponent } from '../../../../../../main/webapp/app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; +import { LectureTitleChannelNameComponent } from '../../../../../../main/webapp/app/lecture/lecture-title-channel-name.component'; +import { CustomNotIncludedInValidatorDirective } from '../../../../../../main/webapp/app/shared/validators/custom-not-included-in-validator.directive'; +import { TitleChannelNameComponent } from '../../../../../../main/webapp/app/shared/form/title-channel-name/title-channel-name.component'; describe('LectureWizardTitleComponent', () => { let wizardTitleComponentFixture: ComponentFixture; @@ -12,8 +14,14 @@ describe('LectureWizardTitleComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ReactiveFormsModule, FormsModule], - declarations: [LectureUpdateWizardTitleComponent, MockComponent(MarkdownEditorMonacoComponent), MockComponent(LectureTitleChannelNameComponent)], + imports: [MockModule(FormsModule)], + declarations: [ + LectureUpdateWizardTitleComponent, + LectureTitleChannelNameComponent, + TitleChannelNameComponent, + MockComponent(MarkdownEditorMonacoComponent), + MockDirective(CustomNotIncludedInValidatorDirective), + ], providers: [], schemas: [], }) diff --git a/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard.component.spec.ts b/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard.component.spec.ts index 716de13900a0..e01eea78ab89 100644 --- a/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard.component.spec.ts @@ -1,5 +1,5 @@ -import { ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing'; -import { MockComponent, MockDirective, MockPipe, MockProvider } from 'ng-mocks'; +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { MockComponent, MockModule, MockProvider } from 'ng-mocks'; import { ActivatedRoute, Router } from '@angular/router'; import { MockRouter } from '../../../helpers/mocks/mock-router'; import { of } from 'rxjs'; @@ -7,18 +7,18 @@ import { Lecture } from 'app/entities/lecture.model'; import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; import { TranslateService } from '@ngx-translate/core'; import { Course } from 'app/entities/course.model'; -import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { LectureUpdateWizardComponent } from 'app/lecture/wizard-mode/lecture-update-wizard.component'; import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { LectureUpdateWizardStepComponent } from 'app/lecture/wizard-mode/lecture-update-wizard-step.component'; import { LectureUpdateWizardUnitsComponent } from 'app/lecture/wizard-mode/lecture-wizard-units.component'; import { LectureUpdateWizardAttachmentsComponent } from 'app/lecture/wizard-mode/lecture-wizard-attachments.component'; -import { LectureUpdateWizardPeriodComponent } from 'app/lecture/wizard-mode/lecture-wizard-period.component'; import { LectureUpdateWizardTitleComponent } from 'app/lecture/wizard-mode/lecture-wizard-title.component'; -import { TranslateDirective } from 'app/shared/language/translate.directive'; -import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import dayjs from 'dayjs/esm'; +import { LectureUpdatePeriodComponent } from '../../../../../../main/webapp/app/lecture/lecture-period/lecture-period.component'; +import { ArtemisTestModule } from '../../../test.module'; +import { ArtemisSharedModule } from '../../../../../../main/webapp/app/shared/shared.module'; +import { FormDateTimePickerComponent } from '../../../../../../main/webapp/app/shared/date-time-picker/date-time-picker.component'; describe('LectureWizardComponent', () => { let wizardComponentFixture: ComponentFixture; @@ -26,17 +26,15 @@ describe('LectureWizardComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [], + imports: [ArtemisTestModule, MockModule(ArtemisSharedModule)], declarations: [ LectureUpdateWizardComponent, - MockPipe(ArtemisTranslatePipe), + LectureUpdatePeriodComponent, + MockComponent(FormDateTimePickerComponent), + MockComponent(LectureUpdateWizardTitleComponent), MockComponent(LectureUpdateWizardStepComponent), MockComponent(LectureUpdateWizardUnitsComponent), MockComponent(LectureUpdateWizardAttachmentsComponent), - MockComponent(LectureUpdateWizardPeriodComponent), - MockComponent(LectureUpdateWizardTitleComponent), - MockComponent(FaIconComponent), - MockDirective(TranslateDirective), ], providers: [ MockProvider(ArtemisNavigationUtilService), @@ -83,6 +81,8 @@ describe('LectureWizardComponent', () => { wizardComponentFixture.detectChanges(); expect(wizardComponent).not.toBeNull(); + tick(); + wizardComponentFixture.whenStable().then(() => { expect(wizardComponent.currentStep).toBe(1); }); @@ -95,6 +95,8 @@ describe('LectureWizardComponent', () => { wizardComponentFixture.detectChanges(); expect(wizardComponent).not.toBeNull(); + tick(); + wizardComponentFixture.whenStable().then(() => { expect(wizardComponent.currentStep).toBe(2); }); From d10a20cde78091af6d2a9b7ce81818bafd0c8782 Mon Sep 17 00:00:00 2001 From: Florian Glombik <63976129+florian-glombik@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:07:37 +0100 Subject: [PATCH 4/5] Lectures: Remove guided mode exercise creation shortcut (#10022) --- ...course-management-exercises.component.html | 5 ---- .../course-management-exercises.component.ts | 30 ++----------------- .../file-upload-exercise-update.component.ts | 21 ++----------- .../modeling-exercise-update.component.ts | 16 +--------- .../manage/quiz-exercise-update.component.ts | 13 -------- .../text-exercise-update.component.ts | 13 -------- .../create-exercise-unit.component.html | 9 ------ .../create-exercise-unit.component.ts | 9 ------ .../lecture-wizard-units.component.html | 2 -- src/main/webapp/i18n/de/lecture.json | 1 - src/main/webapp/i18n/en/lecture.json | 1 - .../text-exercise-update.component.spec.ts | 1 - 12 files changed, 7 insertions(+), 114 deletions(-) diff --git a/src/main/webapp/app/course/manage/course-management-exercises.component.html b/src/main/webapp/app/course/manage/course-management-exercises.component.html index d267f2b04fa4..51f7c1ecfb10 100644 --- a/src/main/webapp/app/course/manage/course-management-exercises.component.html +++ b/src/main/webapp/app/course/manage/course-management-exercises.component.html @@ -10,11 +10,6 @@

- @if (showBackToWizardModeButton) { - - } diff --git a/src/main/webapp/app/course/manage/course-management-exercises.component.ts b/src/main/webapp/app/course/manage/course-management-exercises.component.ts index b22fa31fa819..70afc4bd3471 100644 --- a/src/main/webapp/app/course/manage/course-management-exercises.component.ts +++ b/src/main/webapp/app/course/manage/course-management-exercises.component.ts @@ -1,10 +1,8 @@ -import { Component, ContentChild, OnInit, TemplateRef } from '@angular/core'; +import { Component, ContentChild, OnInit, TemplateRef, inject } from '@angular/core'; import { Course } from 'app/entities/course.model'; -import { CourseManagementService } from './course-management.service'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; import { ExerciseFilter } from 'app/entities/exercise-filter.model'; import { DocumentationType } from 'app/shared/components/documentation-button/documentation-button.component'; -import { faHandshakeAngle } from '@fortawesome/free-solid-svg-icons'; import { ExerciseType } from 'app/entities/exercise.model'; @Component({ @@ -28,22 +26,13 @@ export class CourseManagementExercisesComponent implements OnInit { filteredModelingExercisesCount = 0; filteredFileUploadExercisesCount = 0; exerciseFilter: ExerciseFilter; - showBackToWizardModeButton = false; - lectureIdForGoingBack: number; - lectureWizardStepForGoingBack: number; - - faHandshakeAngle = faHandshakeAngle; // extension points, see shared/extension-point @ContentChild('overrideGenerateAndImportButton') overrideGenerateAndImportButton: TemplateRef; @ContentChild('overrideProgrammingExerciseCard') overrideProgrammingExerciseCard: TemplateRef; @ContentChild('overrideNonProgrammingExerciseCard') overrideNonProgrammingExerciseCard: TemplateRef; - constructor( - private courseService: CourseManagementService, - private router: Router, - private route: ActivatedRoute, - ) {} + private readonly route = inject(ActivatedRoute); /** * initializes course @@ -55,12 +44,6 @@ export class CourseManagementExercisesComponent implements OnInit { } }); - this.route.queryParams.subscribe((params) => { - this.showBackToWizardModeButton = params.shouldHaveBackButtonToWizard; - this.lectureIdForGoingBack = params.lectureId; - this.lectureWizardStepForGoingBack = params.step; - }); - this.exerciseFilter = new ExerciseFilter(''); } @@ -104,11 +87,4 @@ export class CourseManagementExercisesComponent implements OnInit { shouldHideExerciseCard(type: string): boolean { return !['all', type].includes(this.exerciseFilter.exerciseTypeSearch); } - - goBackToWizardMode() { - this.router.navigate(['/course-management', this.course.id, 'lectures', this.lectureIdForGoingBack, 'edit'], { - queryParams: { shouldBeInWizardMode: 'true', shouldOpenCreateExercise: 'true', step: this.lectureWizardStepForGoingBack }, - queryParamsHandling: '', - }); - } } diff --git a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-update.component.ts b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-update.component.ts index 8fc4511e452e..798a1d50f57d 100644 --- a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-update.component.ts +++ b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-update.component.ts @@ -33,6 +33,8 @@ import { FormulaAction } from 'app/shared/monaco-editor/model/actions/formula.ac changeDetection: ChangeDetectionStrategy.OnPush, }) export class FileUploadExerciseUpdateComponent implements AfterViewInit, OnDestroy, OnInit { + protected readonly faQuestionCircle = faQuestionCircle; + readonly IncludedInOverallScore = IncludedInOverallScore; readonly documentationType: DocumentationType = 'FileUpload'; @@ -50,7 +52,6 @@ export class FileUploadExerciseUpdateComponent implements AfterViewInit, OnDestr fileUploadExercise: FileUploadExercise; backupExercise: FileUploadExercise; isSaving: boolean; - goBackAfterSaving = false; exerciseCategories: ExerciseCategory[]; existingCategories: ExerciseCategory[]; notificationText?: string; @@ -59,19 +60,14 @@ export class FileUploadExerciseUpdateComponent implements AfterViewInit, OnDestr isImport: boolean; examCourseId?: number; - saveCommand: SaveExerciseCommand; - formStatusSections: FormSectionStatus[]; - // Subcriptions + // Subscriptions titleChannelNameComponentSubscription?: Subscription; pointsSubscription?: Subscription; bonusPointsSubscription?: Subscription; teamSubscription?: Subscription; - // Icons - faQuestionCircle = faQuestionCircle; - constructor( private fileUploadExerciseService: FileUploadExerciseService, private modalService: NgbModal, @@ -104,11 +100,6 @@ export class FileUploadExerciseUpdateComponent implements AfterViewInit, OnDestr this.examCourseId = getCourseId(fileUploadExercise); }); - this.activatedRoute.queryParams.subscribe((params) => { - if (params.shouldHaveBackButtonToWizard) { - this.goBackAfterSaving = true; - } - }); this.activatedRoute.url .pipe( tap( @@ -264,12 +255,6 @@ export class FileUploadExerciseUpdateComponent implements AfterViewInit, OnDestr private onSaveSuccess(exercise: Exercise) { this.isSaving = false; - if (this.goBackAfterSaving) { - this.navigationUtilService.navigateBack(); - - return; - } - this.navigationUtilService.navigateForwardFromExerciseUpdateOrCreation(exercise); } diff --git a/src/main/webapp/app/exercises/modeling/manage/modeling-exercise-update.component.ts b/src/main/webapp/app/exercises/modeling/manage/modeling-exercise-update.component.ts index 29612cd3c373..18faee221f26 100644 --- a/src/main/webapp/app/exercises/modeling/manage/modeling-exercise-update.component.ts +++ b/src/main/webapp/app/exercises/modeling/manage/modeling-exercise-update.component.ts @@ -1,5 +1,5 @@ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; import { HttpErrorResponse } from '@angular/common/http'; import { ModelingExercise } from 'app/entities/modeling-exercise.model'; import { ModelingExerciseService } from './modeling-exercise.service'; @@ -70,7 +70,6 @@ export class ModelingExerciseUpdateComponent implements AfterViewInit, OnDestroy isImport: boolean; isExamMode: boolean; semiAutomaticAssessmentAvailable = true; - goBackAfterSaving = false; formSectionStatus: FormSectionStatus[]; @@ -91,7 +90,6 @@ export class ModelingExerciseUpdateComponent implements AfterViewInit, OnDestroy private exerciseGroupService: ExerciseGroupService, private eventManager: EventManager, private activatedRoute: ActivatedRoute, - private router: Router, private navigationUtilService: ArtemisNavigationUtilService, private changeDetectorRef: ChangeDetectorRef, ) {} @@ -186,12 +184,6 @@ export class ModelingExerciseUpdateComponent implements AfterViewInit, OnDestroy ) .subscribe(); - this.activatedRoute.queryParams.subscribe((params) => { - if (params.shouldHaveBackButtonToWizard) { - this.goBackAfterSaving = true; - } - }); - this.isSaving = false; this.notificationText = undefined; } @@ -292,12 +284,6 @@ export class ModelingExerciseUpdateComponent implements AfterViewInit, OnDestroy this.eventManager.broadcast({ name: 'modelingExerciseListModification', content: 'OK' }); this.isSaving = false; - if (this.goBackAfterSaving) { - this.navigationUtilService.navigateBack(); - - return; - } - this.navigationUtilService.navigateForwardFromExerciseUpdateOrCreation(exercise); } diff --git a/src/main/webapp/app/exercises/quiz/manage/quiz-exercise-update.component.ts b/src/main/webapp/app/exercises/quiz/manage/quiz-exercise-update.component.ts index f212b3469e6c..0c03bd54d888 100644 --- a/src/main/webapp/app/exercises/quiz/manage/quiz-exercise-update.component.ts +++ b/src/main/webapp/app/exercises/quiz/manage/quiz-exercise-update.component.ts @@ -52,7 +52,6 @@ export class QuizExerciseUpdateComponent extends QuizExerciseValidationDirective notificationText?: string; isImport = false; - goBackAfterSaving = false; /** Constants for 'Add existing questions' and 'Import file' features **/ showExistingQuestions = false; @@ -149,12 +148,6 @@ export class QuizExerciseUpdateComponent extends QuizExerciseValidationDirective this.isImport = true; } - this.route.queryParams.subscribe((params) => { - if (params.shouldHaveBackButtonToWizard) { - this.goBackAfterSaving = true; - } - }); - /** Query the courseService for the participationId given by the params */ if (this.courseId) { this.courseService.find(this.courseId).subscribe((response: HttpResponse) => { @@ -520,12 +513,6 @@ export class QuizExerciseUpdateComponent extends QuizExerciseValidationDirective this.savedEntity = cloneDeep(quizExercise); this.changeDetector.detectChanges(); - if (this.goBackAfterSaving) { - this.navigationUtilService.navigateBack(); - - return; - } - // Navigate back only if it's an import // If we edit the exercise, a user might just want to save the current state of the added quiz questions without going back if (this.isImport) { diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.ts b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.ts index 18a5e8131205..7d0b69df31a3 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.ts +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.ts @@ -67,7 +67,6 @@ export class TextExerciseUpdateComponent implements OnInit, OnDestroy, AfterView examCourseId?: number; isExamMode: boolean; isImport = false; - goBackAfterSaving = false; AssessmentType = AssessmentType; isAthenaEnabled$: Observable | undefined; @@ -170,12 +169,6 @@ export class TextExerciseUpdateComponent implements OnInit, OnDestroy, AfterView ) .subscribe(); - this.activatedRoute.queryParams.subscribe((params) => { - if (params.shouldHaveBackButtonToWizard) { - this.goBackAfterSaving = true; - } - }); - this.isAthenaEnabled$ = this.athenaService.isEnabled(); this.isSaving = false; @@ -278,12 +271,6 @@ export class TextExerciseUpdateComponent implements OnInit, OnDestroy, AfterView this.eventManager.broadcast({ name: 'textExerciseListModification', content: 'OK' }); this.isSaving = false; - if (this.goBackAfterSaving) { - this.navigationUtilService.navigateBack(); - - return; - } - this.navigationUtilService.navigateForwardFromExerciseUpdateOrCreation(exercise); } diff --git a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/create-exercise-unit/create-exercise-unit.component.html b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/create-exercise-unit/create-exercise-unit.component.html index f0f0076d89d3..2a3db2a5e78f 100644 --- a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/create-exercise-unit/create-exercise-unit.component.html +++ b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/create-exercise-unit/create-exercise-unit.component.html @@ -21,15 +21,6 @@

  } - @if (hasCreateExerciseButton()) { - - }