From 0748077da9ec1619cdef0cda98f30ba5f6ae1a69 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Sat, 24 Aug 2024 00:30:40 +0300 Subject: [PATCH 01/15] Development: Add performance guidelines and integrated them into the PR template --- .github/PULL_REQUEST_TEMPLATE.md | 13 +- docs/.readthedocs.yaml | 2 +- docs/README.md | 4 +- docs/dev/guidelines.rst | 1 + docs/dev/guidelines/database.rst | 8 +- docs/dev/guidelines/performance.rst | 219 ++++++++++++++++++++++++++++ docs/dev/guidelines/server.rst | 8 +- docs/requirements.txt | 4 +- 8 files changed, 241 insertions(+), 18 deletions(-) create mode 100644 docs/dev/guidelines/performance.rst diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8cd968c38187..7e405ef32689 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -13,7 +13,8 @@ #### Server -- [ ] **Important**: I implemented the changes with a very good performance and prevented too many (unnecessary) database calls. +- [ ] **Important**: I implemented the changes with a [very good performance](https://docs.artemis.cit.tum.de/dev/guidelines/performance/) and prevented too many (unnecessary) and too complex database calls. +- [ ] I **strictly** followed the principle of **data economy** for all database calls. - [ ] I **strictly** followed the [server coding and design guidelines](https://docs.artemis.cit.tum.de/dev/guidelines/server/). - [ ] I added multiple integration tests (Spring) related to the features (with a high test coverage). - [ ] I added pre-authorization annotations according to the [guidelines](https://docs.artemis.cit.tum.de/dev/guidelines/server/#rest-endpoint-best-practices-for-authorization) and checked the course groups for all new REST Calls (security). @@ -21,7 +22,8 @@ #### Client -- [ ] **Important**: I implemented the changes with a very good performance, prevented too many (unnecessary) REST calls and made sure the UI is responsive, even with large data. +- [ ] **Important**: I implemented the changes with a very good performance, prevented too many (unnecessary) REST calls and made sure the UI is responsive, even with large data (e.g. using paging). +- [ ] I **strictly** followed the principle of **data economy** for all client-server REST calls. - [ ] I **strictly** followed the [client coding and design guidelines](https://docs.artemis.cit.tum.de/dev/guidelines/client/). - [ ] Following the [theming guidelines](https://docs.artemis.cit.tum.de/dev/guidelines/client-design/), I specified colors only in the theming variable files and checked that the changes look consistent in both the light and the dark theme. - [ ] I added multiple integration tests (Jest) related to the features (with a high test coverage), while following the [test guidelines](https://docs.artemis.cit.tum.de/dev/guidelines/client-tests/). @@ -92,8 +94,8 @@ Prerequisites: #### Performance Review -- [ ] I (as a reviewer) confirm that the client changes (in particular related to REST calls and UI responsiveness) are implemented with a very good performance -- [ ] I (as a reviewer) confirm that the server changes (in particular related to database calls) are implemented with a very good performance +- [ ] I (as a reviewer) confirm that the client changes (in particular related to REST calls and UI responsiveness) are implemented with a very good performance even for very large courses with more than 2000 students. +- [ ] I (as a reviewer) confirm that the server changes (in particular related to database calls) are implemented with a very good performance even for very large courses with more than 2000 students. #### Code Review - [ ] Code Review 1 - [ ] Code Review 2 @@ -103,6 +105,9 @@ Prerequisites: #### Exam Mode Test - [ ] Test 1 - [ ] Test 2 +#### Performance Tests +- [ ] Test 1 +- [ ] Test 2 ### Test Coverage diff --git a/docs/.readthedocs.yaml b/docs/.readthedocs.yaml index 9522486c382b..4e14204d7703 100644 --- a/docs/.readthedocs.yaml +++ b/docs/.readthedocs.yaml @@ -4,7 +4,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.10" + python: "3.12" sphinx: fail_on_warning: true python: diff --git a/docs/README.md b/docs/README.md index aaf6ac4ae190..a806a39d5ef5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -60,11 +60,11 @@ RtD will build and deploy changes automatically. You can install Sphinx using `pip` or choose a system-wide installation instead. When using pip, consider using [Python virtual environments]. ```bash -pip install -r requirements.txt +pip install -r requirements.txt --break-system-packages ``` or ```bash -pip3 install -r requirements.txt +pip3 install -r requirements.txt --break-system-packages ``` The [Installing Sphinx] documentation explains more install options. For macOS, it is recommended to install it using homebrew: diff --git a/docs/dev/guidelines.rst b/docs/dev/guidelines.rst index b53f26746d0d..8574b339f28d 100644 --- a/docs/dev/guidelines.rst +++ b/docs/dev/guidelines.rst @@ -7,6 +7,7 @@ Coding and design guidelines :includehidden: :maxdepth: 3 + guidelines/performance guidelines/server guidelines/server-tests guidelines/client diff --git a/docs/dev/guidelines/database.rst b/docs/dev/guidelines/database.rst index 7898ab2caa33..0c7119159a9d 100644 --- a/docs/dev/guidelines/database.rst +++ b/docs/dev/guidelines/database.rst @@ -1,8 +1,6 @@ -********************** -Database Relationships -********************** - -WORK IN PROGRESS +******** +Database +******** 1. Retrieving and Building Objects ================================== diff --git a/docs/dev/guidelines/performance.rst b/docs/dev/guidelines/performance.rst new file mode 100644 index 000000000000..192f3dc34739 --- /dev/null +++ b/docs/dev/guidelines/performance.rst @@ -0,0 +1,219 @@ +*********** +Performance +*********** + +These guidelines focus on optimizing the performance of Spring Boot applications using Hibernate, with an emphasis on data economy, large-scale testing, paging, and general SQL database best practices. You can find more best practices in the `Database Guidelines `_ section. + +1. Data Economy +=============== + +**Database-Level Filtering** + +Ensure that all filtering is done at the database level rather than in memory. This approach minimizes data transfer to the application and reduces memory usage. + +Example: + +.. code-block:: java + + @Query(""" + SELECT e + FROM Exercise e + WHERE e.course.id = :courseId + AND e.releaseDate >= :releaseDate + """) + List findExercisesByCourseAndReleaseDate(@Param("courseId") Long courseId, @Param("releaseDate") ZonedDateTime releaseDate); + +**Projections and DTOs** + +When only a subset of fields is needed, use projections or Data Transfer Objects (DTOs) instead of fetching entire entities. This reduces the amount of data loaded and improves query performance. + +Example: + +.. code-block:: java + + @Query(""" + SELECT new com.example.dto.ExerciseDTO(e.id, e.title) + FROM Exercise e + WHERE e.course.id = :courseId + AND e.releaseDate >= :releaseDate + """) + List findExerciseDTOsByCourseAndReleaseDate(@Param("courseId") Long courseId, @Param("releaseDate") ZonedDateTime releaseDate); + +2. Large Scale Testing +======================= + +**Test with Realistic Data Loads** + +Given that courses can have up to 2,000 students, simulate this scale during testing to identify potential performance bottlenecks when handling large amounts of data. + +**Benchmarking** + +Perform load testing to ensure that the application can handle the expected volume of data efficiently. + +Example: + +Use tools like JMeter or Gatling to simulate concurrent users and large datasets. + +3. Paging +========= + +**Implement Paging for Large Results** + +For queries that return large datasets, implement pagination to avoid loading too much data into memory at once. + +Example: + +.. code-block:: java + + Page findByCourseId(Long courseId, Pageable pageable); + +**Caution with Collection Fetching and Pagination** + +Avoid combining `LEFT JOIN FETCH` with pagination, as this can cause performance issues or even fail due to the Cartesian Product problem. + +Example: + +Instead of: + +.. code-block:: java + + @Query(""" + SELECT c + FROM Course c + LEFT JOIN FETCH c.exercises + WHERE c.id = :courseId + """) + Page findCourseWithExercises(@Param("courseId") Long courseId, Pageable pageable); + +Do: + +.. code-block:: java + + @Query(""" + SELECT c + FROM Course c + WHERE c.id = :courseId + """) + Course findCourseById(@Param("courseId") Long courseId); + + // Fetch exercises in a separate query if needed + @Query(""" + SELECT e + FROM Exercise e + WHERE e.course.id = :courseId + """) + List findExercisesByCourseId(@Param("courseId") Long courseId); + +You can find out more on https://vladmihalcea.com/hibernate-query-fail-on-pagination-over-collection-fetch + +4. Avoiding the N+1 Issue +========================= + +**Eager Fetching and Left Join Fetch** + +The N+1 query issue occurs when lazy-loaded collections cause multiple queries to be executed — one for the parent entity and additional queries for each related entity. To avoid this issue, consider using eager fetching or `JOIN FETCH` for collections that are critical to performance. + +Example: + +.. code-block:: java + + @Query(""" + SELECT e + FROM Exercise e + JOIN FETCH e.submissions + WHERE e.course.id = :courseId + """) + List findExercisesWithSubmissions(@Param("courseId") Long courseId); + +In this example, the query fetches exercises along with their submissions in a single query, avoiding the N+1 problem. Be cautious, however, as fetching too many collections eagerly can lead to performance degradation due to large result sets. + + +5. Optimal Use of Left Join Fetch +================================= + +**Balance Between Queries** + +While reducing the number of queries by using `LEFT JOIN FETCH` is often beneficial, overusing this strategy can lead to performance issues, especially when fetching multiple `OneToMany` relationships. As a best practice, avoid fetching more than three `OneToMany` collections in a single query. + +Example: + +.. code-block:: java + + @Query(""" + SELECT c + FROM Course c + LEFT JOIN FETCH c.exercises e + LEFT JOIN FETCH e.participations + WHERE c.id = :courseId + """) + Course findCourseWithExercisesAndParticipations(@Param("courseId") Long courseId); + +This query efficiently fetches a course with its exercises and their submissions. However, if more collections are added to the fetch, consider splitting the query into multiple parts to prevent large result sets and excessive memory usage. + +**Selective Fetching** + +Use lazy loading by default, and override with `JOIN FETCH` only when necessary for performance-critical queries. This approach minimizes the risk of performance degradation due to large query results. + +Example: + +.. code-block:: java + + @Entity + public class Exercise { + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "exercise") + private List participations; + + // Other fields and methods + } + +By default, participations are lazily loaded. When you need to fetch them, use a specific `JOIN FETCH` query only in performance-sensitive situations. Alternatively, consider using ``@EntityGraph`` to define fetch plans for specific queries. + +6. General SQL Database Best Practices +====================================== + +**Indexing** + +Indexes are critical for query performance, especially on columns that are frequently used in `WHERE` clauses, `JOIN` conditions, or are sorted. Ensure that all key fields, such as `releaseDate` and `courseId`, are properly indexed. + +Example: + +Create an index on the `releaseDate` column to speed up queries filtering exercises by date: + +.. code-block:: sql + + CREATE INDEX idx_exercise_release_date ON exercise(release_date); + +**Normalization vs. Denormalization** + +While normalization reduces data redundancy, it can lead to complex queries with multiple joins. In scenarios where read performance is critical, consider denormalizing certain tables to reduce the number of joins. However, always balance this against potential issues such as data inconsistency and increased storage requirements. + +**Use of Foreign Keys** + +Maintain foreign key constraints to enforce data integrity. However, be aware of the potential performance impact on insert, update, and delete operations in high-load scenarios. Proper indexing can help mitigate these effects. + +Example: + +.. code-block:: sql + + ALTER TABLE submission ADD CONSTRAINT fk_exercise FOREIGN KEY (exercise_id) REFERENCES exercise(id); + +This foreign key ensures that submissions are always linked to a valid exercise, maintaining data integrity. + +**Query Optimization** + +Regularly review and optimize SQL queries to ensure they are performing efficiently. Use tools like `EXPLAIN` to analyze query execution plans and make adjustments where necessary. + +Example: + +.. code-block:: sql + + EXPLAIN SELECT * FROM exercise WHERE course_id = 1 AND release_date > '2024-01-01'; + +Use the `EXPLAIN` output to identify slow-running queries and optimize them by adding indexes, rewriting queries, or adjusting table structures. + +**Avoid Transactions** + +Transactions are generally very slow and should be avoided when possible. + +By following these best practices, you can build Spring Boot applications with Hibernate that are optimized for performance, even under the demands of large-scale data processing. diff --git a/docs/dev/guidelines/server.rst b/docs/dev/guidelines/server.rst index 9e5a8ce068de..a4e510f362b0 100644 --- a/docs/dev/guidelines/server.rst +++ b/docs/dev/guidelines/server.rst @@ -99,7 +99,7 @@ Avoid code duplication. If we cannot reuse a method elsewhere, then the method i 8. Comments =========== -Only write comments for complicated algorithms, to help other developers better understand them. We should only add a comment, if our code is not self-explanatory. +Always add JavaDoc and inline comments to help other developers better understand the code and the rationale behind it. ChatGPT can be a great help. It can generate comments for you, but you should always check them and adjust them to your needs. Prefer more extensive comments and documentation and avoid useless and non sense documentation. Comments should always be in English. 9. Utility ========== @@ -121,10 +121,10 @@ It gets activated when a particular jar file is detected on the classpath. The s * RestControllers should be stateless. * RestControllers are by default singletons. -* RestControllers should not execute business logic but rely on delegation. -* RestControllers should deal with the HTTP layer of the application. +* RestControllers should not execute business logic but rely on delegation to ``@Service`` classes. +* RestControllers should deal with the HTTP layer of the application, handle access control, input data validation, output data cleanup (if necessary) and error handling. * RestControllers should be oriented around a use-case/business-capability. -* RestControllers should return DTOs that are as small as possible +* RestControllers must always return DTOs that are as small as possible (please focus on data economy to improve performance and follow data privacy principles). Route naming conventions: diff --git a/docs/requirements.txt b/docs/requirements.txt index 980c9ef308b6..d7f6f239f035 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ -Sphinx==7.2.6 +Sphinx==7.4.7 sphinx-rtd-theme==2.0.0 -sphinx-autobuild==2024.2.4 +sphinx-autobuild==2024.04.16 sphinxcontrib-bibtex==2.6.2 From 5929e7fc0a813901de548e6422ccebd2085d4573 Mon Sep 17 00:00:00 2001 From: Patrik Zander <38403547+pzdr7@users.noreply.github.com> Date: Sat, 24 Aug 2024 17:48:33 +0200 Subject: [PATCH 02/15] General: Replace the remaining markdown editors with Monaco (#9230) --- .../legal-document-update.component.html | 15 +- .../legal/legal-document-update.component.ts | 39 +-- .../knowledge-area-edit.component.html | 2 +- ...tandardized-competency-edit.component.html | 2 +- ...ardized-competency-management.component.ts | 11 +- .../course/competencies/competency.module.ts | 2 + ...mmon-course-competency-form.component.html | 2 +- ...common-course-competency-form.component.ts | 12 +- ...tency-recommendation-detail.component.html | 2 +- .../course/manage/course-management.module.ts | 2 + .../manage/course-update.component.html | 4 +- .../tutorial-groups-management.module.ts | 3 + .../tutorial-group-form.component.html | 2 +- ...e-announcement-create-modal.component.html | 7 +- ...ive-announcement-create-modal.component.ts | 23 +- .../manage/exams/exam-update.component.html | 8 +- ...file-upload-exercise-update.component.html | 10 +- .../file-upload-exercise-update.component.ts | 9 +- .../modeling-exercise-update.component.html | 10 +- .../modeling-exercise-update.component.ts | 9 +- ...ercise-editable-instruction.component.html | 2 + ...drag-and-drop-question-edit.component.html | 15 +- .../drag-and-drop-question-edit.component.ts | 34 +-- ...ltiple-choice-question-edit.component.html | 17 +- ...multiple-choice-question-edit.component.ts | 64 ++-- ...te-multiple-choice-question.component.html | 31 +- ...uate-multiple-choice-question.component.ts | 2 + .../short-answer-question-edit.component.html | 1 + .../exercise-hint-update.component.html | 11 +- .../manage/exercise-hint-update.component.ts | 8 +- .../exercise-hint/manage/exercise-hint.scss | 5 - ...rading-instructions-details.component.html | 37 ++- ...rading-instructions-details.component.scss | 4 - .../grading-instructions-details.component.ts | 289 ++++++++---------- .../text-exercise-update.component.html | 10 +- .../text-exercise-update.component.ts | 11 +- .../text-unit-form.component.html | 8 +- .../app/lecture/lecture-update.component.html | 7 +- .../app/lecture/lecture-update.component.ts | 6 +- .../lecture-wizard-title.component.html | 2 +- .../lecture-wizard-title.component.ts | 6 +- .../markdown-editor.component.scss | 2 +- .../markdown-editor.component.ts | 2 +- .../markdown-editor-monaco.component.html | 35 ++- .../markdown-editor-monaco.component.scss | 4 +- .../markdown-editor-monaco.component.ts | 123 ++++++-- .../monaco/markdown-editor-parsing.helper.ts | 61 ++++ .../posting-markdown-editor.component.html | 2 + .../monaco-exercise-reference.action.ts | 4 + .../monaco-grading-credits.action.ts | 20 ++ .../monaco-grading-criterion.action.ts | 22 ++ .../monaco-grading-description.action.ts | 20 ++ .../monaco-grading-feedback.action.ts | 20 ++ .../monaco-grading-instruction.action.ts | 35 +++ .../monaco-grading-scale.action.ts | 20 ++ .../monaco-grading-usage-count.action.ts | 20 ++ .../actions/monaco-editor-action.model.ts | 28 +- .../monaco-editor-domain-action.model.ts | 33 +- .../model/actions/monaco-formula.action.ts | 6 +- .../model/actions/monaco-task.action.ts | 9 +- .../model/actions/monaco-test-case.action.ts | 4 + ...o-correct-multiple-choice-answer.action.ts | 20 ++ .../quiz/monaco-quiz-explanation.action.ts | 20 ++ .../actions/quiz/monaco-quiz-hint.action.ts | 20 ++ ...aco-wrong-multiple-choice-answer.action.ts | 20 ++ .../monaco-editor-option-preset.model.ts | 6 +- .../monaco-editor-option.helper.ts | 39 ++- .../monaco-editor/monaco-editor.component.ts | 46 +-- .../legal-document-update.component.spec.ts | 15 +- .../competency-form.component.spec.ts | 8 +- .../edit-competency.component.spec.ts | 8 +- .../edit-prerequisite.component.spec.ts | 8 +- ...cy-recommendation-detail.component.spec.ts | 4 +- .../prerequisite-form.component.spec.ts | 9 +- .../course/course-update.component.spec.ts | 2 +- ...g-and-drop-question-edit.component.spec.ts | 33 +- .../exam/exam-update.component.spec.ts | 6 +- ...nnouncement-create-modal.component.spec.ts | 9 +- .../exercise-hint-update.component.spec.ts | 4 +- ...multiple-choice-question.component.spec.ts | 4 +- .../text-unit-form.component.spec.ts | 2 +- .../lecture/lecture-update.component.spec.ts | 4 +- .../lecture-wizard-title.component.spec.ts | 4 +- .../markdown-editor-monaco.component.spec.ts | 30 +- .../markdown-editor-parsing.helper.spec.ts | 51 ++++ ...ple-choice-question-edit.component.spec.ts | 44 +-- ...ing-instructions-details.component.spec.ts | 100 +++--- ...aco-editor-action-quiz.integration.spec.ts | 40 +++ .../monaco-editor-action.integration.spec.ts | 2 +- ...r-grading-instructions.integration.spec.ts | 74 +++++ .../monaco-editor.component.spec.ts | 6 + .../knowledge-area-edit.component.spec.ts | 4 +- .../standardized-competency-edit.spec.ts | 4 +- .../tutorial-group-form.component.spec.ts | 2 +- ...code-editor-instructor.integration.spec.ts | 2 - .../course/CourseCommunicationPage.ts | 4 +- .../pageobjects/exam/ExamCreationPage.ts | 7 +- .../pageobjects/exam/ExamManagementPage.ts | 2 +- .../FileUploadExerciseCreationPage.ts | 2 +- .../quiz/QuizExerciseCreationPage.ts | 8 +- .../text/TextExerciseCreationPage.ts | 2 +- .../lecture/LectureCreationPage.ts | 2 +- .../lecture/LectureManagementPage.ts | 2 +- 103 files changed, 1264 insertions(+), 629 deletions(-) delete mode 100644 src/main/webapp/app/exercises/shared/exercise-hint/manage/exercise-hint.scss create mode 100644 src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-parsing.helper.ts create mode 100644 src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-credits.action.ts create mode 100644 src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-criterion.action.ts create mode 100644 src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-description.action.ts create mode 100644 src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-feedback.action.ts create mode 100644 src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-instruction.action.ts create mode 100644 src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-scale.action.ts create mode 100644 src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-usage-count.action.ts create mode 100644 src/main/webapp/app/shared/monaco-editor/model/actions/quiz/monaco-correct-multiple-choice-answer.action.ts create mode 100644 src/main/webapp/app/shared/monaco-editor/model/actions/quiz/monaco-quiz-explanation.action.ts create mode 100644 src/main/webapp/app/shared/monaco-editor/model/actions/quiz/monaco-quiz-hint.action.ts create mode 100644 src/main/webapp/app/shared/monaco-editor/model/actions/quiz/monaco-wrong-multiple-choice-answer.action.ts create mode 100644 src/test/javascript/spec/component/markdown-editor/markdown-editor-parsing.helper.spec.ts create mode 100644 src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-grading-instructions.integration.spec.ts diff --git a/src/main/webapp/app/admin/legal/legal-document-update.component.html b/src/main/webapp/app/admin/legal/legal-document-update.component.html index d581d9dcf664..7ab8f31edb25 100644 --- a/src/main/webapp/app/admin/legal/legal-document-update.component.html +++ b/src/main/webapp/app/admin/legal/legal-document-update.component.html @@ -3,15 +3,14 @@

-
@if (!unsavedChanges && !isSaving) { @@ -34,13 +33,7 @@

}

- diff --git a/src/main/webapp/app/admin/legal/legal-document-update.component.ts b/src/main/webapp/app/admin/legal/legal-document-update.component.ts index 11151a4410c1..cbe5aafc7cd5 100644 --- a/src/main/webapp/app/admin/legal/legal-document-update.component.ts +++ b/src/main/webapp/app/admin/legal/legal-document-update.component.ts @@ -1,14 +1,14 @@ import { AfterContentChecked, ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core'; import { faBan, faCheckCircle, faCircleNotch, faExclamationTriangle, faSave } from '@fortawesome/free-solid-svg-icons'; import { LegalDocumentService } from 'app/shared/service/legal-document.service'; -import { MarkdownEditorComponent, MarkdownEditorHeight } from 'app/shared/markdown-editor/markdown-editor.component'; +import { MarkdownEditorHeight } from 'app/shared/markdown-editor/markdown-editor.component'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { UnsavedChangesWarningComponent } from 'app/admin/legal/unsaved-changes-warning/unsaved-changes-warning.component'; import { LegalDocument, LegalDocumentLanguage, LegalDocumentType } from 'app/entities/legal-document.model'; import { ActivatedRoute } from '@angular/router'; import { Observable, tap } from 'rxjs'; import { JhiLanguageHelper } from 'app/core/language/language.helper'; -import { ArtemisMarkdownService } from 'app/shared/markdown.service'; +import { MarkdownEditorMonacoComponent } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; @Component({ selector: 'jhi-privacy-statement-update-component', @@ -35,12 +35,12 @@ export class LegalDocumentUpdateComponent implements OnInit, AfterContentChecked legalDocumentType: LegalDocumentType = LegalDocumentType.PRIVACY_STATEMENT; unsavedChanges = false; isSaving = false; - @ViewChild(MarkdownEditorComponent, { static: false }) markdownEditor: MarkdownEditorComponent; + @ViewChild(MarkdownEditorMonacoComponent, { static: false }) markdownEditor: MarkdownEditorMonacoComponent; + currentContentTrimmed = ''; currentLanguage = this.DEFAULT_LANGUAGE; unsavedChangesWarning: NgbModalRef; titleKey: string; - private languageChangeInPreview: boolean; constructor( private legalDocumentService: LegalDocumentService, @@ -48,7 +48,6 @@ export class LegalDocumentUpdateComponent implements OnInit, AfterContentChecked private route: ActivatedRoute, private languageHelper: JhiLanguageHelper, private changeDetectorRef: ChangeDetectorRef, - private markdownService: ArtemisMarkdownService, ) {} ngOnInit() { @@ -84,7 +83,7 @@ export class LegalDocumentUpdateComponent implements OnInit, AfterContentChecked updateLegalDocument() { this.isSaving = true; - this.legalDocument.text = this.markdownEditor.markdown!; + this.legalDocument.text = this.currentContentTrimmed; if (this.legalDocumentType === LegalDocumentType.PRIVACY_STATEMENT) { this.legalDocumentService.updatePrivacyStatement(this.legalDocument).subscribe((statement) => { this.setUpdatedDocument(statement); @@ -102,29 +101,27 @@ export class LegalDocumentUpdateComponent implements OnInit, AfterContentChecked this.isSaving = false; } - checkUnsavedChanges(content: string) { + onContentChanged(content: string) { + this.currentContentTrimmed = content.trim(); this.unsavedChanges = content !== this.legalDocument.text; } - onLanguageChange(legalDocumentLanguage: any) { + onLanguageChange(legalDocumentLanguage: LegalDocumentLanguage) { if (this.unsavedChanges) { this.showWarning(legalDocumentLanguage); } else { - this.markdownEditor.markdown = ''; this.currentLanguage = legalDocumentLanguage; this.getLegalDocumentForUpdate(this.legalDocumentType, legalDocumentLanguage).subscribe((document) => { this.legalDocument = document; + this.markdownEditor.markdown = this.legalDocument.text; + // Ensure the new text is parsed and displayed in the preview + this.markdownEditor.parseMarkdown(); this.unsavedChanges = false; - // if we are currently in preview mode, we need to update the preview - if (this.markdownEditor.previewMode) { - this.markdownEditor.previewTextAsHtml = this.markdownService.safeHtmlForMarkdown(this.legalDocument.text); - this.languageChangeInPreview = true; - } }); } } - showWarning(legalDocumentLanguage: any) { + showWarning(legalDocumentLanguage: LegalDocumentLanguage) { this.unsavedChangesWarning = this.modalService.open(UnsavedChangesWarningComponent, { size: 'lg', backdrop: 'static' }); if (this.legalDocumentType === LegalDocumentType.PRIVACY_STATEMENT) { this.unsavedChangesWarning.componentInstance.textMessage = 'artemisApp.legal.privacyStatement.unsavedChangesWarning'; @@ -151,16 +148,4 @@ export class LegalDocumentUpdateComponent implements OnInit, AfterContentChecked ngAfterContentChecked() { this.changeDetectorRef.detectChanges(); } - - /** - * If the language is changed while we are in the preview mode, we must trigger a change event, so the ace editor updates its content. - * We must do this when the editor is visible because otherwise the editor will only be updated if you click on it once. - */ - updateTextIfLanguageChangedInPreview() { - if (this.languageChangeInPreview) { - // we have to trigger a change event, so the ace editor updates its content - this.markdownEditor.aceEditorContainer.getEditor().session._emit('change', { start: { row: 0, column: 0 }, end: { row: 0, column: 0 }, action: 'insert', lines: [] }); - this.languageChangeInPreview = false; - } - } } diff --git a/src/main/webapp/app/admin/standardized-competencies/knowledge-area-edit.component.html b/src/main/webapp/app/admin/standardized-competencies/knowledge-area-edit.component.html index 5121a3a30893..249e72addc6b 100644 --- a/src/main/webapp/app/admin/standardized-competencies/knowledge-area-edit.component.html +++ b/src/main/webapp/app/admin/standardized-competencies/knowledge-area-edit.component.html @@ -62,7 +62,7 @@

@if (isEditing) { -
@if (isEditing) { - onError(this.alertService, errorResponse), }); @@ -130,6 +133,7 @@ export class StandardizedCompetencyManagementComponent extends StandardizedCompe this.isEditing = false; this.selectedKnowledgeArea = undefined; } + this.changeDetectorRef.detectChanges(); }, error: (error: HttpErrorResponse) => this.dialogErrorSource.next(error.message), }); @@ -152,6 +156,7 @@ export class StandardizedCompetencyManagementComponent extends StandardizedCompe if (!(this.selectedKnowledgeArea?.id || this.selectedCompetency) && !this.isEditing) { this.selectedKnowledgeArea = resultKnowledgeArea; } + this.changeDetectorRef.detectChanges(); }, error: (error: HttpErrorResponse) => onError(this.alertService, error), }); @@ -167,6 +172,7 @@ export class StandardizedCompetencyManagementComponent extends StandardizedCompe if (resultKnowledgeArea.id === this.selectedKnowledgeArea?.id) { this.selectedKnowledgeArea = resultKnowledgeArea; } + this.changeDetectorRef.detectChanges(); }, error: (error: HttpErrorResponse) => onError(this.alertService, error), }); @@ -205,6 +211,7 @@ export class StandardizedCompetencyManagementComponent extends StandardizedCompe this.isEditing = false; this.selectedCompetency = undefined; } + this.changeDetectorRef.detectChanges(); }, error: (error: HttpErrorResponse) => this.dialogErrorSource.next(error.message), }); @@ -227,6 +234,7 @@ export class StandardizedCompetencyManagementComponent extends StandardizedCompe if (!(this.selectedCompetency?.id || this.selectedKnowledgeArea) && !this.isEditing) { this.selectedCompetency = resultCompetency; } + this.changeDetectorRef.detectChanges(); }, error: (error: HttpErrorResponse) => onError(this.alertService, error), }); @@ -244,6 +252,7 @@ export class StandardizedCompetencyManagementComponent extends StandardizedCompe if (resultCompetency.id === this.selectedCompetency?.id) { this.selectedCompetency = resultCompetency; } + this.changeDetectorRef.detectChanges(); }, error: (error: HttpErrorResponse) => onError(this.alertService, error), }); diff --git a/src/main/webapp/app/course/competencies/competency.module.ts b/src/main/webapp/app/course/competencies/competency.module.ts index 815be526a131..d7b4c6da30b5 100644 --- a/src/main/webapp/app/course/competencies/competency.module.ts +++ b/src/main/webapp/app/course/competencies/competency.module.ts @@ -23,6 +23,7 @@ import { JudgementOfLearningRatingComponent } from 'app/course/competencies/judg import { CompetencyManagementTableComponent } from 'app/course/competencies/competency-management/competency-management-table.component'; import { CompetencySearchComponent } from 'app/course/competencies/import/competency-search.component'; import { ImportCompetenciesTableComponent } from 'app/course/competencies/import/import-competencies-table.component'; +import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; @NgModule({ imports: [ @@ -41,6 +42,7 @@ import { ImportCompetenciesTableComponent } from 'app/course/competencies/import RatingModule, JudgementOfLearningRatingComponent, CompetencyManagementTableComponent, + ArtemisMarkdownEditorModule, ], declarations: [ CompetencyRingsComponent, diff --git a/src/main/webapp/app/course/competencies/forms/common-course-competency-form.component.html b/src/main/webapp/app/course/competencies/forms/common-course-competency-form.component.html index 38a8936b2701..c27066b26924 100644 --- a/src/main/webapp/app/course/competencies/forms/common-course-competency-form.component.html +++ b/src/main/webapp/app/course/competencies/forms/common-course-competency-form.component.html @@ -27,7 +27,7 @@ @if (!isInConnectMode) {
- {{ titleControl.value
@if (isInEditMode) { -
- - GitHub Markdown Guide - +

diff --git a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-announcement-dialog/exam-live-announcement-create-modal.component.html b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-announcement-dialog/exam-live-announcement-create-modal.component.html index 0b05bacb6172..d6ec36ee35e0 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-announcement-dialog/exam-live-announcement-create-modal.component.html +++ b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-announcement-dialog/exam-live-announcement-create-modal.component.html @@ -9,15 +9,14 @@
- +
- +
- +
- +
@if (!isExamMode) { 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 38e688e7c415..abd489160baa 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 @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute, Params } from '@angular/router'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { AlertService, AlertType } from 'app/core/util/alert.service'; @@ -8,7 +8,6 @@ import { CourseManagementService } from 'app/course/manage/course-management.ser import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { Exercise, ExerciseMode, IncludedInOverallScore, getCourseId, resetForImport } from 'app/entities/exercise.model'; import { EditorMode } from 'app/shared/markdown-editor/markdown-editor.component'; -import { KatexCommand } from 'app/shared/markdown-editor/commands/katex.command'; import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; import { ExerciseCategory } from 'app/entities/exercise-category.model'; import { cloneDeep } from 'lodash-es'; @@ -27,10 +26,12 @@ import { TeamConfigFormGroupComponent } from 'app/exercises/shared/team-config-f import { NgModel } from '@angular/forms'; import { Subscription } from 'rxjs'; import { FormSectionStatus } from 'app/forms/form-status-bar/form-status-bar.component'; +import { MonacoFormulaAction } from 'app/shared/monaco-editor/model/actions/monaco-formula.action'; @Component({ selector: 'jhi-file-upload-exercise-update', templateUrl: './file-upload-exercise-update.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, }) export class FileUploadExerciseUpdateComponent implements AfterViewInit, OnDestroy, OnInit { readonly IncludedInOverallScore = IncludedInOverallScore; @@ -55,8 +56,8 @@ export class FileUploadExerciseUpdateComponent implements AfterViewInit, OnDestr existingCategories: ExerciseCategory[]; EditorMode = EditorMode; notificationText?: string; - domainCommandsProblemStatement = [new KatexCommand()]; - domainCommandsSampleSolution = [new KatexCommand()]; + domainActionsProblemStatement = [new MonacoFormulaAction()]; + domainActionsExampleSolution = [new MonacoFormulaAction()]; isImport: boolean; examCourseId?: number; diff --git a/src/main/webapp/app/exercises/modeling/manage/modeling-exercise-update.component.html b/src/main/webapp/app/exercises/modeling/manage/modeling-exercise-update.component.html index 05e678ad84d4..23889e51c013 100644 --- a/src/main/webapp/app/exercises/modeling/manage/modeling-exercise-update.component.html +++ b/src/main/webapp/app/exercises/modeling/manage/modeling-exercise-update.component.html @@ -33,12 +33,11 @@

-
@if (!isExamMode) { @@ -66,12 +65,11 @@

-

} 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 7e66dac4b5d8..1da174b17a01 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,4 +1,4 @@ -import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { HttpErrorResponse } from '@angular/common/http'; import { ModelingExercise } from 'app/entities/modeling-exercise.model'; @@ -7,7 +7,6 @@ import { CourseManagementService } from 'app/course/manage/course-management.ser import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { ExerciseMode, IncludedInOverallScore, resetForImport } from 'app/entities/exercise.model'; import { EditorMode } from 'app/shared/markdown-editor/markdown-editor.component'; -import { KatexCommand } from 'app/shared/markdown-editor/commands/katex.command'; import { AssessmentType } from 'app/entities/assessment-type.model'; import { switchMap, tap } from 'rxjs/operators'; import { ExerciseGroupService } from 'app/exam/manage/exercise-groups/exercise-group.service'; @@ -32,10 +31,12 @@ import { NgModel } from '@angular/forms'; import { ExerciseUpdatePlagiarismComponent } from 'app/exercises/shared/plagiarism/exercise-update-plagiarism/exercise-update-plagiarism.component'; import { TeamConfigFormGroupComponent } from 'app/exercises/shared/team-config-form-group/team-config-form-group.component'; import { FormDateTimePickerComponent } from 'app/shared/date-time-picker/date-time-picker.component'; +import { MonacoFormulaAction } from 'app/shared/monaco-editor/model/actions/monaco-formula.action'; @Component({ selector: 'jhi-modeling-exercise-update', templateUrl: './modeling-exercise-update.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, }) export class ModelingExerciseUpdateComponent implements AfterViewInit, OnDestroy, OnInit { @ViewChild(ExerciseTitleChannelNameComponent) exerciseTitleChannelNameComponent: ExerciseTitleChannelNameComponent; @@ -65,8 +66,8 @@ export class ModelingExerciseUpdateComponent implements AfterViewInit, OnDestroy exerciseCategories: ExerciseCategory[]; existingCategories: ExerciseCategory[]; notificationText?: string; - domainCommandsProblemStatement = [new KatexCommand()]; - domainCommandsSampleSolution = [new KatexCommand()]; + domainActionsProblemStatement = [new MonacoFormulaAction()]; + domainActionsExampleSolution = [new MonacoFormulaAction()]; examCourseId?: number; isImport: boolean; isExamMode: boolean; diff --git a/src/main/webapp/app/exercises/programming/manage/instructions-editor/programming-exercise-editable-instruction.component.html b/src/main/webapp/app/exercises/programming/manage/instructions-editor/programming-exercise-editable-instruction.component.html index 4b697c678b61..677106c40346 100644 --- a/src/main/webapp/app/exercises/programming/manage/instructions-editor/programming-exercise-editable-instruction.component.html +++ b/src/main/webapp/app/exercises/programming/manage/instructions-editor/programming-exercise-editable-instruction.component.html @@ -5,8 +5,10 @@ class="overflow-hidden flex-grow-1" [domainActions]="domainActions" [initialEditorHeight]="initialEditorHeight" + [useDefaultMarkdownEditorOptions]="false" [enableResize]="enableResize" [markdown]="exercise.problemStatement" + [showDefaultPreview]="false" (onPreviewSelect)="generateHtml()" (markdownChange)="updateProblemStatement($event)" > diff --git a/src/main/webapp/app/exercises/quiz/manage/drag-and-drop-question/drag-and-drop-question-edit.component.html b/src/main/webapp/app/exercises/quiz/manage/drag-and-drop-question/drag-and-drop-question-edit.component.html index 183061aefb94..4f9c51cf8ead 100644 --- a/src/main/webapp/app/exercises/quiz/manage/drag-and-drop-question/drag-and-drop-question-edit.component.html +++ b/src/main/webapp/app/exercises/quiz/manage/drag-and-drop-question/drag-and-drop-question-edit.component.html @@ -200,29 +200,28 @@

@if (!reEvaluationInProgress) {
-
- } - @if (reEvaluationInProgress) { + } @else {
- diff --git a/src/main/webapp/app/exercises/quiz/manage/drag-and-drop-question/drag-and-drop-question-edit.component.ts b/src/main/webapp/app/exercises/quiz/manage/drag-and-drop-question/drag-and-drop-question-edit.component.ts index fde9f929e280..784a16bcffcb 100644 --- a/src/main/webapp/app/exercises/quiz/manage/drag-and-drop-question/drag-and-drop-question-edit.component.ts +++ b/src/main/webapp/app/exercises/quiz/manage/drag-and-drop-question/drag-and-drop-question-edit.component.ts @@ -16,14 +16,10 @@ import { DragAndDropQuestionUtil } from 'app/exercises/quiz/shared/drag-and-drop import { DragAndDropMouseEvent } from 'app/exercises/quiz/manage/drag-and-drop-question/drag-and-drop-mouse-event.class'; import { DragState } from 'app/entities/quiz/drag-state.enum'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { HintCommand } from 'app/shared/markdown-editor/domainCommands/hint.command'; -import { ExplanationCommand } from 'app/shared/markdown-editor/domainCommands/explanation.command'; import { DragAndDropMapping } from 'app/entities/quiz/drag-and-drop-mapping.model'; import { DragAndDropQuestion } from 'app/entities/quiz/drag-and-drop-question.model'; -import { MarkdownEditorComponent } from 'app/shared/markdown-editor/markdown-editor.component'; import { DragItem } from 'app/entities/quiz/drag-item.model'; import { DropLocation } from 'app/entities/quiz/drop-location.model'; -import { DomainCommand } from 'app/shared/markdown-editor/domainCommands/domainCommand'; import { QuizQuestionEdit } from 'app/exercises/quiz/manage/quiz-question-edit.interface'; import { cloneDeep } from 'lodash-es'; import { round } from 'app/shared/util/utils'; @@ -52,6 +48,9 @@ import { faFileImage } from '@fortawesome/free-regular-svg-icons'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { MAX_QUIZ_QUESTION_POINTS } from 'app/shared/constants/input.constants'; import { FileService } from 'app/shared/http/file.service'; +import { MonacoQuizHintAction } from 'app/shared/monaco-editor/model/actions/quiz/monaco-quiz-hint.action'; +import { MonacoQuizExplanationAction } from 'app/shared/monaco-editor/model/actions/quiz/monaco-quiz-explanation.action'; +import { MarkdownEditorMonacoComponent, TextWithDomainAction } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; @Component({ selector: 'jhi-drag-and-drop-question-edit', @@ -63,7 +62,7 @@ import { FileService } from 'app/shared/http/file.service'; export class DragAndDropQuestionEditComponent implements OnInit, OnChanges, AfterViewInit, QuizQuestionEdit { @ViewChild('clickLayer', { static: false }) private clickLayer: ElementRef; @ViewChild('backgroundImage ', { static: false }) private backgroundImage: SecuredImageComponent; - @ViewChild('markdownEditor', { static: false }) private markdownEditor: MarkdownEditorComponent; + @ViewChild('markdownEditor', { static: false }) private markdownEditor: MarkdownEditorMonacoComponent; @Input() question: DragAndDropQuestion; @Input() questionIndex: number; @@ -107,11 +106,10 @@ export class DragAndDropQuestionEditComponent implements OnInit, OnChanges, Afte */ mouse: DragAndDropMouseEvent; - hintCommand = new HintCommand(); - explanationCommand = new ExplanationCommand(); + hintAction = new MonacoQuizHintAction(); + explanationAction = new MonacoQuizExplanationAction(); - /** {array} with domainCommands that are needed for a drag and drop question **/ - dragAndDropQuestionDomainCommands: DomainCommand[] = [this.explanationCommand, this.hintCommand]; + dragAndDropDomainActions = [this.explanationAction, this.hintAction]; // Icons faBan = faBan; @@ -838,20 +836,18 @@ export class DragAndDropQuestionEditComponent implements OnInit, OnChanges, Afte } /** - * 1. Gets the {array} containing the text with the domainCommandIdentifier and creates a new drag and drop problem statement - * by assigning the text according to the domainCommandIdentifiers to the drag and drop attributes. - * (question text, explanation, hint) - * @param domainCommands - containing markdownText with the corresponding domainCommand {DomainCommand} identifier + * Creates the drag and drop problem statement from the parsed markdown text, assigning the question text, explanation, and hint according to the domain actions found. + * @param textWithDomainActions The parsed markdown text with the corresponding domain actions. */ - domainCommandsFound(domainCommands: [string, DomainCommand | null][]): void { + domainActionsFound(textWithDomainActions: TextWithDomainAction[]): void { this.cleanupQuestion(); - for (const [text, command] of domainCommands) { - if (command === null && text.length > 0) { + for (const { text, action } of textWithDomainActions) { + if (action === undefined && text.length > 0) { this.question.text = text; } - if (command instanceof ExplanationCommand) { + if (action instanceof MonacoQuizExplanationAction) { this.question.explanation = text; - } else if (command instanceof HintCommand) { + } else if (action instanceof MonacoQuizHintAction) { this.question.hint = text; } } @@ -873,6 +869,6 @@ export class DragAndDropQuestionEditComponent implements OnInit, OnChanges, Afte */ prepareForSave(): void { this.cleanupQuestion(); - this.markdownEditor.parse(); + this.markdownEditor.parseMarkdown(); } } diff --git a/src/main/webapp/app/exercises/quiz/manage/multiple-choice-question/multiple-choice-question-edit.component.html b/src/main/webapp/app/exercises/quiz/manage/multiple-choice-question/multiple-choice-question-edit.component.html index de7ab74f204c..9e13e98db8c7 100644 --- a/src/main/webapp/app/exercises/quiz/manage/multiple-choice-question/multiple-choice-question-edit.component.html +++ b/src/main/webapp/app/exercises/quiz/manage/multiple-choice-question/multiple-choice-question-edit.component.html @@ -94,19 +94,20 @@

MC

- - + @if (showMultipleChoiceQuestionPreview) { } @@ -114,13 +115,13 @@

MC

- + @if (showMultipleChoiceQuestionVisual) { - + } -
+
diff --git a/src/main/webapp/app/exercises/quiz/manage/multiple-choice-question/multiple-choice-question-edit.component.ts b/src/main/webapp/app/exercises/quiz/manage/multiple-choice-question/multiple-choice-question-edit.component.ts index 48fdeb5a2c09..014af02dd3b4 100644 --- a/src/main/webapp/app/exercises/quiz/manage/multiple-choice-question/multiple-choice-question-edit.component.ts +++ b/src/main/webapp/app/exercises/quiz/manage/multiple-choice-question/multiple-choice-question-edit.component.ts @@ -1,29 +1,33 @@ -import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild, ViewEncapsulation } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild, ViewEncapsulation } from '@angular/core'; import { ArtemisMarkdownService } from 'app/shared/markdown.service'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { HintCommand } from 'app/shared/markdown-editor/domainCommands/hint.command'; -import { ExplanationCommand } from 'app/shared/markdown-editor/domainCommands/explanation.command'; -import { IncorrectOptionCommand } from 'app/shared/markdown-editor/domainCommands/incorrectOptionCommand'; import { AnswerOption } from 'app/entities/quiz/answer-option.model'; -import { MarkdownEditorComponent } from 'app/shared/markdown-editor/markdown-editor.component'; import { MultipleChoiceQuestion } from 'app/entities/quiz/multiple-choice-question.model'; -import { CorrectOptionCommand } from 'app/shared/markdown-editor/domainCommands/correctOptionCommand'; -import { DomainCommand } from 'app/shared/markdown-editor/domainCommands/domainCommand'; import { QuizQuestionEdit } from 'app/exercises/quiz/manage/quiz-question-edit.interface'; import { generateExerciseHintExplanation } from 'app/shared/util/markdown.util'; import { faAngleDown, faAngleRight, faQuestionCircle, faTrash } from '@fortawesome/free-solid-svg-icons'; import { ScoringType } from 'app/entities/quiz/quiz-question.model'; import { MAX_QUIZ_QUESTION_POINTS } from 'app/shared/constants/input.constants'; +import { MonacoQuizHintAction } from 'app/shared/monaco-editor/model/actions/quiz/monaco-quiz-hint.action'; +import { MonacoWrongMultipleChoiceAnswerAction } from 'app/shared/monaco-editor/model/actions/quiz/monaco-wrong-multiple-choice-answer.action'; +import { MonacoCorrectMultipleChoiceAnswerAction } from 'app/shared/monaco-editor/model/actions/quiz/monaco-correct-multiple-choice-answer.action'; +import { MonacoQuizExplanationAction } from 'app/shared/monaco-editor/model/actions/quiz/monaco-quiz-explanation.action'; +import { MarkdownEditorMonacoComponent, TextWithDomainAction } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; +import { MultipleChoiceVisualQuestionComponent } from 'app/exercises/quiz/shared/questions/multiple-choice-question/multiple-choice-visual-question.component'; @Component({ selector: 'jhi-multiple-choice-question-edit', templateUrl: './multiple-choice-question-edit.component.html', styleUrls: ['../quiz-exercise.scss', '../../shared/quiz.scss'], encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, }) export class MultipleChoiceQuestionEditComponent implements OnInit, QuizQuestionEdit { @ViewChild('markdownEditor', { static: false }) - private markdownEditor: MarkdownEditorComponent; + private markdownEditor: MarkdownEditorMonacoComponent; + + @ViewChild('visual', { static: false }) + visualChild: MultipleChoiceVisualQuestionComponent; @Input() question: MultipleChoiceQuestion; @@ -43,18 +47,17 @@ export class MultipleChoiceQuestionEditComponent implements OnInit, QuizQuestion /** Set default preview of the markdown editor as preview for the multiple choice question **/ get showPreview(): boolean { - return this.markdownEditor && this.markdownEditor.previewMode; + return this.markdownEditor && this.markdownEditor.inPreviewMode; } showMultipleChoiceQuestionPreview = true; showMultipleChoiceQuestionVisual = true; - hintCommand = new HintCommand(); - correctCommand = new CorrectOptionCommand(); - incorrectCommand = new IncorrectOptionCommand(); - explanationCommand = new ExplanationCommand(); + correctAction = new MonacoCorrectMultipleChoiceAnswerAction(); + wrongAction = new MonacoWrongMultipleChoiceAnswerAction(); + explanationAction = new MonacoQuizExplanationAction(); + hintAction = new MonacoQuizHintAction(); - /** DomainCommands for the multiple choice question **/ - commandMultipleChoiceQuestions: DomainCommand[] = [this.correctCommand, this.incorrectCommand, this.explanationCommand, this.hintCommand]; + multipleChoiceActions = [this.correctAction, this.wrongAction, this.explanationAction, this.hintAction]; // Icons faTrash = faTrash; @@ -131,12 +134,13 @@ export class MultipleChoiceQuestionEditComponent implements OnInit, QuizQuestion * to get the newest values in the editor to update the question attributes */ prepareForSave(): void { - if (this.markdownEditor.visualMode) { - this.markdownEditor.markdown = this.markdownEditor.visualChild.parseQuestion(); - } - this.cleanupQuestion(); - this.markdownEditor.parse(); + this.markdownEditor.parseMarkdown(); + } + + onLeaveVisualTab(): void { + this.markdownEditor.markdown = this.visualChild.parseQuestion(); + this.prepareForSave(); } /** @@ -153,34 +157,34 @@ export class MultipleChoiceQuestionEditComponent implements OnInit, QuizQuestion } /** - * 1. Gets a tuple of text and domainCommandIdentifiers and assigns text values according to the domainCommandIdentifiers a + * 1. Gets a tuple of text and domain action identifiers and assigns text values according to the domain actions a * multiple choice question the to the multiple choice question attributes. * (question text, explanation, hint, answerOption (correct/wrong) - * 2. The tuple order is the same as the order of the commands in the markdown text inserted by the user + * 2. The tuple order is the same as the order of the actions in the markdown text inserted by the user * 3. resetMultipleChoicePreview() is triggered to notify the parent component * about the changes within the question and to cacheValidation() since the assigned values have changed - * @param domainCommands containing tuples of [text, domainCommandIdentifiers] + * @param textWithDomainActions The parsed text segments with their corresponding domain actions. */ - domainCommandsFound(domainCommands: [string, DomainCommand | null][]): void { + domainActionsFound(textWithDomainActions: TextWithDomainAction[]): void { this.cleanupQuestion(); let currentAnswerOption; - for (const [text, command] of domainCommands) { - if (command === null && text.length > 0) { + for (const { text, action } of textWithDomainActions) { + if (action === undefined && text.length > 0) { this.question.text = text; } - if (command instanceof CorrectOptionCommand || command instanceof IncorrectOptionCommand) { + if (action instanceof MonacoCorrectMultipleChoiceAnswerAction || action instanceof MonacoWrongMultipleChoiceAnswerAction) { currentAnswerOption = new AnswerOption(); - currentAnswerOption.isCorrect = command instanceof CorrectOptionCommand; + currentAnswerOption.isCorrect = action instanceof MonacoCorrectMultipleChoiceAnswerAction; currentAnswerOption.text = text; this.question.answerOptions!.push(currentAnswerOption); - } else if (command instanceof ExplanationCommand) { + } else if (action instanceof MonacoQuizExplanationAction) { if (currentAnswerOption) { currentAnswerOption.explanation = text; } else { this.question.explanation = text; } - } else if (command instanceof HintCommand) { + } else if (action instanceof MonacoQuizHintAction) { if (currentAnswerOption) { currentAnswerOption.hint = text; } else { diff --git a/src/main/webapp/app/exercises/quiz/manage/re-evaluate/multiple-choice-question/re-evaluate-multiple-choice-question.component.html b/src/main/webapp/app/exercises/quiz/manage/re-evaluate/multiple-choice-question/re-evaluate-multiple-choice-question.component.html index 29f375533509..b6b99fc4ebac 100644 --- a/src/main/webapp/app/exercises/quiz/manage/re-evaluate/multiple-choice-question/re-evaluate-multiple-choice-question.component.html +++ b/src/main/webapp/app/exercises/quiz/manage/re-evaluate/multiple-choice-question/re-evaluate-multiple-choice-question.component.html @@ -1,3 +1,4 @@ +
@@ -135,17 +136,17 @@

MC

- MC

- diff --git a/src/main/webapp/app/exercises/quiz/manage/re-evaluate/multiple-choice-question/re-evaluate-multiple-choice-question.component.ts b/src/main/webapp/app/exercises/quiz/manage/re-evaluate/multiple-choice-question/re-evaluate-multiple-choice-question.component.ts index 39489c556064..acf8aea5f8d2 100644 --- a/src/main/webapp/app/exercises/quiz/manage/re-evaluate/multiple-choice-question/re-evaluate-multiple-choice-question.component.ts +++ b/src/main/webapp/app/exercises/quiz/manage/re-evaluate/multiple-choice-question/re-evaluate-multiple-choice-question.component.ts @@ -26,6 +26,7 @@ export class ReEvaluateMultipleChoiceQuestionComponent implements OnInit { editorMode = EditorMode.NONE; markdownMap: Map; + questionText: string; // Create Backup Question for resets @Input() backupQuestion: MultipleChoiceQuestion; @@ -49,6 +50,7 @@ export class ReEvaluateMultipleChoiceQuestionComponent implements OnInit { (answer.isCorrect ? CorrectOptionCommand.IDENTIFIER : IncorrectOptionCommand.IDENTIFIER) + ' ' + generateExerciseHintExplanation(answer), ); } + this.questionText = this.getQuestionText(this.question); } /** diff --git a/src/main/webapp/app/exercises/quiz/manage/short-answer-question/short-answer-question-edit.component.html b/src/main/webapp/app/exercises/quiz/manage/short-answer-question/short-answer-question-edit.component.html index 8372aea83c43..49822af7d4d7 100644 --- a/src/main/webapp/app/exercises/quiz/manage/short-answer-question/short-answer-question-edit.component.html +++ b/src/main/webapp/app/exercises/quiz/manage/short-answer-question/short-answer-question-edit.component.html @@ -287,6 +287,7 @@

diff --git a/src/main/webapp/app/exercises/shared/exercise-hint/manage/exercise-hint-update.component.html b/src/main/webapp/app/exercises/shared/exercise-hint/manage/exercise-hint-update.component.html index 1235391ec365..5c80f6e0c868 100644 --- a/src/main/webapp/app/exercises/shared/exercise-hint/manage/exercise-hint-update.component.html +++ b/src/main/webapp/app/exercises/shared/exercise-hint/manage/exercise-hint-update.component.html @@ -60,15 +60,8 @@

- +
+
@if (exerciseHint.type === HintType.CODE) {
diff --git a/src/main/webapp/app/exercises/shared/exercise-hint/manage/exercise-hint-update.component.ts b/src/main/webapp/app/exercises/shared/exercise-hint/manage/exercise-hint-update.component.ts index 3ea7292de040..c4c7b78f0496 100644 --- a/src/main/webapp/app/exercises/shared/exercise-hint/manage/exercise-hint-update.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-hint/manage/exercise-hint-update.component.ts @@ -4,8 +4,7 @@ import { ActivatedRoute } from '@angular/router'; import { Observable, Subscription, filter, switchMap } from 'rxjs'; import { AlertService } from 'app/core/util/alert.service'; import { ExerciseHintService } from '../shared/exercise-hint.service'; -import { EditorMode, MarkdownEditorHeight } from 'app/shared/markdown-editor/markdown-editor.component'; -import { KatexCommand } from 'app/shared/markdown-editor/commands/katex.command'; +import { MarkdownEditorHeight } from 'app/shared/markdown-editor/markdown-editor.component'; import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; import { faBan, faCircleNotch, faSave } from '@fortawesome/free-solid-svg-icons'; import { ExerciseHint, HintType } from 'app/entities/hestia/exercise-hint.model'; @@ -23,13 +22,13 @@ import { IrisSettings } from 'app/entities/iris/settings/iris-settings.model'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { ButtonType } from 'app/shared/components/button.component'; import { PROFILE_IRIS } from 'app/app.constants'; +import { MonacoFormulaAction } from 'app/shared/monaco-editor/model/actions/monaco-formula.action'; const DEFAULT_DISPLAY_THRESHOLD = 3; @Component({ selector: 'jhi-exercise-hint-update', templateUrl: './exercise-hint-update.component.html', - styleUrls: ['./exercise-hint.scss'], }) export class ExerciseHintUpdateComponent implements OnInit, OnDestroy { MarkdownEditorHeight = MarkdownEditorHeight; @@ -47,8 +46,7 @@ export class ExerciseHintUpdateComponent implements OnInit, OnDestroy { isGeneratingDescription: boolean; paramSub: Subscription; - domainCommands = [new KatexCommand()]; - editorMode = EditorMode.LATEX; + domainActions = [new MonacoFormulaAction()]; // Icons faCircleNotch = faCircleNotch; diff --git a/src/main/webapp/app/exercises/shared/exercise-hint/manage/exercise-hint.scss b/src/main/webapp/app/exercises/shared/exercise-hint/manage/exercise-hint.scss deleted file mode 100644 index 536b30c1def3..000000000000 --- a/src/main/webapp/app/exercises/shared/exercise-hint/manage/exercise-hint.scss +++ /dev/null @@ -1,5 +0,0 @@ -.hint-form__editor-wrapper { - ::ng-deep .markdown-editor { - height: 300px; - } -} diff --git a/src/main/webapp/app/exercises/shared/structured-grading-criterion/grading-instructions-details/grading-instructions-details.component.html b/src/main/webapp/app/exercises/shared/structured-grading-criterion/grading-instructions-details/grading-instructions-details.component.html index be9cf1f0e6cd..fccbe00c9353 100644 --- a/src/main/webapp/app/exercises/shared/structured-grading-criterion/grading-instructions-details/grading-instructions-details.component.html +++ b/src/main/webapp/app/exercises/shared/structured-grading-criterion/grading-instructions-details/grading-instructions-details.component.html @@ -151,15 +151,17 @@ @if (!showEditMode) {
-
} @@ -172,13 +174,13 @@
-
-
+
@if (criteria.structuredGradingInstructions!) {
- @for (instruction of criteria.structuredGradingInstructions; track instruction) { + @for (instruction of criteria.structuredGradingInstructions; track instruction; let index = $index) {
-
- +
diff --git a/src/main/webapp/app/exercises/shared/structured-grading-criterion/grading-instructions-details/grading-instructions-details.component.scss b/src/main/webapp/app/exercises/shared/structured-grading-criterion/grading-instructions-details/grading-instructions-details.component.scss index 6ec55ecf1d9e..74e64144c104 100644 --- a/src/main/webapp/app/exercises/shared/structured-grading-criterion/grading-instructions-details/grading-instructions-details.component.scss +++ b/src/main/webapp/app/exercises/shared/structured-grading-criterion/grading-instructions-details/grading-instructions-details.component.scss @@ -37,10 +37,6 @@ width: auto; } - .input-group > input { - height: 100%; - } - .form-group { display: flex; align-items: center; diff --git a/src/main/webapp/app/exercises/shared/structured-grading-criterion/grading-instructions-details/grading-instructions-details.component.ts b/src/main/webapp/app/exercises/shared/structured-grading-criterion/grading-instructions-details/grading-instructions-details.component.ts index 0215dcbfe5d5..64448a6f4569 100644 --- a/src/main/webapp/app/exercises/shared/structured-grading-criterion/grading-instructions-details/grading-instructions-details.component.ts +++ b/src/main/webapp/app/exercises/shared/structured-grading-criterion/grading-instructions-details/grading-instructions-details.component.ts @@ -1,18 +1,19 @@ import { AfterContentInit, ChangeDetectorRef, Component, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; import { GradingCriterion } from 'app/exercises/shared/structured-grading-criterion/grading-criterion.model'; -import { UsageCountCommand } from 'app/shared/markdown-editor/domainCommands/usageCount.command'; -import { CreditsCommand } from 'app/shared/markdown-editor/domainCommands/credits.command'; -import { FeedbackCommand } from 'app/shared/markdown-editor/domainCommands/feedback.command'; -import { DomainCommand } from 'app/shared/markdown-editor/domainCommands/domainCommand'; -import { MarkdownEditorComponent } from 'app/shared/markdown-editor/markdown-editor.component'; +import { MarkdownEditorHeight } from 'app/shared/markdown-editor/markdown-editor.component'; import { GradingInstruction } from 'app/exercises/shared/structured-grading-criterion/grading-instruction.model'; -import { GradingScaleCommand } from 'app/shared/markdown-editor/domainCommands/gradingScaleCommand'; -import { GradingInstructionCommand } from 'app/shared/markdown-editor/domainCommands/gradingInstruction.command'; -import { InstructionDescriptionCommand } from 'app/shared/markdown-editor/domainCommands/instructionDescription.command'; -import { GradingCriterionCommand } from 'app/shared/markdown-editor/domainCommands/gradingCriterionCommand'; import { Exercise } from 'app/entities/exercise.model'; import { cloneDeep } from 'lodash-es'; import { faPlus, faTrash, faUndo } from '@fortawesome/free-solid-svg-icons'; +import { MonacoEditorDomainAction } from 'app/shared/monaco-editor/model/actions/monaco-editor-domain-action.model'; +import { MonacoGradingCreditsAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-credits.action'; +import { MonacoGradingScaleAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-scale.action'; +import { MonacoGradingDescriptionAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-description.action'; +import { MonacoGradingFeedbackAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-feedback.action'; +import { MonacoGradingUsageCountAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-usage-count.action'; +import { MarkdownEditorMonacoComponent, TextWithDomainAction } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; +import { MonacoGradingCriterionAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-criterion.action'; +import { MonacoGradingInstructionAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-instruction.action'; @Component({ selector: 'jhi-grading-instructions-details', @@ -23,9 +24,9 @@ export class GradingInstructionsDetailsComponent implements OnInit, AfterContent /** Ace Editor configuration constants **/ markdownEditorText = ''; @ViewChildren('markdownEditors') - private markdownEditors: QueryList; + private markdownEditors: QueryList; @ViewChild('markdownEditor', { static: false }) - private markdownEditor: MarkdownEditorComponent; + private markdownEditor: MarkdownEditorMonacoComponent; @Input() exercise: Exercise; private instructions: GradingInstruction[]; @@ -33,32 +34,32 @@ export class GradingInstructionsDetailsComponent implements OnInit, AfterContent backupExercise: Exercise; - gradingCriterionCommand = new GradingCriterionCommand(); - gradingInstructionCommand = new GradingInstructionCommand(); - creditsCommand = new CreditsCommand(); - gradingScaleCommand = new GradingScaleCommand(); - instructionDescriptionCommand = new InstructionDescriptionCommand(); - feedbackCommand = new FeedbackCommand(); - usageCountCommand = new UsageCountCommand(); + creditsAction = new MonacoGradingCreditsAction(); + gradingScaleAction = new MonacoGradingScaleAction(); + descriptionAction = new MonacoGradingDescriptionAction(); + feedbackAction = new MonacoGradingFeedbackAction(); + usageCountAction = new MonacoGradingUsageCountAction(); + gradingInstructionAction = new MonacoGradingInstructionAction(this.creditsAction, this.gradingScaleAction, this.descriptionAction, this.feedbackAction, this.usageCountAction); + gradingCriterionAction = new MonacoGradingCriterionAction(this.gradingInstructionAction); + + domainActionsForMainEditor = [ + this.creditsAction, + this.gradingScaleAction, + this.descriptionAction, + this.feedbackAction, + this.usageCountAction, + this.gradingInstructionAction, + this.gradingCriterionAction, + ]; showEditMode: boolean; - domainCommands: DomainCommand[] = [ - this.creditsCommand, - this.gradingScaleCommand, - this.instructionDescriptionCommand, - this.feedbackCommand, - this.usageCountCommand, - this.gradingCriterionCommand, - this.gradingInstructionCommand, - ]; - - domainCommandsGradingInstructions: DomainCommand[] = [ - this.creditsCommand, - this.gradingScaleCommand, - this.instructionDescriptionCommand, - this.feedbackCommand, - this.usageCountCommand, + domainActionsForGradingInstructionParsing: MonacoEditorDomainAction[] = [ + this.creditsAction, + this.gradingScaleAction, + this.descriptionAction, + this.feedbackAction, + this.usageCountAction, ]; // Icons @@ -66,6 +67,8 @@ export class GradingInstructionsDetailsComponent implements OnInit, AfterContent faTrash = faTrash; faUndo = faUndo; + protected readonly MarkdownEditorHeight = MarkdownEditorHeight; + constructor(private changeDetector: ChangeDetectorRef) {} ngOnInit() { @@ -87,7 +90,7 @@ export class GradingInstructionsDetailsComponent implements OnInit, AfterContent this.changeDetector.detectChanges(); this.criteria!.forEach((criterion) => { criterion.structuredGradingInstructions.forEach((instruction) => { - this.markdownEditors.get(index)!.markdownTextChange(this.generateInstructionText(instruction)); + this.markdownEditors.get(index)!.markdown = this.generateInstructionText(instruction); index += 1; }); }); @@ -99,10 +102,10 @@ export class GradingInstructionsDetailsComponent implements OnInit, AfterContent if (this.exercise.gradingCriteria) { for (const criterion of this.exercise.gradingCriteria) { if (criterion.title == undefined) { - // if it is a dummy criterion, leave out the command identifier + // if it is a dummy criterion, leave out the action identifier markdownText += this.generateInstructionsMarkdown(criterion); } else { - markdownText += GradingCriterionCommand.IDENTIFIER + criterion.title + '\n' + '\t' + this.generateInstructionsMarkdown(criterion); + markdownText += `${MonacoGradingCriterionAction.IDENTIFIER} ${criterion.title}\n\t${this.generateInstructionsMarkdown(criterion)}`; } } } @@ -130,7 +133,7 @@ export class GradingInstructionsDetailsComponent implements OnInit, AfterContent generateInstructionText(instruction: GradingInstruction): string { let markdownText = ''; markdownText = - GradingInstructionCommand.IDENTIFIER + + MonacoGradingInstructionAction.IDENTIFIER + '\n' + '\t' + this.generateCreditsText(instruction) + @@ -152,59 +155,52 @@ export class GradingInstructionsDetailsComponent implements OnInit, AfterContent } generateCreditsText(instruction: GradingInstruction): string { + const creditsText = MonacoGradingCreditsAction.TEXT; + const creditsIdentifier = MonacoGradingCreditsAction.IDENTIFIER; if (instruction.credits == undefined) { - instruction.credits = parseFloat(CreditsCommand.TEXT); - return CreditsCommand.IDENTIFIER + ' ' + CreditsCommand.TEXT; + instruction.credits = parseFloat(creditsText) || 0; } - return CreditsCommand.IDENTIFIER + ' ' + instruction.credits; + return `${creditsIdentifier} ${instruction.credits || creditsText}`; } generateGradingScaleText(instruction: GradingInstruction): string { if (instruction.gradingScale == undefined) { - instruction.gradingScale = GradingScaleCommand.TEXT; - return GradingScaleCommand.IDENTIFIER + ' ' + GradingScaleCommand.TEXT; + instruction.gradingScale = MonacoGradingScaleAction.TEXT; } - return GradingScaleCommand.IDENTIFIER + ' ' + instruction.gradingScale; + return `${MonacoGradingScaleAction.IDENTIFIER} ${instruction.gradingScale}`; } generateInstructionDescriptionText(instruction: GradingInstruction): string { if (instruction.instructionDescription == undefined) { - instruction.instructionDescription = InstructionDescriptionCommand.TEXT; - return InstructionDescriptionCommand.IDENTIFIER + ' ' + InstructionDescriptionCommand.TEXT; + instruction.instructionDescription = MonacoGradingDescriptionAction.TEXT; } - return InstructionDescriptionCommand.IDENTIFIER + ' ' + instruction.instructionDescription; + return `${MonacoGradingDescriptionAction.IDENTIFIER} ${instruction.instructionDescription}`; } generateInstructionFeedback(instruction: GradingInstruction): string { if (instruction.feedback == undefined) { - instruction.feedback = FeedbackCommand.TEXT; - return FeedbackCommand.IDENTIFIER + ' ' + FeedbackCommand.TEXT; + instruction.feedback = MonacoGradingFeedbackAction.TEXT; } - return FeedbackCommand.IDENTIFIER + ' ' + instruction.feedback; + return `${MonacoGradingFeedbackAction.IDENTIFIER} ${instruction.feedback}`; } generateUsageCount(instruction: GradingInstruction): string { if (instruction.usageCount == undefined) { - instruction.usageCount = parseInt(UsageCountCommand.TEXT, 10); - return UsageCountCommand.IDENTIFIER + ' ' + UsageCountCommand.TEXT; + instruction.usageCount = parseInt(MonacoGradingUsageCountAction.TEXT, 10) || 0; } - return UsageCountCommand.IDENTIFIER + ' ' + instruction.usageCount; + return `${MonacoGradingUsageCountAction.IDENTIFIER} ${instruction.usageCount}`; } initializeExerciseGradingInstructionText(): string { - if (this.exercise.gradingInstructions) { - return this.exercise.gradingInstructions + '\n\n'; - } else { - return 'Add Assessment Instruction text here' + '\n\n'; - } + return `${this.exercise.gradingInstructions || 'Add Assessment Instruction text here'}\n\n`; } prepareForSave(): void { this.cleanupExerciseGradingInstructions(); - this.markdownEditor.parse(); + this.markdownEditor.parseMarkdown(); if (this.exercise.gradingInstructionFeedbackUsed) { this.markdownEditors.forEach((component) => { - component.parse(); + component.parseMarkdown(this.domainActionsForGradingInstructionParsing); }); } } @@ -217,37 +213,33 @@ export class GradingInstructionsDetailsComponent implements OnInit, AfterContent this.exercise.gradingInstructions = undefined; } - hasCriterionCommand(domainCommands: [string, DomainCommand | null][]): boolean { - return domainCommands.some(([, command]) => command instanceof GradingCriterionCommand); + hasCriterionAction(textWithDomainActions: TextWithDomainAction[]): boolean { + return textWithDomainActions.some(({ action }) => action instanceof MonacoGradingCriterionAction); } /** - * @function createSubInstructionCommands - * @desc 1. divides the input: domainCommands in two subarrays: - * instructionCommands, which consists of all stand-alone instructions - * criteriaCommands, which consists of instructions that belong to a criterion - * 2. for each subarrray a method is called to create the criterion and instruction objects - * @param domainCommands containing tuples of [text, domainCommandIdentifiers] + * Creates criterion and instruction objects based on the parsed markdown text. + * @param textWithDomainActions The parsed text segments with their corresponding domain actions. */ - createSubInstructionCommands(domainCommands: [string, DomainCommand | null][]): void { - let instructionCommands; - let criteriaCommands; - let endOfInstructionsCommand = 0; - if (!this.hasCriterionCommand(domainCommands)) { - this.setParentForInstructionsWithNoCriterion(domainCommands); + createSubInstructionActions(textWithDomainActions: TextWithDomainAction[]): void { + let instructionActions; + let criterionActions; + let endOfInstructionsAction = 0; + if (!this.hasCriterionAction(textWithDomainActions)) { + this.setParentForInstructionsWithNoCriterion(textWithDomainActions); } else { - for (const [, command] of domainCommands) { - endOfInstructionsCommand++; - this.setExerciseGradingInstructionText(domainCommands); - if (command instanceof GradingCriterionCommand) { - instructionCommands = domainCommands.slice(0, endOfInstructionsCommand - 1); - if (instructionCommands.length !== 0) { - this.setParentForInstructionsWithNoCriterion(instructionCommands); + for (const { action } of textWithDomainActions) { + endOfInstructionsAction++; + this.setExerciseGradingInstructionText(textWithDomainActions); + if (action instanceof MonacoGradingCriterionAction) { + instructionActions = textWithDomainActions.slice(0, endOfInstructionsAction - 1); + if (instructionActions.length !== 0) { + this.setParentForInstructionsWithNoCriterion(instructionActions); } - criteriaCommands = domainCommands.slice(endOfInstructionsCommand - 1); - if (criteriaCommands.length !== 0) { + criterionActions = textWithDomainActions.slice(endOfInstructionsAction - 1); + if (criterionActions.length !== 0) { this.instructions = []; // resets the instructions array to be filled with the instructions of the criteria - this.groupInstructionsToCriteria(criteriaCommands); // creates criterion object for each criterion and their corresponding instruction objects + this.groupInstructionsToCriteria(criterionActions); } break; } @@ -256,14 +248,13 @@ export class GradingInstructionsDetailsComponent implements OnInit, AfterContent } /** - * @function setParentForInstructionsWithNoCriterion - * @desc 1. creates a dummy criterion object for each stand-alone instruction - * @param domainCommands containing tuples of [text, domainCommandIdentifiers] + * Creates a dummy grading criterion object for each instruction that does not belong to a criterion and assigns the instruction to it. + * @param textWithDomainActions The parsed text segments with their corresponding domain actions. */ - setParentForInstructionsWithNoCriterion(domainCommands: [string, DomainCommand | null][]): void { - for (const [, command] of domainCommands) { - this.setExerciseGradingInstructionText(domainCommands); - if (command instanceof GradingInstructionCommand) { + setParentForInstructionsWithNoCriterion(textWithDomainActions: TextWithDomainAction[]): void { + for (const { action } of textWithDomainActions) { + this.setExerciseGradingInstructionText(textWithDomainActions); + if (action instanceof MonacoGradingInstructionAction) { const dummyCriterion = new GradingCriterion(); const newInstruction = new GradingInstruction(); dummyCriterion.structuredGradingInstructions = []; @@ -273,68 +264,63 @@ export class GradingInstructionsDetailsComponent implements OnInit, AfterContent } } this.exercise.gradingCriteria = this.criteria; - this.setInstructionParameters(domainCommands); + this.setInstructionParameters(textWithDomainActions); } /** - * @function groupInstructionsToCriteria - * @desc 1. creates a criterion for each GradingCriterionCommandIdentifier - * and creates the instruction objects of this criterion then assigns them to their parent criterion - * @param domainCommands containing tuples of [text, domainCommandIdentifiers] + * Creates a grading criterion object for each criterion action found in the parsed markdown text and assigns the corresponding grading instructions to it. + * @param textWithDomainActions The parsed text segments with their corresponding domain actions. */ - groupInstructionsToCriteria(domainCommands: [string, DomainCommand | null][]): void { - const initialCriteriaCommands = domainCommands; + groupInstructionsToCriteria(textWithDomainActions: TextWithDomainAction[]): void { + const initialCriterionActions = textWithDomainActions; if (this.exercise.gradingCriteria == undefined) { this.exercise.gradingCriteria = []; } - for (const [text, command] of domainCommands) { - if (command instanceof GradingCriterionCommand) { + for (const { text, action } of textWithDomainActions) { + if (action instanceof MonacoGradingCriterionAction) { const newCriterion = new GradingCriterion(); newCriterion.title = text; this.exercise.gradingCriteria.push(newCriterion); newCriterion.structuredGradingInstructions = []; - const modifiedArray = domainCommands.slice(1); // remove GradingCriterionCommandIdentifier after creating its criterion object + const arrayWithoutCriterion = textWithDomainActions.slice(1); // remove the identifier after creating its criterion object let endOfCriterion = 0; - for (const [, instrCommand] of modifiedArray) { + for (const remainingTextWithDomainAction of arrayWithoutCriterion) { + const instrAction = remainingTextWithDomainAction.action; endOfCriterion++; - if (instrCommand instanceof GradingInstructionCommand) { + if (instrAction instanceof MonacoGradingInstructionAction) { const newInstruction = new GradingInstruction(); // create instruction objects that belong to the above created criterion newCriterion.structuredGradingInstructions.push(newInstruction); this.instructions.push(newInstruction); } - if (instrCommand instanceof GradingCriterionCommand) { - domainCommands = domainCommands.slice(endOfCriterion, domainCommands.length); + if (instrAction instanceof MonacoGradingCriterionAction) { + textWithDomainActions = textWithDomainActions.slice(endOfCriterion, textWithDomainActions.length); break; } } } } - this.setInstructionParameters(initialCriteriaCommands.filter(([, command]) => !(command instanceof GradingCriterionCommand))); + this.setInstructionParameters(initialCriterionActions.filter(({ action }) => !(action instanceof MonacoGradingCriterionAction))); } /** - * @function setInstructionParameters - * @desc 1. Gets a tuple of text and domainCommandIdentifiers not including GradingCriterionCommandIdentifiers and assigns text values according to the domainCommandIdentifiers - * 2. The tuple order is the same as the order of the commands in the markdown text inserted by the user - * instruction objects must be created before the method gets triggered - * @param domainCommands containing tuples of [text, domainCommandIdentifiers] + * Sets the parameters of the GradingInstruction objects based on the parsed markdown text. Note that the instruction objects must be created before this method is called. + * @param textWithDomainActions The parsed text segments with their corresponding domain actions. */ - - setInstructionParameters(domainCommands: [string, DomainCommand | null][]): void { + setInstructionParameters(textWithDomainActions: TextWithDomainAction[]): void { let index = 0; - for (const [text, command] of domainCommands) { + for (const { text, action } of textWithDomainActions) { if (!this.instructions[index]) { break; } - if (command instanceof CreditsCommand) { + if (action instanceof MonacoGradingCreditsAction) { this.instructions[index].credits = parseFloat(text); - } else if (command instanceof GradingScaleCommand) { + } else if (action instanceof MonacoGradingScaleAction) { this.instructions[index].gradingScale = text; - } else if (command instanceof InstructionDescriptionCommand) { + } else if (action instanceof MonacoGradingDescriptionAction) { this.instructions[index].instructionDescription = text; - } else if (command instanceof FeedbackCommand) { + } else if (action instanceof MonacoGradingFeedbackAction) { this.instructions[index].feedback = text; - } else if (command instanceof UsageCountCommand) { + } else if (action instanceof MonacoGradingUsageCountAction) { this.instructions[index].usageCount = parseInt(text, 10); index++; // index must be increased after the last parameter of the instruction to continue with the next instruction object } @@ -342,28 +328,19 @@ export class GradingInstructionsDetailsComponent implements OnInit, AfterContent } /** - * @function domainCommandsFound - * @desc 1. Gets a tuple of text and domainCommandIdentifiers and assigns text values according to the domainCommandIdentifiers - * 2. The tuple order is the same as the order of the commands in the markdown text inserted by the user - * @param domainCommands containing tuples of [text, domainCommandIdentifiers] + * Updates the grading instructions of the exercise based on the parsed markdown text. + * @param textWithDomainActions The parsed text segments with their corresponding domain actions. */ - domainCommandsFound(domainCommands: [string, DomainCommand | null][]): void { + onDomainActionsFound(textWithDomainActions: TextWithDomainAction[]): void { this.instructions = []; this.criteria = []; this.exercise.gradingCriteria = []; - this.createSubInstructionCommands(domainCommands); + this.createSubInstructionActions(textWithDomainActions); } - /** - * @function onInstructionChange - * @desc 1. Gets a tuple of text and domainCommandIdentifiers and assigns text values according to the domainCommandIdentifiers - * 2. The tuple order is the same as the order of the commands in the markdown text inserted by the user - * @param domainCommands containing tuples of [text, domainCommandIdentifiers] - * @param {GradingInstruction} instruction - */ - onInstructionChange(domainCommands: [string, DomainCommand | null][], instruction: GradingInstruction): void { + onInstructionChange(textWithDomainActions: TextWithDomainAction[], instruction: GradingInstruction): void { this.instructions = [instruction]; - this.setInstructionParameters(domainCommands); + this.setInstructionParameters(textWithDomainActions); } /** @@ -423,9 +400,8 @@ export class GradingInstructionsDetailsComponent implements OnInit, AfterContent } /** - * @function addNewInstruction - * @desc Adds new grading instruction for desired grading criteria - * @param criterion {GradingCriterion} the criteria, which includes the instruction that will be inserted + * Adds a new grading instruction for the specified grading criterion. + * @param criterion The grading criterion that contains the instruction to insert. */ addNewInstruction(criterion: GradingCriterion) { const criterionIndex = this.exercise.gradingCriteria!.indexOf(criterion); @@ -438,10 +414,6 @@ export class GradingInstructionsDetailsComponent implements OnInit, AfterContent this.initializeMarkdown(); } - /** - * @function addNewGradingCriteria - * @desc Adds new grading criteria for the exercise - */ addNewGradingCriterion() { const criterion = new GradingCriterion(); criterion.structuredGradingInstructions = []; @@ -453,21 +425,11 @@ export class GradingInstructionsDetailsComponent implements OnInit, AfterContent } } - /** - * @function onCriteriaTitleChange - * @desc Detects changes for grading criteria title - * @param {GradingCriterion} criterion the criteria, which includes title that will be changed - */ onCriterionTitleChange($event: any, criterion: GradingCriterion) { const criterionIndex = this.exercise.gradingCriteria!.indexOf(criterion); this.exercise.gradingCriteria![criterionIndex].title = $event.target.value; } - /** - * @function resetCriteriaTitle - * @desc Resets the whole grading criteria title - * @param criterion {GradingCriterion} the criteria, which includes title that will be reset - */ resetCriterionTitle(criterion: GradingCriterion) { const criterionIndex = this.findCriterionIndex(criterion, this.exercise); const backupCriterionIndex = this.findCriterionIndex(criterion, this.backupExercise); @@ -478,24 +440,21 @@ export class GradingInstructionsDetailsComponent implements OnInit, AfterContent } } - /** - * @function deleteGradingCriteria - * @desc Deletes the grading criteria with sub-grading instructions - * @param criterion {GradingCriterion} the criteria, which will be deleted - */ deleteGradingCriterion(criterion: GradingCriterion) { const criterionIndex = this.exercise.gradingCriteria!.indexOf(criterion); this.exercise.gradingCriteria!.splice(criterionIndex, 1); } /** - * @function setExerciseGradingInstructionText - * @desc Gets a tuple of text and domainCommandIdentifiers and assigns text values as grading instructions of exercise - * @param domainCommands containing tuples of [text, domainCommandIdentifiers] + * Extracts the exercise grading instruction text from the start of the parsed markdown text. + * @param textWithDomainActions The parsed text segments with their corresponding domain actions. */ - setExerciseGradingInstructionText(domainCommands: [string, DomainCommand | null][]): void { - const [text, command] = domainCommands[0]; - if (command === null && text.length > 0) { + setExerciseGradingInstructionText(textWithDomainActions: TextWithDomainAction[]): void { + if (!textWithDomainActions.length) { + return; + } + const { text, action } = textWithDomainActions[0]; + if (action === undefined && text.length > 0) { this.exercise.gradingInstructions = text; } } @@ -509,12 +468,6 @@ export class GradingInstructionsDetailsComponent implements OnInit, AfterContent this.markdownEditorText = this.generateMarkdown(); } - /** - * Updates given grading instruction in exercise - * - * @param instruction needs to be updated - * @param criterion includes instruction needs to be updated - */ updateGradingInstruction(instruction: GradingInstruction, criterion: GradingCriterion) { const criterionIndex = this.exercise.gradingCriteria!.indexOf(criterion); const instructionIndex = this.exercise.gradingCriteria![criterionIndex].structuredGradingInstructions.indexOf(instruction); diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.html b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.html index 2e5a417b7c41..5df2017d2538 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.html +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.html @@ -33,12 +33,11 @@

-
@@ -57,12 +56,11 @@

-
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 21011cacb571..e55e5fb28db2 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 @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { HttpErrorResponse } from '@angular/common/http'; import { TextExercise } from 'app/entities/text-exercise.model'; @@ -7,8 +7,6 @@ import { CourseManagementService } from 'app/course/manage/course-management.ser import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { AssessmentType } from 'app/entities/assessment-type.model'; import { ExerciseMode, IncludedInOverallScore, resetForImport } from 'app/entities/exercise.model'; -import { EditorMode } from 'app/shared/markdown-editor/markdown-editor.component'; -import { KatexCommand } from 'app/shared/markdown-editor/commands/katex.command'; import { switchMap, tap } from 'rxjs/operators'; import { ExerciseGroupService } from 'app/exam/manage/exercise-groups/exercise-group.service'; import { NgForm, NgModel } from '@angular/forms'; @@ -31,10 +29,12 @@ import { FormSectionStatus } from 'app/forms/form-status-bar/form-status-bar.com import { ExerciseUpdatePlagiarismComponent } from 'app/exercises/shared/plagiarism/exercise-update-plagiarism/exercise-update-plagiarism.component'; import { TeamConfigFormGroupComponent } from 'app/exercises/shared/team-config-form-group/team-config-form-group.component'; import { FormDateTimePickerComponent } from 'app/shared/date-time-picker/date-time-picker.component'; +import { MonacoFormulaAction } from 'app/shared/monaco-editor/model/actions/monaco-formula.action'; @Component({ selector: 'jhi-text-exercise-update', templateUrl: './text-exercise-update.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, }) export class TextExerciseUpdateComponent implements OnInit, OnDestroy, AfterViewInit { readonly IncludedInOverallScore = IncludedInOverallScore; @@ -56,7 +56,6 @@ export class TextExerciseUpdateComponent implements OnInit, OnDestroy, AfterView isExamMode: boolean; isImport = false; goBackAfterSaving = false; - EditorMode = EditorMode; AssessmentType = AssessmentType; isAthenaEnabled$: Observable | undefined; @@ -67,8 +66,8 @@ export class TextExerciseUpdateComponent implements OnInit, OnDestroy, AfterView existingCategories: ExerciseCategory[]; notificationText?: string; - domainCommandsProblemStatement = [new KatexCommand()]; - domainCommandsSampleSolution = [new KatexCommand()]; + domainActionsProblemStatement = [new MonacoFormulaAction()]; + domainActionsExampleSolution = [new MonacoFormulaAction()]; formSectionStatus: FormSectionStatus[]; diff --git a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/text-unit-form/text-unit-form.component.html b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/text-unit-form/text-unit-form.component.html index 0bf5784ea593..7af504cfd0aa 100644 --- a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/text-unit-form/text-unit-form.component.html +++ b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/text-unit-form/text-unit-form.component.html @@ -44,7 +44,13 @@ GitHub Markdown Guide - +
diff --git a/src/main/webapp/app/lecture/lecture-update.component.html b/src/main/webapp/app/lecture/lecture-update.component.html index 67f423adda06..25a522f09f4f 100644 --- a/src/main/webapp/app/lecture/lecture-update.component.html +++ b/src/main/webapp/app/lecture/lecture-update.component.html @@ -30,12 +30,7 @@

- +
diff --git a/src/main/webapp/app/lecture/lecture-update.component.ts b/src/main/webapp/app/lecture/lecture-update.component.ts index 592d937228cb..e0ac895d50ce 100644 --- a/src/main/webapp/app/lecture/lecture-update.component.ts +++ b/src/main/webapp/app/lecture/lecture-update.component.ts @@ -6,15 +6,14 @@ 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 { EditorMode } from 'app/shared/markdown-editor/markdown-editor.component'; import { Course } from 'app/entities/course.model'; -import { KatexCommand } from 'app/shared/markdown-editor/commands/katex.command'; import { onError } from 'app/shared/util/global.utils'; 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 { FILE_EXTENSIONS } from 'app/shared/constants/file-extensions.constants'; +import { MonacoFormulaAction } from 'app/shared/monaco-editor/model/actions/monaco-formula.action'; @Component({ selector: 'jhi-lecture-update', @@ -26,7 +25,6 @@ export class LectureUpdateComponent implements OnInit { @ViewChild(LectureUpdateWizardComponent, { static: false }) wizardComponent: LectureUpdateWizardComponent; - EditorMode = EditorMode; lecture: Lecture; isSaving: boolean; isProcessing: boolean; @@ -35,7 +33,7 @@ export class LectureUpdateComponent implements OnInit { courses: Course[]; - domainCommandsDescription = [new KatexCommand()]; + domainActionsDescription = [new MonacoFormulaAction()]; file: File; fileName: string; fileInputTouched = false; diff --git a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-title.component.html b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-title.component.html index 9656e52daf04..9100d8436184 100644 --- a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-title.component.html +++ b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-title.component.html @@ -6,6 +6,6 @@

- +

diff --git a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-title.component.ts b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-title.component.ts index 8748b9232417..c8b89b284ffb 100644 --- a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-title.component.ts +++ b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-title.component.ts @@ -1,7 +1,6 @@ import { Component, Input } from '@angular/core'; -import { KatexCommand } from 'app/shared/markdown-editor/commands/katex.command'; -import { EditorMode } from 'app/shared/markdown-editor/markdown-editor.component'; import { Lecture } from 'app/entities/lecture.model'; +import { MonacoFormulaAction } from 'app/shared/monaco-editor/model/actions/monaco-formula.action'; @Component({ selector: 'jhi-lecture-update-wizard-title', @@ -11,8 +10,7 @@ export class LectureUpdateWizardTitleComponent { @Input() currentStep: number; @Input() lecture: Lecture; - domainCommandsDescription = [new KatexCommand()]; - EditorMode = EditorMode; + domainActionsDescription = [new MonacoFormulaAction()]; constructor() {} } diff --git a/src/main/webapp/app/shared/markdown-editor/markdown-editor.component.scss b/src/main/webapp/app/shared/markdown-editor/markdown-editor.component.scss index 008cc9349fe4..935730fc5c81 100644 --- a/src/main/webapp/app/shared/markdown-editor/markdown-editor.component.scss +++ b/src/main/webapp/app/shared/markdown-editor/markdown-editor.component.scss @@ -108,7 +108,7 @@ } .background-editor-high { - overflow: scroll; + overflow: auto; } .dropdown-menu { diff --git a/src/main/webapp/app/shared/markdown-editor/markdown-editor.component.ts b/src/main/webapp/app/shared/markdown-editor/markdown-editor.component.ts index 1795b4718b6e..77910b630a20 100644 --- a/src/main/webapp/app/shared/markdown-editor/markdown-editor.component.ts +++ b/src/main/webapp/app/shared/markdown-editor/markdown-editor.component.ts @@ -41,7 +41,7 @@ import { InteractiveSearchCommand } from 'app/shared/markdown-editor/commands/in export enum MarkdownEditorHeight { INLINE = 100, - SMALL = 200, + SMALL = 300, MEDIUM = 500, LARGE = 1000, EXTRA_LARGE = 1500, diff --git a/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-monaco.component.html b/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-monaco.component.html index 075014915cf8..52d80f585633 100644 --- a/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-monaco.component.html +++ b/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-monaco.component.html @@ -2,17 +2,18 @@
- @if (enableResize && !inPreviewMode) { + @if (enableResize && inEditMode) {
(); + @Output() + onBlurEditor = new EventEmitter(); + + @Output() + textWithDomainActionsFound = new EventEmitter(); + + @Output() + onDefaultPreviewHtmlChanged = new EventEmitter(); + + @Output() + onLeaveVisualTab = new EventEmitter(); + + defaultPreviewHtml: SafeHtml | undefined; inPreviewMode = false; + inVisualMode = false; + inEditMode = true; uniqueMarkdownEditorId: string; resizeObserver?: ResizeObserver; targetWrapperHeight?: number; + minWrapperHeight?: number; constrainDragPositionFn?: (pointerPosition: Point) => Point; isResizing = false; displayedActions: MarkdownActionsByGroup = { @@ -186,19 +222,27 @@ export class MarkdownEditorMonacoComponent implements AfterContentInit, AfterVie colorSignal: Signal = computed(() => [...this.colorToClassMap.keys()]); + static readonly TAB_EDIT = 'editor_edit'; + static readonly TAB_PREVIEW = 'editor_preview'; + static readonly TAB_VISUAL = 'editor_visual'; readonly colorPickerMarginTop = 35; readonly colorPickerHeight = 110; // Icons protected readonly faQuestionCircle = faQuestionCircle; protected readonly faGripLines = faGripLines; protected readonly faAngleDown = faAngleDown; - // Types exposed to the template + // Types and values exposed to the template protected readonly LectureUnitType = LectureUnitType; protected readonly ReferenceType = ReferenceType; + // We cannot reference these static fields in the template, so we expose them here. + protected readonly TAB_EDIT = MarkdownEditorMonacoComponent.TAB_EDIT; + protected readonly TAB_PREVIEW = MarkdownEditorMonacoComponent.TAB_PREVIEW; + protected readonly TAB_VISUAL = MarkdownEditorMonacoComponent.TAB_VISUAL; constructor( private alertService: AlertService, private fileUploaderService: FileUploaderService, + private artemisMarkdown: ArtemisMarkdownService, ) { this.uniqueMarkdownEditorId = 'markdown-editor-' + uuid(); } @@ -206,24 +250,33 @@ export class MarkdownEditorMonacoComponent implements AfterContentInit, AfterVie ngAfterContentInit(): void { // Affects the template - done in this method to avoid ExpressionChangedAfterItHasBeenCheckedErrors. this.targetWrapperHeight = this.initialEditorHeight !== EXTERNAL_HEIGHT ? this.initialEditorHeight.valueOf() : undefined; + this.minWrapperHeight = this.resizableMinHeight.valueOf(); this.constrainDragPositionFn = this.constrainDragPosition.bind(this); this.displayedActions = { - standard: this.defaultActions, - header: this.headerActions?.actions ?? [], - color: this.colorAction, + standard: this.filterDisplayedActions(this.defaultActions), + header: this.filterDisplayedActions(this.headerActions?.actions ?? []), + color: this.filterDisplayedAction(this.colorAction), domain: { - withoutOptions: this.domainActions.filter((action) => !(action instanceof MonacoEditorDomainActionWithOptions)), - withOptions: this.domainActions.filter((action) => action instanceof MonacoEditorDomainActionWithOptions), + withoutOptions: this.filterDisplayedActions(this.domainActions.filter((action) => !(action instanceof MonacoEditorDomainActionWithOptions))), + withOptions: this.filterDisplayedActions(this.domainActions.filter((action) => action instanceof MonacoEditorDomainActionWithOptions)), }, - lecture: this.lectureReferenceAction, - meta: this.metaActions, + lecture: this.filterDisplayedAction(this.lectureReferenceAction), + meta: this.filterDisplayedActions(this.metaActions), }; } + filterDisplayedActions(actions: T[]): T[] { + return actions.filter((action) => !action.hideInEditor); + } + + filterDisplayedAction(action?: T): T | undefined { + return action?.hideInEditor ? undefined : action; + } + ngAfterViewInit(): void { this.adjustEditorDimensions(); this.monacoEditor.setWordWrap(true); - this.monacoEditor.changeModel('markdown-content.custom-md', this._markdown, 'custom-md'); + this.monacoEditor.changeModel('markdown-content.custom-md', this._markdown ?? '', 'custom-md'); this.resizeObserver = new ResizeObserver(() => { this.adjustEditorDimensions(); }); @@ -232,6 +285,9 @@ export class MarkdownEditorMonacoComponent implements AfterContentInit, AfterVie if (this.fileUploadFooter?.nativeElement) { this.resizeObserver.observe(this.fileUploadFooter.nativeElement); } + if (this.actionPalette?.nativeElement) { + this.resizeObserver.observe(this.actionPalette.nativeElement); + } [ this.defaultActions, this.headerActions?.actions ?? [], @@ -248,6 +304,10 @@ export class MarkdownEditorMonacoComponent implements AfterContentInit, AfterVie } this.monacoEditor.registerAction(action); }); + + if (this.useDefaultMarkdownEditorOptions) { + this.monacoEditor.applyOptionPreset(DEFAULT_MARKDOWN_EDITOR_OPTIONS); + } } /** @@ -289,9 +349,9 @@ export class MarkdownEditorMonacoComponent implements AfterContentInit, AfterVie * Adjusts the height of the element when the content height changes. * @param newContentHeight The new height of the content in the editor. */ - onContentHeightChanged(newContentHeight: number): void { + onContentHeightChanged(newContentHeight: number | undefined): void { if (this.linkEditorHeightToContentHeight) { - const totalHeight = newContentHeight + this.getElementClientHeight(this.fileUploadFooter) + this.getElementClientHeight(this.actionPalette); + const totalHeight = (newContentHeight ?? 0) + this.getElementClientHeight(this.fileUploadFooter) + this.getElementClientHeight(this.actionPalette); // Clamp the height so it is between the minimum and maximum height. this.targetWrapperHeight = Math.max(this.resizableMinHeight, Math.min(this.resizableMaxHeight, totalHeight)); } @@ -305,7 +365,7 @@ export class MarkdownEditorMonacoComponent implements AfterContentInit, AfterVie const elementHeight = this.getElementClientHeight(this.isInitialHeightExternal() ? this.fullElement : this.wrapper); const fileUploadFooterHeight = this.getElementClientHeight(this.fileUploadFooter); const actionPaletteHeight = this.getElementClientHeight(this.actionPalette); - return elementHeight - fileUploadFooterHeight - actionPaletteHeight; + return elementHeight - fileUploadFooterHeight - actionPaletteHeight - BORDER_HEIGHT_OFFSET; } /** @@ -327,7 +387,9 @@ export class MarkdownEditorMonacoComponent implements AfterContentInit, AfterVie * Adjust the dimensions of the editor to fit the available space. */ adjustEditorDimensions(): void { - this.monacoEditor.layoutWithFixedSize(this.getEditorWidth(), this.getEditorHeight()); + this.onContentHeightChanged(this.monacoEditor.getContentHeight()); + const editorHeight = this.getEditorHeight(); + this.monacoEditor.layoutWithFixedSize(this.getEditorWidth(), editorHeight); } /** @@ -335,13 +397,36 @@ export class MarkdownEditorMonacoComponent implements AfterContentInit, AfterVie * @param event The event that contains the new active tab. */ onNavChanged(event: NgbNavChangeEvent) { - this.inPreviewMode = event.nextId === 'editor_preview'; - if (!this.inPreviewMode) { + this.inPreviewMode = event.nextId === this.TAB_PREVIEW; + this.inVisualMode = event.nextId === this.TAB_VISUAL; + this.inEditMode = event.nextId === this.TAB_EDIT; + if (this.inEditMode) { this.adjustEditorDimensions(); this.monacoEditor.focus(); - } else { + this.onEditSelect.emit(); + } else if (this.inPreviewMode) { this.onPreviewSelect.emit(); } + + // Some components need to know when the user leaves the visual tab, as it might make changes to the underlying data. + if (event.activeId === this.TAB_VISUAL) { + this.onLeaveVisualTab.emit(); + } + + // Parse the markdown when switching away from the edit tab or from visual to preview mode, as the visual mode may make changes to the markdown. + if (event.activeId === this.TAB_EDIT || (event.activeId === this.TAB_VISUAL && this.inPreviewMode)) { + this.parseMarkdown(); + } + } + + parseMarkdown(domainActionsToCheck: MonacoEditorDomainAction[] = this.domainActions): void { + if (this.showDefaultPreview) { + this.defaultPreviewHtml = this.artemisMarkdown.safeHtmlForMarkdown(this._markdown); + this.onDefaultPreviewHtmlChanged.emit(this.defaultPreviewHtml); + } + if (domainActionsToCheck.length && this._markdown) { + this.textWithDomainActionsFound.emit(parseMarkdownForDomainActions(this._markdown, domainActionsToCheck)); + } } /** @@ -434,7 +519,7 @@ export class MarkdownEditorMonacoComponent implements AfterContentInit, AfterVie * Enable the text field mode of the editor. This makes the editor look and behave like a normal text field. */ enableTextFieldMode(): void { - this.monacoEditor.enableTextFieldMode(); + this.monacoEditor.applyOptionPreset(COMMUNICATION_MARKDOWN_EDITOR_OPTIONS); } /** diff --git a/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-parsing.helper.ts b/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-parsing.helper.ts new file mode 100644 index 000000000000..058cacf8c2ba --- /dev/null +++ b/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-parsing.helper.ts @@ -0,0 +1,61 @@ +import { MonacoEditorDomainAction } from 'app/shared/monaco-editor/model/actions/monaco-editor-domain-action.model'; +import { escapeStringForUseInRegex } from 'app/shared/util/global.utils'; +import { TextWithDomainAction } from './markdown-editor-monaco.component'; + +/** + * Searches for the domain actions in the given markdown text and returns the text split into parts, each part associated with the domain action it belongs to. + * Note that the text will be trimmed before being returned. + * @param markdown The markdown text to parse + * @param domainActions The domain actions to search for in the markdown + */ +export function parseMarkdownForDomainActions(markdown: string, domainActions: MonacoEditorDomainAction[]): TextWithDomainAction[] { + let remainingText = markdown; + const actionIdentifiersString = domainActions + .map((action) => action.getOpeningIdentifier()) + .map((identifier) => identifier.replace('[', '').replace(']', '')) + .map(escapeStringForUseInRegex) + .join('|'); + const textMappedToActionIdentifiers: TextWithDomainAction[] = []; + + /* + * The following regex is used to split the text into parts, each part associated with the domain action it belongs to. It is structured as follows: + * 1. (?= If an action is found, add the action identifier to the result of the split + * 2. \\[ look for the character '[' to determine the beginning of the action identifier + * 3. (${actionIdentifiersString}) look if after the '[' one of the element of actionIdentifiersString is contained + * 4. ] look for the character ']' to determine the end of the action identifier + * 5. ) ends the group + * Flags: + * - g: search in the whole string + * - m: match the regex over multiple lines + * - i: case-insensitive matching + */ + const regex = new RegExp(`(?=\\[(${actionIdentifiersString})])`, 'gmi'); + while (remainingText.length) { + const [textWithActionIdentifier] = remainingText.split(regex, 1); + remainingText = remainingText.substring(textWithActionIdentifier.length); + const textWithDomainAction = parseLineForDomainAction(textWithActionIdentifier.trim(), domainActions); + textMappedToActionIdentifiers.push(textWithDomainAction); + } + + return textMappedToActionIdentifiers; +} + +/** + * Checks if the given line contains any of the domain action identifiers and returns the text without the identifier along with the domain action it belongs to. + * @param text The text to parse + * @param domainActions The domain actions to search for in the text + */ +function parseLineForDomainAction(text: string, domainActions: MonacoEditorDomainAction[]): TextWithDomainAction { + for (const domainAction of domainActions) { + const possibleOpeningIdentifiers = [ + domainAction.getOpeningIdentifier(), + domainAction.getOpeningIdentifier().toLowerCase(), + domainAction.getOpeningIdentifier().toUpperCase(), + ]; + if (possibleOpeningIdentifiers.some((identifier) => text.indexOf(identifier) !== -1)) { + const trimmedLineWithoutIdentifier = possibleOpeningIdentifiers.reduce((line, identifier) => line.replace(identifier, ''), text).trim(); + return { text: trimmedLineWithoutIdentifier, action: domainAction }; + } + } + return { text: text.trim() }; +} diff --git a/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.html b/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.html index fb18ef521aa2..cd20f9b54615 100644 --- a/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.html +++ b/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.html @@ -12,6 +12,8 @@ [enableFileUpload]="false" [colorAction]="undefined" [enableResize]="false" + [showDefaultPreview]="false" + [useDefaultMarkdownEditorOptions]="false" [linkEditorHeightToContentHeight]="true" [initialEditorHeight]="editorHeight" [resizableMinHeight]="editorHeight" diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/communication/monaco-exercise-reference.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/communication/monaco-exercise-reference.action.ts index d9e2024b7513..014894dedd42 100644 --- a/src/main/webapp/app/shared/monaco-editor/model/actions/communication/monaco-exercise-reference.action.ts +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/communication/monaco-exercise-reference.action.ts @@ -59,4 +59,8 @@ export class MonacoExerciseReferenceAction extends MonacoEditorDomainActionWithO super.dispose(); this.disposableCompletionProvider?.dispose(); } + + getOpeningIdentifier(): string { + return '[exercise]'; + } } diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-credits.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-credits.action.ts new file mode 100644 index 000000000000..200a239de9ee --- /dev/null +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-credits.action.ts @@ -0,0 +1,20 @@ +import { MonacoEditorDomainAction } from 'app/shared/monaco-editor/model/actions/monaco-editor-domain-action.model'; +import * as monaco from 'monaco-editor'; + +export class MonacoGradingCreditsAction extends MonacoEditorDomainAction { + static readonly ID = 'monaco-grading-credits.action'; + static readonly IDENTIFIER = '[credits]'; + static readonly TEXT = '0'; + + constructor() { + super(MonacoGradingCreditsAction.ID, 'artemisApp.assessmentInstructions.instructions.editor.addCredits', undefined, undefined, true); + } + + run(editor: monaco.editor.ICodeEditor): void { + this.addTextWithDomainActionIdentifier(editor, MonacoGradingCreditsAction.TEXT, true, false); + } + + getOpeningIdentifier(): string { + return MonacoGradingCreditsAction.IDENTIFIER; + } +} diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-criterion.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-criterion.action.ts new file mode 100644 index 000000000000..02302850bca5 --- /dev/null +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-criterion.action.ts @@ -0,0 +1,22 @@ +import * as monaco from 'monaco-editor'; +import { MonacoEditorDomainAction } from '../monaco-editor-domain-action.model'; +import { MonacoGradingInstructionAction } from './monaco-grading-instruction.action'; + +export class MonacoGradingCriterionAction extends MonacoEditorDomainAction { + static readonly ID = 'monaco-grading-criterion.action'; + static readonly IDENTIFIER = '[criterion]'; + static readonly TEXT = 'Add criterion title (only visible to tutors)'; + + constructor(private readonly gradingInstructionAction: MonacoGradingInstructionAction) { + super(MonacoGradingCriterionAction.ID, 'artemisApp.assessmentInstructions.instructions.editor.addCriterion'); + } + + run(editor: monaco.editor.ICodeEditor): void { + this.addTextWithDomainActionIdentifier(editor, MonacoGradingCriterionAction.TEXT, false, false); + this.gradingInstructionAction.executeInCurrentEditor(); + } + + getOpeningIdentifier(): string { + return MonacoGradingCriterionAction.IDENTIFIER; + } +} diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-description.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-description.action.ts new file mode 100644 index 000000000000..f04d861cb4d7 --- /dev/null +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-description.action.ts @@ -0,0 +1,20 @@ +import * as monaco from 'monaco-editor'; +import { MonacoEditorDomainAction } from 'app/shared/monaco-editor/model/actions/monaco-editor-domain-action.model'; + +export class MonacoGradingDescriptionAction extends MonacoEditorDomainAction { + static readonly ID = 'monaco-grading-description.action'; + static readonly IDENTIFIER = '[description]'; + static readonly TEXT = 'Add grading instruction here (only visible for tutors)'; + + constructor() { + super(MonacoGradingDescriptionAction.ID, 'artemisApp.assessmentInstructions.instructions.editor.addDescription', undefined, undefined, true); + } + + run(editor: monaco.editor.ICodeEditor): void { + this.addTextWithDomainActionIdentifier(editor, MonacoGradingDescriptionAction.TEXT, true, false); + } + + getOpeningIdentifier(): string { + return MonacoGradingDescriptionAction.IDENTIFIER; + } +} diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-feedback.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-feedback.action.ts new file mode 100644 index 000000000000..2dc1306eeffd --- /dev/null +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-feedback.action.ts @@ -0,0 +1,20 @@ +import * as monaco from 'monaco-editor'; +import { MonacoEditorDomainAction } from 'app/shared/monaco-editor/model/actions/monaco-editor-domain-action.model'; + +export class MonacoGradingFeedbackAction extends MonacoEditorDomainAction { + static readonly ID = 'monaco-grading-feedback.action'; + static readonly IDENTIFIER = '[feedback]'; + static readonly TEXT = 'Add feedback for students here (visible for students)'; + + constructor() { + super(MonacoGradingFeedbackAction.ID, 'artemisApp.assessmentInstructions.instructions.editor.addFeedback', undefined, undefined, true); + } + + run(editor: monaco.editor.ICodeEditor): void { + this.addTextWithDomainActionIdentifier(editor, MonacoGradingFeedbackAction.TEXT, true, false); + } + + getOpeningIdentifier(): string { + return MonacoGradingFeedbackAction.IDENTIFIER; + } +} diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-instruction.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-instruction.action.ts new file mode 100644 index 000000000000..fa14b18da454 --- /dev/null +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-instruction.action.ts @@ -0,0 +1,35 @@ +import * as monaco from 'monaco-editor'; +import { MonacoEditorDomainAction } from '../monaco-editor-domain-action.model'; +import { MonacoGradingCreditsAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-credits.action'; +import { MonacoGradingScaleAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-scale.action'; +import { MonacoGradingDescriptionAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-description.action'; +import { MonacoGradingFeedbackAction } from './monaco-grading-feedback.action'; +import { MonacoGradingUsageCountAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-usage-count.action'; + +export class MonacoGradingInstructionAction extends MonacoEditorDomainAction { + static readonly ID = 'monaco-grading-instruction.action'; + static readonly IDENTIFIER = '[instruction]'; + + constructor( + private readonly creditsAction: MonacoGradingCreditsAction, + private readonly scaleAction: MonacoGradingScaleAction, + private readonly descriptionAction: MonacoGradingDescriptionAction, + private readonly feedbackAction: MonacoGradingFeedbackAction, + private readonly usageCountAction: MonacoGradingUsageCountAction, + ) { + super(MonacoGradingInstructionAction.ID, 'artemisApp.assessmentInstructions.instructions.editor.addInstruction'); + } + + run(editor: monaco.editor.ICodeEditor): void { + this.addTextWithDomainActionIdentifier(editor, '', false, false); + this.creditsAction.executeInCurrentEditor(); + this.scaleAction.executeInCurrentEditor(); + this.descriptionAction.executeInCurrentEditor(); + this.feedbackAction.executeInCurrentEditor(); + this.usageCountAction.executeInCurrentEditor(); + } + + getOpeningIdentifier(): string { + return MonacoGradingInstructionAction.IDENTIFIER; + } +} diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-scale.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-scale.action.ts new file mode 100644 index 000000000000..53e1bd2b9485 --- /dev/null +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-scale.action.ts @@ -0,0 +1,20 @@ +import { MonacoEditorDomainAction } from 'app/shared/monaco-editor/model/actions/monaco-editor-domain-action.model'; +import * as monaco from 'monaco-editor'; + +export class MonacoGradingScaleAction extends MonacoEditorDomainAction { + static readonly ID = 'monaco-grading-scale.action'; + static readonly IDENTIFIER = '[gradingScale]'; + static readonly TEXT = 'Add instruction grading scale here (only visible for tutors)'; + + constructor() { + super(MonacoGradingScaleAction.ID, 'artemisApp.assessmentInstructions.instructions.editor.addScale', undefined, undefined, true); + } + + run(editor: monaco.editor.ICodeEditor): void { + this.addTextWithDomainActionIdentifier(editor, MonacoGradingScaleAction.TEXT, true, false); + } + + getOpeningIdentifier(): string { + return MonacoGradingScaleAction.IDENTIFIER; + } +} diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-usage-count.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-usage-count.action.ts new file mode 100644 index 000000000000..b68700d3e094 --- /dev/null +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-usage-count.action.ts @@ -0,0 +1,20 @@ +import * as monaco from 'monaco-editor'; +import { MonacoEditorDomainAction } from 'app/shared/monaco-editor/model/actions/monaco-editor-domain-action.model'; + +export class MonacoGradingUsageCountAction extends MonacoEditorDomainAction { + static readonly ID = 'monaco-grading-usage-count.action'; + static readonly IDENTIFIER = '[maxCountInScore]'; + static readonly TEXT = '0'; + + constructor() { + super(MonacoGradingUsageCountAction.ID, 'artemisApp.assessmentInstructions.instructions.editor.addUsageCount', undefined, undefined, true); + } + + run(editor: monaco.editor.ICodeEditor): void { + this.addTextWithDomainActionIdentifier(editor, MonacoGradingUsageCountAction.TEXT, true, false); + } + + getOpeningIdentifier(): string { + return MonacoGradingUsageCountAction.IDENTIFIER; + } +} diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/monaco-editor-action.model.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/monaco-editor-action.model.ts index 8a022d32b7fc..07c6c89e4ac5 100644 --- a/src/main/webapp/app/shared/monaco-editor/model/actions/monaco-editor-action.model.ts +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/monaco-editor-action.model.ts @@ -11,6 +11,7 @@ export abstract class MonacoEditorAction implements monaco.editor.IActionDescrip keybindings?: number[]; icon?: IconDefinition; + readonly hideInEditor: boolean; /** * The disposable action that is returned by `editor.addAction`. This is required to unregister the action from the editor. @@ -28,12 +29,14 @@ export abstract class MonacoEditorAction implements monaco.editor.IActionDescrip * @param translationKey The translation key of the action label. * @param icon The icon to display in the editor toolbar, if any. * @param keybindings The keybindings to trigger the action, if any. + * @param hideInEditor Whether to hide the action in the editor toolbar. Defaults to false. */ - constructor(id: string, translationKey: string, icon?: IconDefinition, keybindings?: number[]) { + constructor(id: string, translationKey: string, icon?: IconDefinition, keybindings?: number[], hideInEditor?: boolean) { this.id = id; this.translationKey = translationKey; this.icon = icon; this.keybindings = keybindings; + this.hideInEditor = hideInEditor ?? false; } /** @@ -61,6 +64,7 @@ export abstract class MonacoEditorAction implements monaco.editor.IActionDescrip */ dispose(): void { this.disposableAction?.dispose(); + this.disposableAction = undefined; this._editor = undefined; } @@ -321,6 +325,10 @@ export abstract class MonacoEditorAction implements monaco.editor.IActionDescrip } } + getPosition(editor: monaco.editor.ICodeEditor): monaco.IPosition { + return editor.getPosition() ?? { lineNumber: 1, column: 1 }; + } + /** * Sets the selection of the given editor to the given range and reveals it in the center of the editor. * @param editor The editor to set the selection in. @@ -331,6 +339,24 @@ export abstract class MonacoEditorAction implements monaco.editor.IActionDescrip editor.revealRangeInCenter(selection); } + /** + * Clears the current selection in the given editor, but preserves the cursor position. + * @param editor The editor to clear the selection in. + */ + clearSelection(editor: monaco.editor.ICodeEditor): void { + const position = this.getPosition(editor); + this.setSelection(editor, new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column)); + } + + /** + * Adjusts the cursor position so it is at the end of the current line. + * @param editor The editor to adjust the cursor position in. + */ + moveCursorToEndOfLine(editor: monaco.editor.ICodeEditor): void { + const position: monaco.IPosition = { ...this.getPosition(editor), column: Number.POSITIVE_INFINITY }; + this.setPosition(editor, position); + } + /** * Toggles the fullscreen mode of the given element. If no element is provided, the editor's DOM node is used. * @param editor The editor to toggle the fullscreen mode for. diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/monaco-editor-domain-action.model.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/monaco-editor-domain-action.model.ts index c914241efa76..2a9999e96fd2 100644 --- a/src/main/webapp/app/shared/monaco-editor/model/actions/monaco-editor-domain-action.model.ts +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/monaco-editor-domain-action.model.ts @@ -1,7 +1,36 @@ import { MonacoEditorAction } from './monaco-editor-action.model'; +import * as monaco from 'monaco-editor'; /** * Class representing domain actions for Artemis-specific use cases. - * TODO: In the future, each domain action should have its own logic and a unique identifier (e.g. multiple choice questions, drag and drop questions). */ -export abstract class MonacoEditorDomainAction extends MonacoEditorAction {} +export abstract class MonacoEditorDomainAction extends MonacoEditorAction { + abstract getOpeningIdentifier(): string; + + /** + * Inserts, below the current line, a new line with the domain action identifier and the given text. + * Afterward, if specified, sets the selection to cover exactly the provided text part. + * @param editor The editor in which to insert the text + * @param text The text to insert. Can be empty. + * @param indent Whether to indent the inserted text with a tab. + * @param updateSelection Whether to update the selection after inserting the text + */ + addTextWithDomainActionIdentifier(editor: monaco.editor.ICodeEditor, text: string, indent = false, updateSelection = true): void { + this.clearSelection(editor); + this.moveCursorToEndOfLine(editor); + const identifierWithText = text ? `${this.getOpeningIdentifier()} ${text}` : this.getOpeningIdentifier(); + const insertText = indent ? `\n\t${identifierWithText}` : `\n${identifierWithText}`; + this.insertTextAtPosition(editor, this.getPosition(editor), insertText); + if (updateSelection) { + const newPosition = this.getPosition(editor); + // Set the selection to cover exactly the text part + this.setSelection(editor, { + startLineNumber: newPosition.lineNumber, + endLineNumber: newPosition.lineNumber, + startColumn: newPosition.column - text.length, + endColumn: newPosition.column, + }); + } + editor.focus(); + } +} diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/monaco-formula.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/monaco-formula.action.ts index 4a605b733520..e7d5de9d0324 100644 --- a/src/main/webapp/app/shared/monaco-editor/model/actions/monaco-formula.action.ts +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/monaco-formula.action.ts @@ -20,7 +20,11 @@ export class MonacoFormulaAction extends MonacoEditorDomainAction { * @param editor The editor in which to toggle formula text. */ run(editor: monaco.editor.ICodeEditor) { - this.toggleDelimiterAroundSelection(editor, FORMULA_OPEN_DELIMITER, FORMULA_CLOSE_DELIMITER, MonacoFormulaAction.DEFAULT_FORMULA); + this.toggleDelimiterAroundSelection(editor, this.getOpeningIdentifier(), FORMULA_CLOSE_DELIMITER, MonacoFormulaAction.DEFAULT_FORMULA); editor.focus(); } + + getOpeningIdentifier(): string { + return FORMULA_OPEN_DELIMITER; + } } diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/monaco-task.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/monaco-task.action.ts index 3a2aa120375a..e1fe3f87b53e 100644 --- a/src/main/webapp/app/shared/monaco-editor/model/actions/monaco-task.action.ts +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/monaco-task.action.ts @@ -6,7 +6,8 @@ import { MonacoEditorDomainAction } from 'app/shared/monaco-editor/model/actions */ export class MonacoTaskAction extends MonacoEditorDomainAction { static readonly ID = 'monaco-task.action'; - static readonly INSERT_TASK_TEXT = '[task][Task Short Description](testCaseName)\n'; + static readonly TEXT = '[Task Short Description](testCaseName)\n'; + constructor() { super(MonacoTaskAction.ID, 'artemisApp.programmingExercise.problemStatement.taskCommand', undefined, undefined); } @@ -16,7 +17,11 @@ export class MonacoTaskAction extends MonacoEditorDomainAction { * @param editor The editor in which to insert the task. */ run(editor: monaco.editor.ICodeEditor): void { - this.replaceTextAtCurrentSelection(editor, MonacoTaskAction.INSERT_TASK_TEXT); + this.replaceTextAtCurrentSelection(editor, `${this.getOpeningIdentifier()}${MonacoTaskAction.TEXT}`); editor.focus(); } + + getOpeningIdentifier(): string { + return '[task]'; + } } diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/monaco-test-case.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/monaco-test-case.action.ts index fc243b3943ee..e2d48c615709 100644 --- a/src/main/webapp/app/shared/monaco-editor/model/actions/monaco-test-case.action.ts +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/monaco-test-case.action.ts @@ -74,4 +74,8 @@ export class MonacoTestCaseAction extends MonacoEditorDomainActionWithOptions { this.replaceTextAtCurrentSelection(editor, args?.selectedItem?.value ?? MonacoTestCaseAction.DEFAULT_INSERT_TEXT); editor.focus(); } + + getOpeningIdentifier(): string { + return '('; + } } diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/quiz/monaco-correct-multiple-choice-answer.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/quiz/monaco-correct-multiple-choice-answer.action.ts new file mode 100644 index 000000000000..e8307539ec41 --- /dev/null +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/quiz/monaco-correct-multiple-choice-answer.action.ts @@ -0,0 +1,20 @@ +import { MonacoEditorDomainAction } from 'app/shared/monaco-editor/model/actions/monaco-editor-domain-action.model'; +import * as monaco from 'monaco-editor'; + +export class MonacoCorrectMultipleChoiceAnswerAction extends MonacoEditorDomainAction { + static readonly ID = 'artemisApp.multipleChoiceQuestion.editor.addCorrectAnswerOption'; + static readonly IDENTIFIER = '[correct]'; + static readonly TEXT = 'Enter a correct answer option here'; + + constructor() { + super(MonacoCorrectMultipleChoiceAnswerAction.ID, 'artemisApp.multipleChoiceQuestion.editor.addCorrectAnswerOption'); + } + + run(editor: monaco.editor.ICodeEditor): void { + this.addTextWithDomainActionIdentifier(editor, MonacoCorrectMultipleChoiceAnswerAction.TEXT); + } + + getOpeningIdentifier(): string { + return MonacoCorrectMultipleChoiceAnswerAction.IDENTIFIER; + } +} diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/quiz/monaco-quiz-explanation.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/quiz/monaco-quiz-explanation.action.ts new file mode 100644 index 000000000000..6cc078baa448 --- /dev/null +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/quiz/monaco-quiz-explanation.action.ts @@ -0,0 +1,20 @@ +import { MonacoEditorDomainAction } from 'app/shared/monaco-editor/model/actions/monaco-editor-domain-action.model'; +import * as monaco from 'monaco-editor'; + +export class MonacoQuizExplanationAction extends MonacoEditorDomainAction { + static readonly ID = 'monaco-quiz-explanation.action'; + static readonly IDENTIFIER = '[exp]'; + static readonly TEXT = 'Add an explanation here (only visible in feedback after quiz has ended)'; + + constructor() { + super(MonacoQuizExplanationAction.ID, 'artemisApp.multipleChoiceQuestion.editor.addExplanation'); + } + + run(editor: monaco.editor.ICodeEditor): void { + this.addTextWithDomainActionIdentifier(editor, MonacoQuizExplanationAction.TEXT, true); + } + + getOpeningIdentifier(): string { + return MonacoQuizExplanationAction.IDENTIFIER; + } +} diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/quiz/monaco-quiz-hint.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/quiz/monaco-quiz-hint.action.ts new file mode 100644 index 000000000000..29c320bf016b --- /dev/null +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/quiz/monaco-quiz-hint.action.ts @@ -0,0 +1,20 @@ +import { MonacoEditorDomainAction } from 'app/shared/monaco-editor/model/actions/monaco-editor-domain-action.model'; +import * as monaco from 'monaco-editor'; + +export class MonacoQuizHintAction extends MonacoEditorDomainAction { + static readonly ID = 'monaco-quiz-hint.action'; + static readonly IDENTIFIER = '[hint]'; + static readonly TEXT = 'Add a hint here (visible during the quiz via ?-Button)'; + + constructor() { + super(MonacoQuizHintAction.ID, 'artemisApp.multipleChoiceQuestion.editor.addHint'); + } + + run(editor: monaco.editor.ICodeEditor): void { + this.addTextWithDomainActionIdentifier(editor, MonacoQuizHintAction.TEXT, true); + } + + getOpeningIdentifier(): string { + return MonacoQuizHintAction.IDENTIFIER; + } +} diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/quiz/monaco-wrong-multiple-choice-answer.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/quiz/monaco-wrong-multiple-choice-answer.action.ts new file mode 100644 index 000000000000..7c682072eb5a --- /dev/null +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/quiz/monaco-wrong-multiple-choice-answer.action.ts @@ -0,0 +1,20 @@ +import * as monaco from 'monaco-editor'; +import { MonacoEditorDomainAction } from 'app/shared/monaco-editor/model/actions/monaco-editor-domain-action.model'; + +export class MonacoWrongMultipleChoiceAnswerAction extends MonacoEditorDomainAction { + static readonly ID = 'monaco-incorrect-multiple-choice-answer.action'; + static readonly IDENTIFIER = '[wrong]'; + static readonly TEXT = 'Enter a wrong answer option here'; + + constructor() { + super(MonacoWrongMultipleChoiceAnswerAction.ID, 'artemisApp.multipleChoiceQuestion.editor.addInCorrectAnswerOption'); + } + + run(editor: monaco.editor.ICodeEditor): void { + this.addTextWithDomainActionIdentifier(editor, MonacoWrongMultipleChoiceAnswerAction.TEXT); + } + + getOpeningIdentifier(): string { + return MonacoWrongMultipleChoiceAnswerAction.IDENTIFIER; + } +} diff --git a/src/main/webapp/app/shared/monaco-editor/model/monaco-editor-option-preset.model.ts b/src/main/webapp/app/shared/monaco-editor/model/monaco-editor-option-preset.model.ts index b4ddcdc5a58d..e3470bdb6837 100644 --- a/src/main/webapp/app/shared/monaco-editor/model/monaco-editor-option-preset.model.ts +++ b/src/main/webapp/app/shared/monaco-editor/model/monaco-editor-option-preset.model.ts @@ -1,17 +1,19 @@ import * as monaco from 'monaco-editor'; +export type MonacoEditorOptions = monaco.editor.IEditorOptions; + /** * A preset of Monaco Editor options that can be applied to an editor for specific use cases, e.g. short answer quiz questions. * Presets are defined in the file monaco-editor-option.helper.ts. */ export class MonacoEditorOptionPreset { - constructor(private options: monaco.editor.IEditorOptions) {} + constructor(private options: MonacoEditorOptions) {} /** * Update the editor options with the preset options. * @param editor The editor to which the options should be applied. */ - apply(editor: monaco.editor.IStandaloneCodeEditor): void { + apply(editor: monaco.editor.ICodeEditor): void { editor.updateOptions(this.options); } } diff --git a/src/main/webapp/app/shared/monaco-editor/monaco-editor-option.helper.ts b/src/main/webapp/app/shared/monaco-editor/monaco-editor-option.helper.ts index d9c13af84836..ea36346ec1d3 100644 --- a/src/main/webapp/app/shared/monaco-editor/monaco-editor-option.helper.ts +++ b/src/main/webapp/app/shared/monaco-editor/monaco-editor-option.helper.ts @@ -1,4 +1,4 @@ -import { MonacoEditorOptionPreset } from 'app/shared/monaco-editor/model/monaco-editor-option-preset.model'; +import { MonacoEditorOptionPreset, MonacoEditorOptions } from 'app/shared/monaco-editor/model/monaco-editor-option-preset.model'; export const SHORT_ANSWER_QUIZ_QUESTION_EDITOR_OPTIONS = new MonacoEditorOptionPreset({ // Hide the gutter @@ -14,3 +14,40 @@ export const SHORT_ANSWER_QUIZ_QUESTION_EDITOR_OPTIONS = new MonacoEditorOptionP // Disable line highlighting renderLineHighlight: 'none', }); + +const defaultMarkdownOptions: MonacoEditorOptions = { + // Sets up the layout to make the editor look more like a text field (no line numbers, margin, or highlights). + lineNumbers: 'off', + glyphMargin: false, + folding: false, + lineDecorationsWidth: '1ch', + lineNumbersMinChars: 0, + padding: { + top: 5, + }, + renderLineHighlight: 'none', + // Only show scrollbars if required. + scrollbar: { + vertical: 'auto', + horizontal: 'auto', + }, + overviewRulerLanes: 0, + hideCursorInOverviewRuler: true, + // The suggestions from showWords are shared between editors of the same language, so we disable them. + suggest: { + showWords: false, + }, + // We use the 'simple' strategy for word wraps to prevent performance issues. This prevents us from switching to a different font as the lines would no longer break correctly. + wordWrap: 'on', + wrappingStrategy: 'simple', + selectionHighlight: false, + occurrencesHighlight: 'off', +}; + +export const DEFAULT_MARKDOWN_EDITOR_OPTIONS = new MonacoEditorOptionPreset(defaultMarkdownOptions); + +export const COMMUNICATION_MARKDOWN_EDITOR_OPTIONS = new MonacoEditorOptionPreset({ + ...defaultMarkdownOptions, + // Separates the editor suggest widget from the editor's layout. It will stick to the page, but it won't interfere with other elements. + fixedOverflowWidgets: true, +}); diff --git a/src/main/webapp/app/shared/monaco-editor/monaco-editor.component.ts b/src/main/webapp/app/shared/monaco-editor/monaco-editor.component.ts index e7c5908d1a9a..1f28308ecb11 100644 --- a/src/main/webapp/app/shared/monaco-editor/monaco-editor.component.ts +++ b/src/main/webapp/app/shared/monaco-editor/monaco-editor.component.ts @@ -100,8 +100,12 @@ export class MonacoEditorComponent implements OnInit, OnDestroy { @Output() contentHeightChanged = new EventEmitter(); + @Output() + onBlurEditor = new EventEmitter(); + private contentHeightListener?: monaco.IDisposable; private textChangedListener?: monaco.IDisposable; + private blurEditorWidgetListener?: monaco.IDisposable; private textChangedEmitTimeout?: NodeJS.Timeout; ngOnInit(): void { @@ -120,6 +124,10 @@ export class MonacoEditorComponent implements OnInit, OnDestroy { } }); + this.blurEditorWidgetListener = this._editor.onDidBlurEditorWidget(() => { + this.onBlurEditor.emit(); + }); + this.themeSubscription = this.themeService.getCurrentThemeObservable().subscribe((theme) => this.changeTheme(theme)); } @@ -129,6 +137,7 @@ export class MonacoEditorComponent implements OnInit, OnDestroy { this.themeSubscription?.unsubscribe(); this.textChangedListener?.dispose(); this.contentHeightListener?.dispose(); + this.blurEditorWidgetListener?.dispose(); } private emitTextChangeEvent() { @@ -393,43 +402,6 @@ export class MonacoEditorComponent implements OnInit, OnDestroy { }); } - /** - * Enables a text field mode for the editor. This will make the editor look more like a text field and less like a code editor. - * In particular, line numbers, margins, and highlights will be disabled. - */ - enableTextFieldMode(): void { - this._editor.updateOptions({ - // Sets up the layout to make the editor look more like a text field (no line numbers, margin, or highlights). - lineNumbers: 'off', - glyphMargin: false, - folding: false, - lineDecorationsWidth: '1ch', - lineNumbersMinChars: 0, - padding: { - top: 5, - }, - renderLineHighlight: 'none', - selectionHighlight: false, - occurrencesHighlight: 'off', - // Only show scrollbars if required. - scrollbar: { - vertical: 'auto', - horizontal: 'auto', - }, - overviewRulerLanes: 0, - hideCursorInOverviewRuler: true, - // The suggestions from showWords are shared between editors of the same language. - suggest: { - showWords: false, - }, - // Separates the editor suggest widget from the editor's layout. It will stick to the page, but it won't interfere with other elements. - fixedOverflowWidgets: true, - // We use the 'simple' strategy for word wraps to prevent performance issues. This prevents us from switching to a different font as the lines would no longer break correctly. - wrappingStrategy: 'simple', - }); - this.setWordWrap(true); - } - /** * Applies the given options to the editor. * @param options The options to apply. diff --git a/src/test/javascript/spec/component/admin/legal-document-update.component.spec.ts b/src/test/javascript/spec/component/admin/legal-document-update.component.spec.ts index c9f6b2ebd4ee..bbec5290351b 100644 --- a/src/test/javascript/spec/component/admin/legal-document-update.component.spec.ts +++ b/src/test/javascript/spec/component/admin/legal-document-update.component.spec.ts @@ -6,7 +6,6 @@ import { UnsavedChangesWarningComponent } from 'app/admin/legal/unsaved-changes- import { ButtonComponent } from 'app/shared/components/button.component'; import { TranslateDirective } from 'app/shared/language/translate.directive'; import { ArtemisTestModule } from '../../test.module'; -import { MarkdownEditorComponent } from 'app/shared/markdown-editor/markdown-editor.component'; import { JhiLanguageHelper } from 'app/core/language/language.helper'; import { MockNgbModalService } from '../../helpers/mocks/service/mock-ngb-modal.service'; import { MockLanguageHelper } from '../../helpers/mocks/service/mock-translate.service'; @@ -19,6 +18,7 @@ import { ActivatedRoute, UrlSegment } from '@angular/router'; import { of } from 'rxjs'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { PrivacyStatement } from 'app/entities/privacy-statement.model'; +import { MarkdownEditorMonacoComponent } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; describe('LegalDocumentUpdateComponent', () => { let component: LegalDocumentUpdateComponent; @@ -35,7 +35,7 @@ describe('LegalDocumentUpdateComponent', () => { MockComponent(UnsavedChangesWarningComponent), MockComponent(ButtonComponent), MockDirective(TranslateDirective), - MockComponent(MarkdownEditorComponent), + MockComponent(MarkdownEditorMonacoComponent), MockComponent(ModePickerComponent), MockPipe(ArtemisTranslatePipe), ], @@ -117,7 +117,7 @@ describe('LegalDocumentUpdateComponent', () => { it('should correctly determine unsaved changes', () => { component.unsavedChanges = false; component.legalDocument.text = 'text'; - component.checkUnsavedChanges('changed text'); + component.onContentChanged('changed text'); expect(component.unsavedChanges).toBeTrue(); }); @@ -135,7 +135,7 @@ describe('LegalDocumentUpdateComponent', () => { updateFile = jest.spyOn(legalDocumentService, 'updateImprint').mockReturnValue(of(returnValue)); } component.markdownEditor.markdown = 'text'; - component.unsavedChanges = true; + component.onContentChanged('text'); const expected = new LegalDocument(documentType, LegalDocumentLanguage.GERMAN); expected.text = 'text'; fixture.detectChanges(); @@ -149,13 +149,12 @@ describe('LegalDocumentUpdateComponent', () => { expect(component.unsavedChanges).toBeFalse(); }), ); - it('should set the value of the markdown editor when switching to the edit mode if the language is changed while in preview mode', () => { + it('should set the value of the markdown editor when the language is changed while in preview mode', () => { setupRoutes(LegalDocumentType.PRIVACY_STATEMENT); const returnValue = new PrivacyStatement(LegalDocumentLanguage.GERMAN); returnValue.text = 'new content'; - const updateTextOnEditSelect = jest.spyOn(component, 'updateTextIfLanguageChangedInPreview').mockImplementation(); + const parseMarkdownStub = jest.spyOn(component.markdownEditor, 'parseMarkdown').mockImplementation(); component.markdownEditor.markdown = 'text'; - component.markdownEditor.previewMode = true; component.unsavedChanges = false; component.ngOnInit(); const loadFile = jest.spyOn(legalDocumentService, 'getPrivacyStatementForUpdate').mockReturnValue(of(returnValue)); @@ -164,7 +163,7 @@ describe('LegalDocumentUpdateComponent', () => { expect(loadFile).toHaveBeenCalledOnce(); expect(component.legalDocument.text).toBe('new content'); component.markdownEditor.onEditSelect.emit(); - expect(updateTextOnEditSelect).toHaveBeenCalledOnce(); + expect(parseMarkdownStub).toHaveBeenCalledOnce(); }); function setupRoutes(documentType: LegalDocumentType) { diff --git a/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts b/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts index ebe7506fbc81..40ef3c893792 100644 --- a/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/competency-form.component.spec.ts @@ -7,7 +7,7 @@ import { Competency, CompetencyTaxonomy } from 'app/entities/competency.model'; import { TextUnit } from 'app/entities/lecture-unit/textUnit.model'; import { Lecture } from 'app/entities/lecture.model'; import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/lectureUnit.service'; -import { MockProvider } from 'ng-mocks'; +import { MockComponent, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; import { ArtemisTestModule } from '../../test.module'; import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; @@ -17,6 +17,8 @@ import { CompetencyFormComponent } from 'app/course/competencies/forms/competenc import { CourseCompetencyFormData } from 'app/course/competencies/forms/course-competency-form.component'; import { By } from '@angular/platform-browser'; import { CommonCourseCompetencyFormComponent } from 'app/course/competencies/forms/common-course-competency-form.component'; +import { MarkdownEditorMonacoComponent } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; +import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; describe('CompetencyFormComponent', () => { let competencyFormComponentFixture: ComponentFixture; @@ -30,6 +32,10 @@ describe('CompetencyFormComponent', () => { declarations: [], providers: [MockProvider(CompetencyService), MockProvider(LectureUnitService), { provide: TranslateService, useClass: MockTranslateService }], }) + .overrideModule(ArtemisMarkdownEditorModule, { + remove: { exports: [MarkdownEditorMonacoComponent] }, + add: { exports: [MockComponent(MarkdownEditorMonacoComponent)], declarations: [MockComponent(MarkdownEditorMonacoComponent)] }, + }) .compileComponents() .then(() => { competencyFormComponentFixture = TestBed.createComponent(CompetencyFormComponent); diff --git a/src/test/javascript/spec/component/competencies/edit-competency.component.spec.ts b/src/test/javascript/spec/component/competencies/edit-competency.component.spec.ts index 9581c68a404a..88b261712f1f 100644 --- a/src/test/javascript/spec/component/competencies/edit-competency.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/edit-competency.component.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MockProvider } from 'ng-mocks'; +import { MockComponent, MockProvider } from 'ng-mocks'; import { AlertService } from 'app/core/util/alert.service'; import { ActivatedRoute, Router } from '@angular/router'; import { of } from 'rxjs'; @@ -15,6 +15,8 @@ import { MockRouter } from '../../helpers/mocks/mock-router'; import { CompetencyFormStubComponent } from './competency-form-stub.component'; import { ArtemisTestModule } from '../../test.module'; import { CompetencyFormComponent } from 'app/course/competencies/forms/competency/competency-form.component'; +import { MarkdownEditorMonacoComponent } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; +import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; describe('EditCompetencyComponent', () => { let editCompetencyComponentFixture: ComponentFixture; @@ -56,6 +58,10 @@ describe('EditCompetencyComponent', () => { ], schemas: [], }) + .overrideModule(ArtemisMarkdownEditorModule, { + remove: { exports: [MarkdownEditorMonacoComponent] }, + add: { exports: [MockComponent(MarkdownEditorMonacoComponent)], declarations: [MockComponent(MarkdownEditorMonacoComponent)] }, + }) .compileComponents() .then(() => { editCompetencyComponentFixture = TestBed.createComponent(EditCompetencyComponent); diff --git a/src/test/javascript/spec/component/competencies/edit-prerequisite.component.spec.ts b/src/test/javascript/spec/component/competencies/edit-prerequisite.component.spec.ts index 50e21caf9dfa..7e1009b2d1e8 100644 --- a/src/test/javascript/spec/component/competencies/edit-prerequisite.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/edit-prerequisite.component.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MockProvider } from 'ng-mocks'; +import { MockComponent, MockProvider } from 'ng-mocks'; import { AlertService } from 'app/core/util/alert.service'; import { ActivatedRoute, Router } from '@angular/router'; import { of } from 'rxjs'; @@ -16,6 +16,8 @@ import { EditPrerequisiteComponent } from 'app/course/competencies/edit/edit-pre import { PrerequisiteService } from 'app/course/competencies/prerequisite.service'; import { PrerequisiteFormComponent } from 'app/course/competencies/forms/prerequisite/prerequisite-form.component'; import { PrerequisiteFormStubComponent } from './prerequisite-form-stub.component'; +import { MarkdownEditorMonacoComponent } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; +import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; describe('EditPrerequisiteComponent', () => { let editPrerequisiteComponentFixture: ComponentFixture; @@ -57,6 +59,10 @@ describe('EditPrerequisiteComponent', () => { ], schemas: [], }) + .overrideModule(ArtemisMarkdownEditorModule, { + remove: { exports: [MarkdownEditorMonacoComponent] }, + add: { exports: [MockComponent(MarkdownEditorMonacoComponent)], declarations: [MockComponent(MarkdownEditorMonacoComponent)] }, + }) .compileComponents() .then(() => { editPrerequisiteComponentFixture = TestBed.createComponent(EditPrerequisiteComponent); diff --git a/src/test/javascript/spec/component/competencies/generate-competencies/competency-recommendation-detail.component.spec.ts b/src/test/javascript/spec/component/competencies/generate-competencies/competency-recommendation-detail.component.spec.ts index a2ee1c0c064b..b97f8099cd1d 100644 --- a/src/test/javascript/spec/component/competencies/generate-competencies/competency-recommendation-detail.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/generate-competencies/competency-recommendation-detail.component.spec.ts @@ -11,8 +11,8 @@ import { NgbCollapseMocksModule } from '../../../helpers/mocks/directive/ngbColl import { FeatureToggleDirective } from 'app/shared/feature-toggle/feature-toggle.directive'; import { TranslateDirective } from 'app/shared/language/translate.directive'; import { HtmlForMarkdownPipe } from 'app/shared/pipes/html-for-markdown.pipe'; -import { MarkdownEditorComponent } from 'app/shared/markdown-editor/markdown-editor.component'; import { TaxonomySelectComponent } from 'app/course/competencies/taxonomy-select/taxonomy-select.component'; +import { MarkdownEditorMonacoComponent } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; describe('CompetencyRecommendationDetailComponent', () => { let competencyRecommendationDetailComponentFixture: ComponentFixture; @@ -28,7 +28,7 @@ describe('CompetencyRecommendationDetailComponent', () => { MockDirective(TranslateDirective), MockPipe(ArtemisTranslatePipe), MockPipe(HtmlForMarkdownPipe), - MockComponent(MarkdownEditorComponent), + MockComponent(MarkdownEditorMonacoComponent), MockComponent(TaxonomySelectComponent), ], providers: [], diff --git a/src/test/javascript/spec/component/competencies/prerequisite-form.component.spec.ts b/src/test/javascript/spec/component/competencies/prerequisite-form.component.spec.ts index 3a3e852e4c39..4471d024976b 100644 --- a/src/test/javascript/spec/component/competencies/prerequisite-form.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/prerequisite-form.component.spec.ts @@ -6,7 +6,7 @@ import { CompetencyTaxonomy } from 'app/entities/competency.model'; import { TextUnit } from 'app/entities/lecture-unit/textUnit.model'; import { Lecture } from 'app/entities/lecture.model'; import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/lectureUnit.service'; -import { MockProvider } from 'ng-mocks'; +import { MockComponent, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; import { ArtemisTestModule } from '../../test.module'; import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; @@ -19,6 +19,8 @@ import { CommonCourseCompetencyFormComponent } from 'app/course/competencies/for import { PrerequisiteFormComponent } from 'app/course/competencies/forms/prerequisite/prerequisite-form.component'; import { PrerequisiteService } from 'app/course/competencies/prerequisite.service'; import { Prerequisite } from 'app/entities/prerequisite.model'; +import { MarkdownEditorMonacoComponent } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; +import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; describe('PrerequisiteFormComponent', () => { let prerequisiteFormComponentFixture: ComponentFixture; @@ -29,9 +31,12 @@ describe('PrerequisiteFormComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [CompetencyFormComponent, ArtemisTestModule, ReactiveFormsModule, NgbDropdownModule], - declarations: [], providers: [MockProvider(PrerequisiteService), MockProvider(LectureUnitService), { provide: TranslateService, useClass: MockTranslateService }], }) + .overrideModule(ArtemisMarkdownEditorModule, { + remove: { exports: [MarkdownEditorMonacoComponent] }, + add: { exports: [MockComponent(MarkdownEditorMonacoComponent)], declarations: [MockComponent(MarkdownEditorMonacoComponent)] }, + }) .compileComponents() .then(() => { prerequisiteFormComponentFixture = TestBed.createComponent(PrerequisiteFormComponent); diff --git a/src/test/javascript/spec/component/course/course-update.component.spec.ts b/src/test/javascript/spec/component/course/course-update.component.spec.ts index 9566d806f9cf..2239ab6f91d1 100644 --- a/src/test/javascript/spec/component/course/course-update.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-update.component.spec.ts @@ -43,7 +43,7 @@ import { ImageCropperModalComponent } from 'app/course/manage/image-cropper-moda import { FeatureToggle, FeatureToggleService } from 'app/shared/feature-toggle/feature-toggle.service'; import { MockFeatureToggleService } from '../../helpers/mocks/service/mock-feature-toggle.service'; -@Component({ selector: 'jhi-markdown-editor', template: '' }) +@Component({ selector: 'jhi-markdown-editor-monaco', template: '' }) class MarkdownEditorStubComponent { @Input() markdown: string; @Input() enableResize = false; diff --git a/src/test/javascript/spec/component/drag-and-drop-question/drag-and-drop-question-edit.component.spec.ts b/src/test/javascript/spec/component/drag-and-drop-question/drag-and-drop-question-edit.component.spec.ts index 2f5a5ee00a31..2a5d9fde2bb3 100644 --- a/src/test/javascript/spec/component/drag-and-drop-question/drag-and-drop-question-edit.component.spec.ts +++ b/src/test/javascript/spec/component/drag-and-drop-question/drag-and-drop-question-edit.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { NgModel } from '@angular/forms'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { NgbCollapse, NgbModal, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { DragAndDropMapping } from 'app/entities/quiz/drag-and-drop-mapping.model'; import { DragAndDropQuestion } from 'app/entities/quiz/drag-and-drop-question.model'; import { DragItem } from 'app/entities/quiz/drag-item.model'; @@ -13,10 +13,6 @@ import { QuizScoringInfoModalComponent } from 'app/exercises/quiz/manage/quiz-sc import { DragAndDropQuestionComponent } from 'app/exercises/quiz/shared/questions/drag-and-drop-question/drag-and-drop-question.component'; import { FileUploaderService } from 'app/shared/http/file-uploader.service'; import { SecuredImageComponent } from 'app/shared/image/secured-image.component'; -import { DomainCommand } from 'app/shared/markdown-editor/domainCommands/domainCommand'; -import { ExplanationCommand } from 'app/shared/markdown-editor/domainCommands/explanation.command'; -import { HintCommand } from 'app/shared/markdown-editor/domainCommands/hint.command'; -import { MarkdownEditorComponent } from 'app/shared/markdown-editor/markdown-editor.component'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { MockComponent, MockDirective, MockModule, MockPipe, MockProvider } from 'ng-mocks'; import { MockNgbModalService } from '../../helpers/mocks/service/mock-ngb-modal.service'; @@ -27,8 +23,9 @@ import { DragAndDropQuestionUtil } from 'app/exercises/quiz/shared/drag-and-drop import { ChangeDetectorRef } from '@angular/core'; import { clone } from 'lodash-es'; import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'; -import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap'; -import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { MonacoQuizExplanationAction } from 'app/shared/monaco-editor/model/actions/quiz/monaco-quiz-explanation.action'; +import { MonacoQuizHintAction } from 'app/shared/monaco-editor/model/actions/quiz/monaco-quiz-hint.action'; +import { MarkdownEditorMonacoComponent, TextWithDomainAction } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; describe('DragAndDropQuestionEditComponent', () => { let fixture: ComponentFixture; @@ -75,7 +72,7 @@ describe('DragAndDropQuestionEditComponent', () => { DragAndDropQuestionEditComponent, MockPipe(ArtemisTranslatePipe), MockComponent(QuizScoringInfoModalComponent), - MockComponent(MarkdownEditorComponent), + MockComponent(MarkdownEditorMonacoComponent), MockComponent(SecuredImageComponent), MockComponent(DragAndDropQuestionComponent), MockDirective(NgModel), @@ -603,36 +600,36 @@ describe('DragAndDropQuestionEditComponent', () => { expect(component.questionEditorText).toBe(newValue); }); - it('should detect domain commands', () => { + it('should detect domain actions', () => { component.question = new DragAndDropQuestion(); component.question.text = 'text'; component.question.explanation = 'explanation'; component.question.hint = 'hint'; - let domainCommand: [string, DomainCommand]; + let textWithDomainAction: TextWithDomainAction; // explanation - let command = new ExplanationCommand(); + let action = new MonacoQuizExplanationAction(); let text = 'take this as an explanationCommand'; - domainCommand = [text, command]; + textWithDomainAction = { text, action }; - component.domainCommandsFound([domainCommand]); + component.domainActionsFound([textWithDomainAction]); expect(component.question.explanation).toBe(text); // hint - command = new HintCommand(); + action = new MonacoQuizHintAction(); text = 'take this as a hintCommand'; - domainCommand = [text, command]; + textWithDomainAction = { text, action }; - component.domainCommandsFound([domainCommand]); + component.domainActionsFound([textWithDomainAction]); expect(component.question.hint).toBe(text); // text text = 'take this null as a command'; - domainCommand = [text, null as unknown as DomainCommand]; + textWithDomainAction = { text, action: undefined }; - component.domainCommandsFound([domainCommand]); + component.domainActionsFound([textWithDomainAction]); expect(component.question.text).toBe(text); }); diff --git a/src/test/javascript/spec/component/exam/exam-update.component.spec.ts b/src/test/javascript/spec/component/exam/exam-update.component.spec.ts index 3536693cbe09..6de1e1db5c13 100644 --- a/src/test/javascript/spec/component/exam/exam-update.component.spec.ts +++ b/src/test/javascript/spec/component/exam/exam-update.component.spec.ts @@ -19,7 +19,6 @@ import { Exam } from 'app/entities/exam.model'; import { Course, CourseInformationSharingConfiguration } from 'app/entities/course.model'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { FormDateTimePickerComponent } from 'app/shared/date-time-picker/date-time-picker.component'; -import { MarkdownEditorComponent } from 'app/shared/markdown-editor/markdown-editor.component'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { GradingSystemService } from 'app/grading-system/grading-system.service'; @@ -43,6 +42,7 @@ import { DocumentationButtonComponent } from 'app/shared/components/documentatio import { TitleChannelNameComponent } from 'app/shared/form/title-channel-name/title-channel-name.component'; import { UMLDiagramType } from '@ls1intum/apollon'; import { TextExercise } from 'app/entities/text-exercise.model'; +import { MarkdownEditorMonacoComponent } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; @Component({ template: '', @@ -77,7 +77,7 @@ describe('ExamUpdateComponent', () => { declarations: [ ExamUpdateComponent, MockComponent(FormDateTimePickerComponent), - MockComponent(MarkdownEditorComponent), + MockComponent(MarkdownEditorMonacoComponent), MockComponent(DataTableComponent), DummyComponent, MockPipe(ArtemisTranslatePipe), @@ -606,7 +606,7 @@ describe('ExamUpdateComponent', () => { ExamUpdateComponent, ExamExerciseImportComponent, MockComponent(FormDateTimePickerComponent), - MockComponent(MarkdownEditorComponent), + MockComponent(MarkdownEditorMonacoComponent), MockComponent(DataTableComponent), DummyComponent, MockPipe(ArtemisTranslatePipe), diff --git a/src/test/javascript/spec/component/exam/manage/exams/exam-announcement-dialog/exam-live-announcement-create-modal.component.spec.ts b/src/test/javascript/spec/component/exam/manage/exams/exam-announcement-dialog/exam-live-announcement-create-modal.component.spec.ts index 6e248103b247..6ab12448b3fb 100644 --- a/src/test/javascript/spec/component/exam/manage/exams/exam-announcement-dialog/exam-live-announcement-create-modal.component.spec.ts +++ b/src/test/javascript/spec/component/exam/manage/exams/exam-announcement-dialog/exam-live-announcement-create-modal.component.spec.ts @@ -7,9 +7,9 @@ import { AccountService } from 'app/core/auth/account.service'; import { By } from '@angular/platform-browser'; import { MockComponent } from 'ng-mocks'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; -import { MarkdownEditorComponent } from 'app/shared/markdown-editor/markdown-editor.component'; import { ExamLiveEventComponent } from 'app/exam/shared/events/exam-live-event.component'; import { ExamWideAnnouncementEvent } from 'app/exam/participate/exam-participation-live-events.service'; +import { MarkdownEditorMonacoComponent } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; describe('ExamLiveAnnouncementCreateModalComponent', () => { let component: ExamLiveAnnouncementCreateModalComponent; @@ -19,7 +19,12 @@ describe('ExamLiveAnnouncementCreateModalComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ExamLiveAnnouncementCreateModalComponent, MockComponent(FaIconComponent), MockComponent(MarkdownEditorComponent), MockComponent(ExamLiveEventComponent)], + declarations: [ + ExamLiveAnnouncementCreateModalComponent, + MockComponent(FaIconComponent), + MockComponent(MarkdownEditorMonacoComponent), + MockComponent(ExamLiveEventComponent), + ], providers: [ { provide: NgbActiveModal, useValue: { dismiss: jest.fn() } }, { provide: ExamManagementService, useValue: { createAnnouncement: jest.fn() } }, diff --git a/src/test/javascript/spec/component/exercise-hint/manage/exercise-hint-update.component.spec.ts b/src/test/javascript/spec/component/exercise-hint/manage/exercise-hint-update.component.spec.ts index 038a9cc515ef..2af4e435afaf 100644 --- a/src/test/javascript/spec/component/exercise-hint/manage/exercise-hint-update.component.spec.ts +++ b/src/test/javascript/spec/component/exercise-hint/manage/exercise-hint-update.component.spec.ts @@ -11,7 +11,6 @@ import { HelpIconComponent } from 'app/shared/components/help-icon.component'; import { ExerciseHintService } from 'app/exercises/shared/exercise-hint/shared/exercise-hint.service'; import { ExerciseHint } from 'app/entities/hestia/exercise-hint.model'; import { ActivatedRoute } from '@angular/router'; -import { MarkdownEditorComponent } from 'app/shared/markdown-editor/markdown-editor.component'; import { ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service'; import { ProgrammingExerciseServerSideTask } from 'app/entities/hestia/programming-exercise-task.model'; import { ProgrammingExerciseTestCase } from 'app/entities/programming-exercise-test-case.model'; @@ -25,6 +24,7 @@ import { IrisSettings } from 'app/entities/iris/settings/iris-settings.model'; import { ProfileInfo } from 'app/shared/layouts/profiles/profile-info.model'; import { ProgrammingExerciseSolutionEntry } from 'app/entities/hestia/programming-exercise-solution-entry.model'; import { PROFILE_IRIS } from 'app/app.constants'; +import { MarkdownEditorMonacoComponent } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; describe('ExerciseHint Management Update Component', () => { let comp: ExerciseHintUpdateComponent; @@ -52,7 +52,7 @@ describe('ExerciseHint Management Update Component', () => { beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ imports: [ArtemisTestModule, FormsModule], - declarations: [ExerciseHintUpdateComponent, MockComponent(MarkdownEditorComponent), MockComponent(HelpIconComponent)], + declarations: [ExerciseHintUpdateComponent, MockComponent(MarkdownEditorMonacoComponent), MockComponent(HelpIconComponent)], providers: [ FormBuilder, MockProvider(ProgrammingExerciseService), diff --git a/src/test/javascript/spec/component/exercises/quiz/manage/re-evaluate/re-evaluate-multiple-choice-question.component.spec.ts b/src/test/javascript/spec/component/exercises/quiz/manage/re-evaluate/re-evaluate-multiple-choice-question.component.spec.ts index 7c65c546617a..67de3457a999 100644 --- a/src/test/javascript/spec/component/exercises/quiz/manage/re-evaluate/re-evaluate-multiple-choice-question.component.spec.ts +++ b/src/test/javascript/spec/component/exercises/quiz/manage/re-evaluate/re-evaluate-multiple-choice-question.component.spec.ts @@ -4,13 +4,13 @@ import { MockComponent, MockDirective, MockModule, MockPipe, MockProvider } from import { ArtemisTestModule } from '../../../../../test.module'; import { ReEvaluateMultipleChoiceQuestionComponent } from 'app/exercises/quiz/manage/re-evaluate/multiple-choice-question/re-evaluate-multiple-choice-question.component'; import { MultipleChoiceQuestionEditComponent } from 'app/exercises/quiz/manage/multiple-choice-question/multiple-choice-question-edit.component'; -import { MarkdownEditorComponent } from 'app/shared/markdown-editor/markdown-editor.component'; import { NgModel } from '@angular/forms'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { Directive, Input } from '@angular/core'; import { MultipleChoiceQuestion } from 'app/entities/quiz/multiple-choice-question.model'; import { AnswerOption } from 'app/entities/quiz/answer-option.model'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { MarkdownEditorMonacoComponent } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; // eslint-disable-next-line @angular-eslint/directive-selector @Directive({ selector: '[sortableData]' }) @@ -31,7 +31,7 @@ describe('ReEvaluateMultipleChoiceQuestionComponent', () => { declarations: [ ReEvaluateMultipleChoiceQuestionComponent, MockComponent(MultipleChoiceQuestionEditComponent), - MockComponent(MarkdownEditorComponent), + MockComponent(MarkdownEditorMonacoComponent), MockDirective(NgModel), MockDirective(MockSortableDataDirective), MockPipe(ArtemisTranslatePipe), diff --git a/src/test/javascript/spec/component/lecture-unit/text-unit/text-unit-form.component.spec.ts b/src/test/javascript/spec/component/lecture-unit/text-unit/text-unit-form.component.spec.ts index 0dfc849da228..32f0d6889eec 100644 --- a/src/test/javascript/spec/component/lecture-unit/text-unit/text-unit-form.component.spec.ts +++ b/src/test/javascript/spec/component/lecture-unit/text-unit/text-unit-form.component.spec.ts @@ -12,7 +12,7 @@ import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; import { MockRouter } from '../../../helpers/mocks/mock-router'; import { CompetencySelectionComponent } from 'app/shared/competency-selection/competency-selection.component'; -@Component({ selector: 'jhi-markdown-editor', template: '' }) +@Component({ selector: 'jhi-markdown-editor-monaco', template: '' }) class MarkdownEditorStubComponent { @Input() markdown: string; @Input() enableResize = false; 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 c53c66a2034f..03be222b4cb4 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 @@ -10,7 +10,6 @@ import { LectureUpdateComponent } from 'app/lecture/lecture-update.component'; import { LectureService } from 'app/lecture/lecture.service'; import { LectureUpdateWizardComponent } from 'app/lecture/wizard-mode/lecture-update-wizard.component'; import { FormDateTimePickerComponent } from 'app/shared/date-time-picker/date-time-picker.component'; -import { MarkdownEditorComponent } from 'app/shared/markdown-editor/markdown-editor.component'; 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'; @@ -23,6 +22,7 @@ import { MockTranslateService } from '../../helpers/mocks/service/mock-translate 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'; describe('LectureUpdateComponent', () => { let lectureUpdateWizardComponentFixture: ComponentFixture; @@ -51,7 +51,7 @@ describe('LectureUpdateComponent', () => { MockComponent(LectureTitleChannelNameComponent), MockComponent(LectureUpdateWizardComponent), MockComponent(FormDateTimePickerComponent), - MockComponent(MarkdownEditorComponent), + MockComponent(MarkdownEditorMonacoComponent), MockComponent(DocumentationButtonComponent), MockPipe(ArtemisTranslatePipe), MockPipe(ArtemisDatePipe), 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 2d40c01a5b50..36701004edbb 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,10 @@ 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 { MarkdownEditorComponent } from 'app/shared/markdown-editor/markdown-editor.component'; 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'; describe('LectureWizardTitleComponent', () => { let wizardTitleComponentFixture: ComponentFixture; @@ -13,7 +13,7 @@ describe('LectureWizardTitleComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ReactiveFormsModule, FormsModule], - declarations: [LectureUpdateWizardTitleComponent, MockComponent(MarkdownEditorComponent), MockComponent(LectureTitleChannelNameComponent)], + declarations: [LectureUpdateWizardTitleComponent, MockComponent(MarkdownEditorMonacoComponent), MockComponent(LectureTitleChannelNameComponent)], providers: [], schemas: [], }) diff --git a/src/test/javascript/spec/component/markdown-editor/markdown-editor-monaco.component.spec.ts b/src/test/javascript/spec/component/markdown-editor/markdown-editor-monaco.component.spec.ts index 2e0ea029b664..6e9ec8921a96 100644 --- a/src/test/javascript/spec/component/markdown-editor/markdown-editor-monaco.component.spec.ts +++ b/src/test/javascript/spec/component/markdown-editor/markdown-editor-monaco.component.spec.ts @@ -21,6 +21,7 @@ import { MonacoTestCaseAction } from 'app/shared/monaco-editor/model/actions/mon import { MonacoTaskAction } from 'app/shared/monaco-editor/model/actions/monaco-task.action'; import { MonacoFullscreenAction } from 'app/shared/monaco-editor/model/actions/monaco-fullscreen.action'; import { MonacoEditorOptionPreset } from 'app/shared/monaco-editor/model/monaco-editor-option-preset.model'; +import { COMMUNICATION_MARKDOWN_EDITOR_OPTIONS } from 'app/shared/monaco-editor/monaco-editor-option.helper'; describe('MarkdownEditorMonacoComponent', () => { let fixture: ComponentFixture; @@ -86,11 +87,28 @@ describe('MarkdownEditorMonacoComponent', () => { fixture.detectChanges(); const adjustEditorDimensionsSpy = jest.spyOn(comp, 'adjustEditorDimensions'); const focusSpy = jest.spyOn(comp.monacoEditor, 'focus'); - comp.onNavChanged({ nextId: 'editor', activeId: 'editor_preview', preventDefault: jest.fn() }); + comp.onNavChanged({ nextId: MarkdownEditorMonacoComponent.TAB_EDIT, activeId: MarkdownEditorMonacoComponent.TAB_PREVIEW, preventDefault: jest.fn() }); expect(adjustEditorDimensionsSpy).toHaveBeenCalledOnce(); expect(focusSpy).toHaveBeenCalledOnce(); }); + it('should emit when leaving the visual tab', () => { + const emitSpy = jest.spyOn(comp.onLeaveVisualTab, 'emit'); + fixture.detectChanges(); + comp.onNavChanged({ nextId: MarkdownEditorMonacoComponent.TAB_EDIT, activeId: MarkdownEditorMonacoComponent.TAB_VISUAL, preventDefault: jest.fn() }); + expect(emitSpy).toHaveBeenCalledOnce(); + }); + + it.each([ + { tab: MarkdownEditorMonacoComponent.TAB_EDIT, flags: [true, false, false] }, + { tab: MarkdownEditorMonacoComponent.TAB_PREVIEW, flags: [false, true, false] }, + { tab: MarkdownEditorMonacoComponent.TAB_VISUAL, flags: [false, false, true] }, + ])(`should set the correct flags when navigating to $tab`, ({ tab, flags }) => { + fixture.detectChanges(); + comp.onNavChanged({ nextId: tab, activeId: MarkdownEditorMonacoComponent.TAB_EDIT, preventDefault: jest.fn() }); + expect([comp.inEditMode, comp.inPreviewMode, comp.inVisualMode]).toEqual(flags); + }); + it('should embed manually uploaded files', () => { const embedFilesStub = jest.spyOn(comp, 'embedFiles').mockImplementation(); fixture.detectChanges(); @@ -213,11 +231,9 @@ describe('MarkdownEditorMonacoComponent', () => { it('should react to content height changes if the height is linked to the editor', () => { comp.linkEditorHeightToContentHeight = true; - comp.initialEditorHeight = MarkdownEditorHeight.MEDIUM; - comp.resizableMinHeight = MarkdownEditorHeight.SMALL; comp.resizableMaxHeight = MarkdownEditorHeight.LARGE; fixture.detectChanges(); - expect(comp.targetWrapperHeight).toBe(MarkdownEditorHeight.MEDIUM); + expect(comp.targetWrapperHeight).toBe(MarkdownEditorHeight.SMALL); comp.onContentHeightChanged(1500); expect(comp.targetWrapperHeight).toBe(MarkdownEditorHeight.LARGE); comp.onContentHeightChanged(20); @@ -235,11 +251,11 @@ describe('MarkdownEditorMonacoComponent', () => { expect(comp.targetWrapperHeight).toBe(300 - wrapperTop - dragElemHeight / 2); }); - it('should forward enableTextFieldMode call', () => { + it('should use the correct options to enable text field mode', () => { fixture.detectChanges(); - const setTextFieldModeSpy = jest.spyOn(comp.monacoEditor, 'enableTextFieldMode'); + const applySpy = jest.spyOn(comp.monacoEditor, 'applyOptionPreset'); comp.enableTextFieldMode(); - expect(setTextFieldModeSpy).toHaveBeenCalledOnce(); + expect(applySpy).toHaveBeenCalledExactlyOnceWith(COMMUNICATION_MARKDOWN_EDITOR_OPTIONS); }); it('should apply option presets to the editor', () => { diff --git a/src/test/javascript/spec/component/markdown-editor/markdown-editor-parsing.helper.spec.ts b/src/test/javascript/spec/component/markdown-editor/markdown-editor-parsing.helper.spec.ts new file mode 100644 index 000000000000..99b9ec09559f --- /dev/null +++ b/src/test/javascript/spec/component/markdown-editor/markdown-editor-parsing.helper.spec.ts @@ -0,0 +1,51 @@ +import { parseMarkdownForDomainActions } from 'app/shared/markdown-editor/monaco/markdown-editor-parsing.helper'; +import { MonacoGradingDescriptionAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-description.action'; +import { MonacoGradingFeedbackAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-feedback.action'; + +describe('MarkdownEditorParsingHelper', () => { + it('should parse markdown without domain action identifiers', () => { + const markdown = 'This is some text that uses no domain action identifiers.'; + const result = parseMarkdownForDomainActions(markdown, []); + expect(result).toEqual([{ text: markdown, action: undefined }]); + }); + + it('should parse single-line text with one domain action identifier', () => { + const action = new MonacoGradingDescriptionAction(); + const markdown = 'This is some text. [description] This is a description.'; + const result = parseMarkdownForDomainActions(markdown, [action]); + expect(result).toEqual([ + { text: 'This is some text.', action: undefined }, + { text: 'This is a description.', action }, + ]); + }); + + it('should parse single-line text with multiple domain action identifiers', () => { + const descriptionAction = new MonacoGradingDescriptionAction(); + const feedbackAction = new MonacoGradingFeedbackAction(); + const markdown = 'This is some text. [description] This is a description. [feedback] This is some feedback.'; + const result = parseMarkdownForDomainActions(markdown, [descriptionAction, feedbackAction]); + expect(result).toEqual([ + { text: 'This is some text.', action: undefined }, + { text: 'This is a description.', action: descriptionAction }, + { text: 'This is some feedback.', action: feedbackAction }, + ]); + }); + + it('should parse multi-line text without domain action identifiers', () => { + const markdown = 'This is some text that uses no domain action identifiers.\nThis is a second line.'; + const result = parseMarkdownForDomainActions(markdown, []); + expect(result).toEqual([{ text: markdown, action: undefined }]); + }); + + it('should parse multi-line text with multiple domain action identifiers', () => { + const descriptionAction = new MonacoGradingDescriptionAction(); + const feedbackAction = new MonacoGradingFeedbackAction(); + const markdown = 'This is some text. [description] This is a description.\n [feedback] This is some feedback.'; + const result = parseMarkdownForDomainActions(markdown, [descriptionAction, feedbackAction]); + expect(result).toEqual([ + { text: 'This is some text.', action: undefined }, + { text: 'This is a description.', action: descriptionAction }, + { text: 'This is some feedback.', action: feedbackAction }, + ]); + }); +}); diff --git a/src/test/javascript/spec/component/multiple-choice-question/multiple-choice-question-edit.component.spec.ts b/src/test/javascript/spec/component/multiple-choice-question/multiple-choice-question-edit.component.spec.ts index 04a5b5b9e616..763122daee76 100644 --- a/src/test/javascript/spec/component/multiple-choice-question/multiple-choice-question-edit.component.spec.ts +++ b/src/test/javascript/spec/component/multiple-choice-question/multiple-choice-question-edit.component.spec.ts @@ -7,12 +7,6 @@ import { QuizScoringInfoModalComponent } from 'app/exercises/quiz/manage/quiz-sc import { DragAndDropQuestionComponent } from 'app/exercises/quiz/shared/questions/drag-and-drop-question/drag-and-drop-question.component'; import { MultipleChoiceQuestionComponent } from 'app/exercises/quiz/shared/questions/multiple-choice-question/multiple-choice-question.component'; import { SecuredImageComponent } from 'app/shared/image/secured-image.component'; -import { CorrectOptionCommand } from 'app/shared/markdown-editor/domainCommands/correctOptionCommand'; -import { ExplanationCommand } from 'app/shared/markdown-editor/domainCommands/explanation.command'; -import { HintCommand } from 'app/shared/markdown-editor/domainCommands/hint.command'; -import { IncorrectOptionCommand } from 'app/shared/markdown-editor/domainCommands/incorrectOptionCommand'; -import { TestCaseCommand } from 'app/shared/markdown-editor/domainCommands/programming-exercise/testCase.command'; -import { MarkdownEditorComponent } from 'app/shared/markdown-editor/markdown-editor.component'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { MockComponent, MockDirective, MockPipe } from 'ng-mocks'; import { DragDropModule } from '@angular/cdk/drag-drop'; @@ -21,6 +15,12 @@ import { NgbCollapseMocksModule } from '../../helpers/mocks/directive/ngbCollaps import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { MultipleChoiceVisualQuestionComponent } from 'app/exercises/quiz/shared/questions/multiple-choice-question/multiple-choice-visual-question.component'; import { ScoringType } from 'app/entities/quiz/quiz-question.model'; +import { MonacoQuizHintAction } from 'app/shared/monaco-editor/model/actions/quiz/monaco-quiz-hint.action'; +import { MonacoQuizExplanationAction } from 'app/shared/monaco-editor/model/actions/quiz/monaco-quiz-explanation.action'; +import { MonacoWrongMultipleChoiceAnswerAction } from 'app/shared/monaco-editor/model/actions/quiz/monaco-wrong-multiple-choice-answer.action'; +import { MonacoCorrectMultipleChoiceAnswerAction } from 'app/shared/monaco-editor/model/actions/quiz/monaco-correct-multiple-choice-answer.action'; +import { MonacoTestCaseAction } from 'app/shared/monaco-editor/model/actions/monaco-test-case.action'; +import { MarkdownEditorMonacoComponent } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; describe('MultipleChoiceQuestionEditComponent', () => { let fixture: ComponentFixture; @@ -47,7 +47,7 @@ describe('MultipleChoiceQuestionEditComponent', () => { MultipleChoiceQuestionEditComponent, MockPipe(ArtemisTranslatePipe), MockComponent(QuizScoringInfoModalComponent), - MockComponent(MarkdownEditorComponent), + MockComponent(MarkdownEditorMonacoComponent), MockComponent(SecuredImageComponent), MockComponent(DragAndDropQuestionComponent), MockComponent(MultipleChoiceQuestionComponent), @@ -91,12 +91,12 @@ describe('MultipleChoiceQuestionEditComponent', () => { }); it('should parse answer options but not question titles', () => { - component.domainCommandsFound([ - ['text1', new TestCaseCommand()], - ['text2', new CorrectOptionCommand()], - ['text3', new IncorrectOptionCommand()], - ['text4', new ExplanationCommand()], - ['text5', new HintCommand()], + component.domainActionsFound([ + { text: 'text1', action: new MonacoTestCaseAction() }, + { text: 'text2', action: new MonacoCorrectMultipleChoiceAnswerAction() }, + { text: 'text3', action: new MonacoWrongMultipleChoiceAnswerAction() }, + { text: 'text4', action: new MonacoQuizExplanationAction() }, + { text: 'text5', action: new MonacoQuizHintAction() }, ]); const expected: MultipleChoiceQuestion = { @@ -129,12 +129,12 @@ describe('MultipleChoiceQuestionEditComponent', () => { }); it('should parse answer options with question titles', () => { - component.domainCommandsFound([ - ['text1', new ExplanationCommand()], - ['text2', new HintCommand()], - ['text3', new TestCaseCommand()], - ['text4', new CorrectOptionCommand()], - ['text5', new IncorrectOptionCommand()], + component.domainActionsFound([ + { text: 'text1', action: new MonacoQuizExplanationAction() }, + { text: 'text2', action: new MonacoQuizHintAction() }, + { text: 'text3', action: new MonacoTestCaseAction() }, + { text: 'text4', action: new MonacoCorrectMultipleChoiceAnswerAction() }, + { text: 'text5', action: new MonacoWrongMultipleChoiceAnswerAction() }, ]); const expected: MultipleChoiceQuestion = { @@ -165,7 +165,7 @@ describe('MultipleChoiceQuestionEditComponent', () => { }); it('should parse question titles', () => { - component.domainCommandsFound([['text1', null]]); + component.domainActionsFound([{ text: 'text1', action: undefined }]); const expected: MultipleChoiceQuestion = { id: question.id, @@ -183,8 +183,8 @@ describe('MultipleChoiceQuestionEditComponent', () => { expect(component.showMultipleChoiceQuestionPreview).toBeTrue(); }); - it('should find no domain commands', () => { - component.domainCommandsFound([]); + it('should find no domain actions', () => { + component.domainActionsFound([]); expectCleanupQuestion(); expect(component.showMultipleChoiceQuestionPreview).toBeTrue(); diff --git a/src/test/javascript/spec/component/shared/grading-instructions-details.component.spec.ts b/src/test/javascript/spec/component/shared/grading-instructions-details.component.spec.ts index c5f89566b009..ae00000ae4b6 100644 --- a/src/test/javascript/spec/component/shared/grading-instructions-details.component.spec.ts +++ b/src/test/javascript/spec/component/shared/grading-instructions-details.component.spec.ts @@ -4,20 +4,20 @@ import { Exercise } from 'app/entities/exercise.model'; import { GradingCriterion } from 'app/exercises/shared/structured-grading-criterion/grading-criterion.model'; import { GradingInstruction } from 'app/exercises/shared/structured-grading-criterion/grading-instruction.model'; import { GradingInstructionsDetailsComponent } from 'app/exercises/shared/structured-grading-criterion/grading-instructions-details/grading-instructions-details.component'; -import { CreditsCommand } from 'app/shared/markdown-editor/domainCommands/credits.command'; -import { DomainCommand } from 'app/shared/markdown-editor/domainCommands/domainCommand'; -import { FeedbackCommand } from 'app/shared/markdown-editor/domainCommands/feedback.command'; -import { GradingCriterionCommand } from 'app/shared/markdown-editor/domainCommands/gradingCriterionCommand'; -import { GradingInstructionCommand } from 'app/shared/markdown-editor/domainCommands/gradingInstruction.command'; -import { GradingScaleCommand } from 'app/shared/markdown-editor/domainCommands/gradingScaleCommand'; -import { InstructionDescriptionCommand } from 'app/shared/markdown-editor/domainCommands/instructionDescription.command'; -import { UsageCountCommand } from 'app/shared/markdown-editor/domainCommands/usageCount.command'; import { LocalStorageService, SessionStorageService } from 'ngx-webstorage'; import { MockSyncStorage } from '../../helpers/mocks/service/mock-sync-storage.service'; import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; import { ArtemisTestModule } from '../../test.module'; - -describe('Grading Instructions Management Component', () => { +import { MonacoGradingInstructionAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-instruction.action'; +import { MonacoGradingCreditsAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-credits.action'; +import { MonacoGradingScaleAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-scale.action'; +import { MonacoGradingDescriptionAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-description.action'; +import { MonacoGradingFeedbackAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-feedback.action'; +import { MonacoGradingUsageCountAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-usage-count.action'; +import { MonacoGradingCriterionAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-criterion.action'; +import { TextWithDomainAction } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; + +describe('GradingInstructionsDetailsComponent', () => { let component: GradingInstructionsDetailsComponent; let fixture: ComponentFixture; let gradingInstruction: GradingInstruction; @@ -28,22 +28,13 @@ describe('Grading Instructions Management Component', () => { const backupExercise = { id: 1 } as Exercise; const criterionMarkdownText = - '[criterion]' + - 'testCriteria' + - '\n' + - '\t' + - '[instruction]\n' + - '\t' + - '[credits]' + - ' 1\n' + - '\t' + - '[gradingScale] scale\n' + - '\t' + - '[description] description\n' + - '\t' + - '[feedback] feedback\n' + - '\t' + - '[maxCountInScore] 0\n\n'; + '[criterion] testCriteria\n' + + '\t[instruction]\n' + + '\t[credits] 1\n' + + '\t[gradingScale] scale\n' + + '\t[description] description\n' + + '\t[feedback] feedback\n' + + '\t[maxCountInScore] 0\n\n'; beforeEach(() => { TestBed.configureTestingModule({ @@ -146,10 +137,10 @@ describe('Grading Instructions Management Component', () => { it('should change grading instruction', () => { const newDescription = 'new text'; - const domainCommands = [[newDescription, new InstructionDescriptionCommand()]] as [string, DomainCommand | null][]; + const domainActions = [{ text: newDescription, action: new MonacoGradingDescriptionAction() }] as TextWithDomainAction[]; component.exercise.gradingCriteria = [gradingCriterion]; - component.onInstructionChange(domainCommands, gradingInstruction); + component.onInstructionChange(domainActions, gradingInstruction); fixture.detectChanges(); expect(component.exercise.gradingCriteria[0].structuredGradingInstructions[0].instructionDescription).toEqual(newDescription); @@ -165,25 +156,38 @@ describe('Grading Instructions Management Component', () => { it('should set grading instruction text for exercise', () => { const markdownText = 'new text'; - const domainCommands = [[markdownText, null]] as [string, DomainCommand | null][]; + const domainActions = [{ text: markdownText, action: undefined }] as TextWithDomainAction[]; - component.setExerciseGradingInstructionText(domainCommands); + component.setExerciseGradingInstructionText(domainActions); fixture.detectChanges(); expect(component.exercise.gradingInstructions).toEqual(markdownText); }); - it('should set grading instruction without criterion command when markdown-change triggered', () => { - const domainCommands = [ - ['', new GradingInstructionCommand()], - ['1', new CreditsCommand()], - ['scale', new GradingScaleCommand()], - ['description', new InstructionDescriptionCommand()], - ['feedback', new FeedbackCommand()], - ['0', new UsageCountCommand()], - ] as [string, DomainCommand | null][]; + const getDomainActionArray = () => { + const creditsAction = new MonacoGradingCreditsAction(); + const scaleAction = new MonacoGradingScaleAction(); + const descriptionAction = new MonacoGradingDescriptionAction(); + const feedbackAction = new MonacoGradingFeedbackAction(); + const usageCountAction = new MonacoGradingUsageCountAction(); + const instructionAction = new MonacoGradingInstructionAction(creditsAction, scaleAction, descriptionAction, feedbackAction, usageCountAction); + const criterionAction = new MonacoGradingCriterionAction(instructionAction); + + return [ + { text: 'testCriteria', action: criterionAction }, + { text: '', action: instructionAction }, + { text: '1', action: creditsAction }, + { text: 'scale', action: scaleAction }, + { text: 'description', action: descriptionAction }, + { text: 'feedback', action: feedbackAction }, + { text: '0', action: usageCountAction }, + ] as TextWithDomainAction[]; + }; - component.domainCommandsFound(domainCommands); + it('should set grading instruction without criterion action when markdown-change triggered', () => { + const domainActionsWithoutCriterion = getDomainActionArray().slice(1); + + component.onDomainActionsFound(domainActionsWithoutCriterion); fixture.detectChanges(); expect(component.exercise.gradingCriteria).toBeDefined(); @@ -191,18 +195,10 @@ describe('Grading Instructions Management Component', () => { expect(gradingCriteria.structuredGradingInstructions[0]).toEqual(gradingInstructionWithoutId); }); - it('should set grading instruction with criterion command when markdown-change triggered', () => { - const domainCommands = [ - ['testCriteria', new GradingCriterionCommand()], - ['', new GradingInstructionCommand()], - ['1', new CreditsCommand()], - ['scale', new GradingScaleCommand()], - ['description', new InstructionDescriptionCommand()], - ['feedback', new FeedbackCommand()], - ['0', new UsageCountCommand()], - ] as [string, DomainCommand | null][]; - - component.domainCommandsFound(domainCommands); + it('should set grading instruction with criterion action when markdown-change triggered', () => { + const domainActions = getDomainActionArray(); + + component.onDomainActionsFound(domainActions); fixture.detectChanges(); expect(component.exercise.gradingCriteria).toBeDefined(); diff --git a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-action-quiz.integration.spec.ts b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-action-quiz.integration.spec.ts index 4ef09a0bed8c..482467968cdb 100644 --- a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-action-quiz.integration.spec.ts +++ b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-action-quiz.integration.spec.ts @@ -5,6 +5,10 @@ import { MonacoEditorModule } from 'app/shared/monaco-editor/monaco-editor.modul import { MockResizeObserver } from '../../../helpers/mocks/service/mock-resize-observer'; import { MonacoInsertShortAnswerOptionAction } from 'app/shared/monaco-editor/model/actions/quiz/monaco-insert-short-answer-option.action'; import { MonacoInsertShortAnswerSpotAction } from 'app/shared/monaco-editor/model/actions/quiz/monaco-insert-short-answer-spot.action'; +import { MonacoWrongMultipleChoiceAnswerAction } from 'app/shared/monaco-editor/model/actions/quiz/monaco-wrong-multiple-choice-answer.action'; +import { MonacoCorrectMultipleChoiceAnswerAction } from 'app/shared/monaco-editor/model/actions/quiz/monaco-correct-multiple-choice-answer.action'; +import { MonacoQuizExplanationAction } from 'app/shared/monaco-editor/model/actions/quiz/monaco-quiz-explanation.action'; +import { MonacoQuizHintAction } from 'app/shared/monaco-editor/model/actions/quiz/monaco-quiz-hint.action'; describe('MonacoEditorActionQuizIntegration', () => { let fixture: ComponentFixture; @@ -135,4 +139,40 @@ describe('MonacoEditorActionQuizIntegration', () => { expect(comp.getText()).toBe(questionText + '[-spot 1]' + `\n\n\n[-option 1] ${MonacoInsertShortAnswerOptionAction.DEFAULT_TEXT}`); }); }); + + describe('Multiple Choice answer options', () => { + it('should insert a wrong MC option', () => { + comp.triggerKeySequence('This is a question that needs some options.'); + const action = new MonacoWrongMultipleChoiceAnswerAction(); + comp.registerAction(action); + action.executeInCurrentEditor(); + expect(comp.getText()).toBe('This is a question that needs some options.\n[wrong] Enter a wrong answer option here'); + }); + + it('should insert a correct MC option', () => { + comp.triggerKeySequence('This is a question that needs some options.'); + const action = new MonacoCorrectMultipleChoiceAnswerAction(); + comp.registerAction(action); + action.executeInCurrentEditor(); + expect(comp.getText()).toBe('This is a question that needs some options.\n[correct] Enter a correct answer option here'); + }); + + it('should add an explanation to an answer option', () => { + comp.triggerKeySequence('This is a question that has an option.\n[correct] Option 1'); + const action = new MonacoQuizExplanationAction(); + comp.registerAction(action); + action.executeInCurrentEditor(); + expect(comp.getText()).toBe( + 'This is a question that has an option.\n[correct] Option 1\n\t[exp] Add an explanation here (only visible in feedback after quiz has ended)', + ); + }); + + it('should add a hint to an answer option', () => { + comp.triggerKeySequence('This is a question that has an option.\n[correct] Option 1'); + const action = new MonacoQuizHintAction(); + comp.registerAction(action); + action.executeInCurrentEditor(); + expect(comp.getText()).toBe('This is a question that has an option.\n[correct] Option 1\n\t[hint] Add a hint here (visible during the quiz via ?-Button)'); + }); + }); }); diff --git a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-action.integration.spec.ts b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-action.integration.spec.ts index c1204d455f57..3ccaa08b5f79 100644 --- a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-action.integration.spec.ts +++ b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-action.integration.spec.ts @@ -180,7 +180,7 @@ describe('MonacoEditorActionIntegration', () => { const action = new MonacoTaskAction(); comp.registerAction(action); action.executeInCurrentEditor(); - expect(comp.getText()).toBe(MonacoTaskAction.INSERT_TASK_TEXT); + expect(comp.getText()).toBe(`[task]${MonacoTaskAction.TEXT}`); }); it('should enter fullscreen', () => { diff --git a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-grading-instructions.integration.spec.ts b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-grading-instructions.integration.spec.ts new file mode 100644 index 000000000000..a5df872438f0 --- /dev/null +++ b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-grading-instructions.integration.spec.ts @@ -0,0 +1,74 @@ +import { MonacoEditorComponent } from 'app/shared/monaco-editor/monaco-editor.component'; +import { ArtemisTestModule } from '../../../test.module'; +import { MonacoEditorModule } from 'app/shared/monaco-editor/monaco-editor.module'; +import { MockResizeObserver } from '../../../helpers/mocks/service/mock-resize-observer'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MonacoGradingInstructionAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-instruction.action'; +import { MonacoGradingCreditsAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-credits.action'; +import { MonacoGradingScaleAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-scale.action'; +import { MonacoGradingDescriptionAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-description.action'; +import { MonacoGradingFeedbackAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-feedback.action'; +import { MonacoGradingUsageCountAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-usage-count.action'; +import { MonacoGradingCriterionAction } from 'app/shared/monaco-editor/model/actions/grading-criteria/monaco-grading-criterion.action'; + +describe('MonacoEditorActionGradingInstructionsIntegration', () => { + let fixture: ComponentFixture; + let comp: MonacoEditorComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, MonacoEditorModule], + declarations: [MonacoEditorComponent], + providers: [], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(MonacoEditorComponent); + comp = fixture.componentInstance; + global.ResizeObserver = jest.fn().mockImplementation((callback: ResizeObserverCallback) => { + return new MockResizeObserver(callback); + }); + fixture.detectChanges(); + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + const setupActions = () => { + const creditsAction = new MonacoGradingCreditsAction(); + const scaleAction = new MonacoGradingScaleAction(); + const descriptionAction = new MonacoGradingDescriptionAction(); + const feedbackAction = new MonacoGradingFeedbackAction(); + const usageCountAction = new MonacoGradingUsageCountAction(); + const instructionAction = new MonacoGradingInstructionAction(creditsAction, scaleAction, descriptionAction, feedbackAction, usageCountAction); + const criterionAction = new MonacoGradingCriterionAction(instructionAction); + [creditsAction, scaleAction, descriptionAction, feedbackAction, usageCountAction, instructionAction, criterionAction].forEach((action) => comp.registerAction(action)); + return { creditsAction, scaleAction, descriptionAction, feedbackAction, usageCountAction, instructionAction, criterionAction }; + }; + + const expectedInstructionTextWithoutCriterion = + '\n[instruction]' + + '\n\t[credits] 0' + + '\n\t[gradingScale] Add instruction grading scale here (only visible for tutors)' + + '\n\t[description] Add grading instruction here (only visible for tutors)' + + '\n\t[feedback] Add feedback for students here (visible for students)' + + '\n\t[maxCountInScore] 0'; + + const generalInstructionText = 'These are some general instructions for the tutors.'; + + it('should insert grading instructions', () => { + comp.triggerKeySequence(generalInstructionText); + const actions = setupActions(); + actions.instructionAction.executeInCurrentEditor(); + expect(comp.getText()).toBe(generalInstructionText + expectedInstructionTextWithoutCriterion); + }); + + it('should insert grading criterion', () => { + comp.triggerKeySequence(generalInstructionText); + const actions = setupActions(); + actions.criterionAction.executeInCurrentEditor(); + expect(comp.getText()).toBe(`${generalInstructionText}\n[criterion] Add criterion title (only visible to tutors)${expectedInstructionTextWithoutCriterion}`); + }); +}); diff --git a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts index dc9f7fb8b33c..ae34f743a3a7 100644 --- a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts +++ b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts @@ -170,6 +170,12 @@ describe('MonacoEditorComponent', () => { expect(documentHighlightedMargins).toHaveLength(3); }); + it('should get the number of lines in the editor', () => { + fixture.detectChanges(); + comp.setText(multiLineText); + expect(comp.getNumberOfLines()).toBe(5); + }); + it('should pass the current line number to the line decorations hover button when clicked', () => { const clickCallbackStub = jest.fn(); const className = 'testClass'; diff --git a/src/test/javascript/spec/component/standardized-competencies/detail/knowledge-area-edit.component.spec.ts b/src/test/javascript/spec/component/standardized-competencies/detail/knowledge-area-edit.component.spec.ts index 1dcb3a517efd..da3219405bd2 100644 --- a/src/test/javascript/spec/component/standardized-competencies/detail/knowledge-area-edit.component.spec.ts +++ b/src/test/javascript/spec/component/standardized-competencies/detail/knowledge-area-edit.component.spec.ts @@ -5,7 +5,6 @@ import { KnowledgeAreaEditComponent } from 'app/admin/standardized-competencies/ import { ButtonComponent } from 'app/shared/components/button.component'; import { MockComponent, MockDirective, MockPipe } from 'ng-mocks'; import { HtmlForMarkdownPipe } from 'app/shared/pipes/html-for-markdown.pipe'; -import { MarkdownEditorComponent } from 'app/shared/markdown-editor/markdown-editor.component'; import { TaxonomySelectComponent } from 'app/course/competencies/taxonomy-select/taxonomy-select.component'; import { TranslatePipeMock } from '../../../helpers/mocks/service/mock-translate.service'; import { TranslateDirective } from 'app/shared/language/translate.directive'; @@ -13,6 +12,7 @@ import { NgbTooltipMocksModule } from '../../../helpers/mocks/directive/ngbToolt import { DeleteButtonDirective } from 'app/shared/delete-dialog/delete-button.directive'; import { KnowledgeAreaDTO } from 'app/entities/competency/standardized-competency.model'; import { By } from '@angular/platform-browser'; +import { MarkdownEditorMonacoComponent } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; describe('KnowledgeAreaEditComponent', () => { let componentFixture: ComponentFixture; @@ -44,7 +44,7 @@ describe('KnowledgeAreaEditComponent', () => { MockComponent(ButtonComponent), TranslatePipeMock, MockPipe(HtmlForMarkdownPipe), - MockComponent(MarkdownEditorComponent), + MockComponent(MarkdownEditorMonacoComponent), MockComponent(TaxonomySelectComponent), MockDirective(TranslateDirective), MockDirective(DeleteButtonDirective), diff --git a/src/test/javascript/spec/component/standardized-competencies/detail/standardized-competency-edit.spec.ts b/src/test/javascript/spec/component/standardized-competencies/detail/standardized-competency-edit.spec.ts index b33c93d1eabd..35d65359675f 100644 --- a/src/test/javascript/spec/component/standardized-competencies/detail/standardized-competency-edit.spec.ts +++ b/src/test/javascript/spec/component/standardized-competencies/detail/standardized-competency-edit.spec.ts @@ -6,13 +6,13 @@ import { ButtonComponent } from 'app/shared/components/button.component'; import { CompetencyTaxonomy } from 'app/entities/competency.model'; import { MockComponent, MockDirective, MockPipe } from 'ng-mocks'; import { HtmlForMarkdownPipe } from 'app/shared/pipes/html-for-markdown.pipe'; -import { MarkdownEditorComponent } from 'app/shared/markdown-editor/markdown-editor.component'; import { TaxonomySelectComponent } from 'app/course/competencies/taxonomy-select/taxonomy-select.component'; import { TranslatePipeMock } from '../../../helpers/mocks/service/mock-translate.service'; import { TranslateDirective } from 'app/shared/language/translate.directive'; import { NgbTooltipMocksModule } from '../../../helpers/mocks/directive/ngbTooltipMocks.module'; import { DeleteButtonDirective } from 'app/shared/delete-dialog/delete-button.directive'; import { KnowledgeAreaDTO, StandardizedCompetencyDTO } from 'app/entities/competency/standardized-competency.model'; +import { MarkdownEditorMonacoComponent } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; describe('StandardizedCompetencyEditComponent', () => { let componentFixture: ComponentFixture; @@ -47,7 +47,7 @@ describe('StandardizedCompetencyEditComponent', () => { MockComponent(ButtonComponent), TranslatePipeMock, MockPipe(HtmlForMarkdownPipe), - MockComponent(MarkdownEditorComponent), + MockComponent(MarkdownEditorMonacoComponent), MockComponent(TaxonomySelectComponent), MockDirective(TranslateDirective), MockDirective(DeleteButtonDirective), diff --git a/src/test/javascript/spec/component/tutorial-groups/tutorial-groups-management/tutorial-groups/crud/tutorial-group-form.component.spec.ts b/src/test/javascript/spec/component/tutorial-groups/tutorial-groups-management/tutorial-groups/crud/tutorial-group-form.component.spec.ts index 04d2c353f042..06e2de934c41 100644 --- a/src/test/javascript/spec/component/tutorial-groups/tutorial-groups-management/tutorial-groups/crud/tutorial-group-form.component.spec.ts +++ b/src/test/javascript/spec/component/tutorial-groups/tutorial-groups-management/tutorial-groups/crud/tutorial-group-form.component.spec.ts @@ -26,7 +26,7 @@ import { generateClickSubmitButton, generateTestFormIsInvalidOnMissingRequiredPr import { ArtemisDateRangePipe } from 'app/shared/pipes/artemis-date-range.pipe'; import { runOnPushChangeDetection } from '../../../../../helpers/on-push-change-detection.helper'; -@Component({ selector: 'jhi-markdown-editor', template: '' }) +@Component({ selector: 'jhi-markdown-editor-monaco', template: '' }) class MarkdownEditorStubComponent { @Input() markdown: string; @Input() enableResize = false; diff --git a/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts b/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts index 5fffcf112b9e..a793a859f204 100644 --- a/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts +++ b/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts @@ -56,7 +56,6 @@ import { CodeEditorBuildOutputComponent } from 'app/exercises/programming/shared import { KeysPipe } from 'app/shared/pipes/keys.pipe'; import { CodeEditorInstructionsComponent } from 'app/exercises/programming/shared/code-editor/instructions/code-editor-instructions.component'; import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe'; -import { MarkdownEditorComponent } from 'app/shared/markdown-editor/markdown-editor.component'; import { ProgrammingExerciseInstructionComponent } from 'app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component'; import { ProgrammingExerciseInstructionAnalysisComponent } from 'app/exercises/programming/manage/instructions-editor/analysis/programming-exercise-instruction-analysis.component'; import { ResultComponent } from 'app/exercises/shared/result/result.component'; @@ -107,7 +106,6 @@ describe('CodeEditorInstructorIntegration', () => { UpdatingResultComponent, MockComponent(ProgrammingExerciseStudentTriggerBuildButtonComponent), ProgrammingExerciseEditableInstructionComponent, - MockComponent(MarkdownEditorComponent), MockComponent(MarkdownEditorMonacoComponent), ProgrammingExerciseInstructionComponent, MockComponent(ProgrammingExerciseInstructionAnalysisComponent), diff --git a/src/test/playwright/support/pageobjects/course/CourseCommunicationPage.ts b/src/test/playwright/support/pageobjects/course/CourseCommunicationPage.ts index 17d24b98c07a..6fd47e0a9a7f 100644 --- a/src/test/playwright/support/pageobjects/course/CourseCommunicationPage.ts +++ b/src/test/playwright/support/pageobjects/course/CourseCommunicationPage.ts @@ -156,7 +156,7 @@ export class CourseCommunicationPage { */ async reply(postID: number, content: string) { const postElement = this.getSinglePost(postID); - const postReplyField = postElement.locator('.new-reply-inline-input .markdown-editor .ace_content'); + const postReplyField = postElement.locator('.new-reply-inline-input .markdown-editor .monaco-editor'); await postReplyField.click(); await postReplyField.pressSequentially(content); const responsePromise = this.page.waitForResponse(`${COURSE_BASE}/*/answer-posts`); @@ -172,7 +172,7 @@ export class CourseCommunicationPage { */ async replyWithMessage(postID: number, content: string): Promise { const postElement = this.getSinglePost(postID); - const postReplyField = postElement.locator('.new-reply-inline-input .markdown-editor .ace_content'); + const postReplyField = postElement.locator('.new-reply-inline-input .markdown-editor .monaco-editor'); await postReplyField.click(); await postReplyField.pressSequentially(content); const responsePromise = this.page.waitForResponse(`${COURSE_BASE}/*/answer-messages`); diff --git a/src/test/playwright/support/pageobjects/exam/ExamCreationPage.ts b/src/test/playwright/support/pageobjects/exam/ExamCreationPage.ts index 53ac528bcdcc..e710822e6723 100644 --- a/src/test/playwright/support/pageobjects/exam/ExamCreationPage.ts +++ b/src/test/playwright/support/pageobjects/exam/ExamCreationPage.ts @@ -131,15 +131,10 @@ export class ExamCreationPage { private async enterText(selector: string, text: string) { const textField = this.page.locator(selector); - const textInput = textField.locator('.ace_text-input'); - // const currentText = textField.locator('.ace_text'); - - // if (await currentText.isVisible()) { - // const text = await currentText.textContent(); + const textInput = textField.locator('.monaco-editor'); if (text) { await clearTextField(textInput); } - // } await textInput.pressSequentially(text); } } diff --git a/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts b/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts index ad7dae5a7225..e2e5f76c24f0 100644 --- a/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts +++ b/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts @@ -114,7 +114,7 @@ export class ExamManagementPage { } async typeAnnouncementMessage(message: string) { - await this.page.locator('.ace_text-input').fill(message); + await this.page.locator('.monaco-editor textarea').fill(message); } async verifyAnnouncementContent(announcementTime: Dayjs, message: string, authorUsername: string) { diff --git a/src/test/playwright/support/pageobjects/exercises/file-upload/FileUploadExerciseCreationPage.ts b/src/test/playwright/support/pageobjects/exercises/file-upload/FileUploadExerciseCreationPage.ts index 1520841bccf9..760eb91275dc 100644 --- a/src/test/playwright/support/pageobjects/exercises/file-upload/FileUploadExerciseCreationPage.ts +++ b/src/test/playwright/support/pageobjects/exercises/file-upload/FileUploadExerciseCreationPage.ts @@ -53,6 +53,6 @@ export class FileUploadExerciseCreationPage { } private async typeText(selector: string, text: string) { - await this.page.locator(selector).locator('.ace_content').pressSequentially(text); + await this.page.locator(selector).locator('.monaco-editor').pressSequentially(text); } } diff --git a/src/test/playwright/support/pageobjects/exercises/quiz/QuizExerciseCreationPage.ts b/src/test/playwright/support/pageobjects/exercises/quiz/QuizExerciseCreationPage.ts index 7317e96d0711..9cccd6576272 100644 --- a/src/test/playwright/support/pageobjects/exercises/quiz/QuizExerciseCreationPage.ts +++ b/src/test/playwright/support/pageobjects/exercises/quiz/QuizExerciseCreationPage.ts @@ -25,8 +25,8 @@ export class QuizExerciseCreationPage { await this.page.locator('#score').fill(points.toString()); const fileContent = await Fixtures.get('exercise/quiz/multiple_choice/question.txt'); - const textInputField = this.page.locator('.ace_text-input'); - await textInputField.focus(); + const textInputField = this.page.locator('.monaco-editor'); + await textInputField.click(); await textInputField.pressSequentially(fileContent!); } @@ -61,9 +61,9 @@ export class QuizExerciseCreationPage { await drag(this.page, dragLocator, dropLocator); const fileContent = await Fixtures.get('exercise/quiz/drag_and_drop/question.txt'); - const textInputField = this.page.locator('.ace_text-input'); + const textInputField = this.page.locator('.monaco-editor'); await clearTextField(textInputField); - await textInputField.fill(fileContent!); + await textInputField.pressSequentially(fileContent!); } async createDragAndDropItem(text: string) { diff --git a/src/test/playwright/support/pageobjects/exercises/text/TextExerciseCreationPage.ts b/src/test/playwright/support/pageobjects/exercises/text/TextExerciseCreationPage.ts index a18217aac3d1..7d13e7ee1101 100644 --- a/src/test/playwright/support/pageobjects/exercises/text/TextExerciseCreationPage.ts +++ b/src/test/playwright/support/pageobjects/exercises/text/TextExerciseCreationPage.ts @@ -57,7 +57,7 @@ export class TextExerciseCreationPage { } private async typeText(selector: string, text: string) { - const textField = this.page.locator(selector).locator('.ace_content'); + const textField = this.page.locator(selector).locator('.monaco-editor'); await textField.click(); await textField.pressSequentially(text); } diff --git a/src/test/playwright/support/pageobjects/lecture/LectureCreationPage.ts b/src/test/playwright/support/pageobjects/lecture/LectureCreationPage.ts index 2d2e122969a6..8e4fa68e7f31 100644 --- a/src/test/playwright/support/pageobjects/lecture/LectureCreationPage.ts +++ b/src/test/playwright/support/pageobjects/lecture/LectureCreationPage.ts @@ -35,7 +35,7 @@ export class LectureCreationPage { * @param description - The description text for the lecture. */ async typeDescription(description: string) { - const descriptionField = this.page.locator('.ace_content'); + const descriptionField = this.page.locator('.monaco-editor'); await descriptionField.click(); await descriptionField.pressSequentially(description); } diff --git a/src/test/playwright/support/pageobjects/lecture/LectureManagementPage.ts b/src/test/playwright/support/pageobjects/lecture/LectureManagementPage.ts index af5624689a7d..40b336fdb1de 100644 --- a/src/test/playwright/support/pageobjects/lecture/LectureManagementPage.ts +++ b/src/test/playwright/support/pageobjects/lecture/LectureManagementPage.ts @@ -95,7 +95,7 @@ export class LectureManagementPage { await this.openCreateUnit(UnitType.TEXT); await this.page.fill('#name', name); await this.page.fill('#pick-releaseDate #date-input-field', releaseDate.toString()); - const contentField = this.page.locator('.ace_content'); + const contentField = this.page.locator('.monaco-editor'); await contentField.click(); await contentField.pressSequentially(text); return this.submitUnit(); From 1bb98807618a5f6c4d4a859a96273db296414b67 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Sat, 24 Aug 2024 18:39:06 +0300 Subject: [PATCH 03/15] Development: Update client dependencies --- package-lock.json | 534 +++++++++++++++++++++++++--------------------- package.json | 52 +++-- 2 files changed, 321 insertions(+), 265 deletions(-) diff --git a/package-lock.json b/package-lock.json index bc5fa5d014c4..4ac4c8f4a4b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,18 +10,18 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@angular/animations": "18.2.0", - "@angular/cdk": "18.2.0", - "@angular/common": "18.2.0", - "@angular/compiler": "18.2.0", - "@angular/core": "18.2.0", - "@angular/forms": "18.2.0", - "@angular/localize": "18.2.0", - "@angular/material": "18.2.0", - "@angular/platform-browser": "18.2.0", - "@angular/platform-browser-dynamic": "18.2.0", - "@angular/router": "18.2.0", - "@angular/service-worker": "18.2.0", + "@angular/animations": "18.2.1", + "@angular/cdk": "18.2.1", + "@angular/common": "18.2.1", + "@angular/compiler": "18.2.1", + "@angular/core": "18.2.1", + "@angular/forms": "18.2.1", + "@angular/localize": "18.2.1", + "@angular/material": "18.2.1", + "@angular/platform-browser": "18.2.1", + "@angular/platform-browser-dynamic": "18.2.1", + "@angular/router": "18.2.1", + "@angular/service-worker": "18.2.1", "@ctrl/ngx-emoji-mart": "9.2.0", "@danielmoncada/angular-datetime-picker": "18.1.0", "@fingerprintjs/fingerprintjs": "4.4.3", @@ -38,7 +38,7 @@ "@swimlane/ngx-charts": "20.5.0", "@swimlane/ngx-graph": "8.4.0", "@vscode/codicons": "0.0.36", - "ace-builds": "1.35.5", + "ace-builds": "1.36.0", "bootstrap": "5.3.3", "brace": "0.11.1", "compare-versions": "6.1.1", @@ -57,7 +57,7 @@ "jszip": "3.10.1", "lodash-es": "4.17.21", "mobile-drag-drop": "3.0.0-rc.0", - "monaco-editor": "0.50.0", + "monaco-editor": "0.51.0", "ngx-infinite-scroll": "18.0.0", "ngx-webstorage": "18.0.0", "papaparse": "5.4.1", @@ -66,12 +66,12 @@ "showdown": "2.1.0", "showdown-highlight": "3.1.0", "showdown-katex": "0.6.0", - "simple-statistics": "7.8.3", + "simple-statistics": "7.8.4", "smoothscroll-polyfill": "0.4.4", "sockjs-client": "1.6.1", "split.js": "1.6.5", "ts-cacheable": "1.0.10", - "tslib": "2.6.3", + "tslib": "2.7.0", "uuid": "10.0.0", "webstomp-client": "1.2.6", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", @@ -79,22 +79,22 @@ }, "devDependencies": { "@angular-builders/jest": "18.0.0", - "@angular-devkit/build-angular": "18.2.0", + "@angular-devkit/build-angular": "18.2.1", "@angular-eslint/builder": "18.3.0", "@angular-eslint/eslint-plugin": "18.3.0", "@angular-eslint/eslint-plugin-template": "18.3.0", "@angular-eslint/schematics": "18.3.0", "@angular-eslint/template-parser": "18.3.0", - "@angular/cli": "18.2.0", - "@angular/compiler-cli": "18.2.0", - "@angular/language-service": "18.2.0", + "@angular/cli": "18.2.1", + "@angular/compiler-cli": "18.2.1", + "@angular/language-service": "18.2.1", "@sentry/types": "8.26.0", "@types/crypto-js": "4.2.2", "@types/d3-shape": "3.1.6", "@types/dompurify": "3.0.5", "@types/jest": "29.5.12", "@types/lodash-es": "4.17.12", - "@types/node": "22.4.2", + "@types/node": "22.5.0", "@types/papaparse": "5.3.14", "@types/showdown": "2.0.6", "@types/smoothscroll-polyfill": "0.3.4", @@ -102,7 +102,7 @@ "@types/uuid": "10.0.0", "@typescript-eslint/eslint-plugin": "8.2.0", "@typescript-eslint/parser": "8.2.0", - "eslint": "9.9.0", + "eslint": "9.9.1", "eslint-config-prettier": "9.1.0", "eslint-plugin-deprecation": "3.0.0", "eslint-plugin-jest": "28.8.0", @@ -121,7 +121,7 @@ "ng-mocks": "14.13.0", "prettier": "3.3.3", "sass": "1.77.8", - "ts-jest": "29.2.4", + "ts-jest": "29.2.5", "typescript": "5.5.4", "weak-napi": "2.0.2" }, @@ -211,13 +211,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1802.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.0.tgz", - "integrity": "sha512-s1atTSL98XLUUxfWEQAnhFaOFIJZDLWjSqec+Sb+f4iZFj+hOFejzJxPVnHMIJudOzn0Lqjk3t987KG/zNEGdg==", + "version": "0.1802.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.1.tgz", + "integrity": "sha512-XTnJfCBMDQl3xF4w/eNrq821gbj2Ig1cqbzpRflhz4pqrANTAfHfPoIC7piWEZ60FNlHapzb6fvh6tJUGXG9og==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.0", + "@angular-devkit/core": "18.2.1", "rxjs": "7.8.1" }, "engines": { @@ -227,17 +227,17 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.0.tgz", - "integrity": "sha512-V0XKT7xt6d6duXqmDAQEQgEJNXuWAekpHEDxafvBT0MTxcEhu0ozQVwI4oAekiKOz+APIcAZyMzvw3Tlzog5cw==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.1.tgz", + "integrity": "sha512-ANsTWKjIlEvJ6s276TbwnDhkoHhQDfsNiRFUDRGBZu94UNR78ImQZSyKYGHJOeQQH6jpBtraA1rvW5WKozAtlw==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.0", - "@angular-devkit/build-webpack": "0.1802.0", - "@angular-devkit/core": "18.2.0", - "@angular/build": "18.2.0", + "@angular-devkit/architect": "0.1802.1", + "@angular-devkit/build-webpack": "0.1802.1", + "@angular-devkit/core": "18.2.1", + "@angular/build": "18.2.1", "@babel/core": "7.25.2", "@babel/generator": "7.25.0", "@babel/helper-annotate-as-pure": "7.24.7", @@ -248,7 +248,7 @@ "@babel/preset-env": "7.25.3", "@babel/runtime": "7.25.0", "@discoveryjs/json-ext": "0.6.1", - "@ngtools/webpack": "18.2.0", + "@ngtools/webpack": "18.2.1", "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.20", @@ -280,7 +280,7 @@ "postcss-loader": "8.1.1", "resolve-url-loader": "5.0.0", "rxjs": "7.8.1", - "sass": "1.77.8", + "sass": "1.77.6", "sass-loader": "16.0.0", "semver": "7.6.3", "source-map-loader": "5.0.0", @@ -355,14 +355,39 @@ } } }, + "node_modules/@angular-devkit/build-angular/node_modules/sass": { + "version": "1.77.6", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", + "integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true, + "license": "0BSD" + }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1802.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.0.tgz", - "integrity": "sha512-bU7AxlI/avnlOLrgE195cokrOA1ayT6JjRv8Hxzh1bIOa1jE87HsyjxvQhOLcdEb97zwHqMqbntcgUNBgsegJQ==", + "version": "0.1802.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.1.tgz", + "integrity": "sha512-xOP9Hxkj/mWYdMTa/8uNxFTv7z+3UiGdt4VAO7vetV5qkU/S9rRq8FEKviCc2llXfwkhInSgeeHpWKdATa+YIQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1802.0", + "@angular-devkit/architect": "0.1802.1", "rxjs": "7.8.1" }, "engines": { @@ -376,9 +401,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.0.tgz", - "integrity": "sha512-8SOopyUKUMqAq2rj32XkTIfr79Y274k4uglxxRtzHYoWwHlLdG0KrA+wCcsh/FU9PyR4dA+5dcDAApn358ZF+Q==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.1.tgz", + "integrity": "sha512-fSuGj6CxiTFR+yjuVcaWqaVb5Wts39CSBYRO1BlsOlbuWFZ2NKC/BAb5bdxpB31heCBJi7e3XbPvcMMJIcnKlA==", "dev": true, "license": "MIT", "dependencies": { @@ -404,13 +429,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.0.tgz", - "integrity": "sha512-WWKwz2RKMVI6T25JFwOSSfRLB+anNzabVIRwf85R/YMIo34BUk777f2JU/7cCKlxSpQu39TqIfMQZAyzAD1z0A==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.1.tgz", + "integrity": "sha512-2t/q0Jcv7yqhAzEdNgsxoGSCmPgD4qfnVOJ7EJw3LNIA+kX1CmtN4FESUS0i49kN4AyNJFAI5O2pV8iJiliKaw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.0", + "@angular-devkit/core": "18.2.1", "jsonc-parser": "3.3.1", "magic-string": "0.30.11", "ora": "5.4.1", @@ -523,9 +548,9 @@ } }, "node_modules/@angular/animations": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.0.tgz", - "integrity": "sha512-BFAfqDDjsa0Q91F4s33pFcBG+ydFDurEmwYGG1gmO7UXZJI6ZbRVdULaZHz75M+Bf3hJkzVB05pesvfbK+Fg/g==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.1.tgz", + "integrity": "sha512-jit452yuE6DMVV09E6RAjgapgw64mMVH31ccpPvMDekzPsTuP3KNKtgRFU/k2DFhYJvyczM1AqqlgccE/JGaRw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -534,18 +559,18 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.0" + "@angular/core": "18.2.1" } }, "node_modules/@angular/build": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.0.tgz", - "integrity": "sha512-LvNJ2VOEVy3N1tGzt+xnKyweRBuUE1NsnuEEWAb9Y+V1cyRgj0s7FX2+IQZZQhP+W/pXfbsKaByOAbJ5KWV85Q==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.1.tgz", + "integrity": "sha512-HwzjB+I31cAtjTTbbS2NbayzfcWthaKaofJlSmZIst3PN+GwLZ8DU0DRpd/xu5AXkk+DoAIWd+lzUIaqngz6ow==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.0", + "@angular-devkit/architect": "0.1802.1", "@babel/core": "7.25.2", "@babel/helper-annotate-as-pure": "7.24.7", "@babel/helper-split-export-declaration": "7.24.7", @@ -565,7 +590,7 @@ "picomatch": "4.0.2", "piscina": "4.6.1", "rollup": "4.20.0", - "sass": "1.77.8", + "sass": "1.77.6", "semver": "7.6.3", "vite": "5.4.0", "watchpack": "2.4.1" @@ -606,10 +631,28 @@ } } }, + "node_modules/@angular/build/node_modules/sass": { + "version": "1.77.6", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", + "integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@angular/cdk": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.0.tgz", - "integrity": "sha512-hjuUWNhxU48WB2i1j4NLwnPTaCnucRElfp7DBX5Io0rY5Lwl3HXafvyi7/A1D0Ah+YsJpktKOWPDGv8r7vxymg==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.1.tgz", + "integrity": "sha512-6y4MmpEPXze6igUHkLsBUPkxw32F8+rmW0xVXZchkSyGlFgqfh53ueXoryWb0qL4s5enkNY6AzXnKAqHfPNkVQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -624,18 +667,18 @@ } }, "node_modules/@angular/cli": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.0.tgz", - "integrity": "sha512-hA60QIA7Dh8LQxM42wqd7WrhwQjbjB8oIRH5Slgbiv8iocAo76scp1/qyZo2SSzjfkB7jxUiao5L4ckiJ/hvZg==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.1.tgz", + "integrity": "sha512-SomUFDHanY4o7k3XBGf1eFt4z1h05IGJHfcbl2vxoc0lY59VN13m/pZsD2AtpqtJTzLQT02XQOUP4rmBbGoQ+Q==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1802.0", - "@angular-devkit/core": "18.2.0", - "@angular-devkit/schematics": "18.2.0", + "@angular-devkit/architect": "0.1802.1", + "@angular-devkit/core": "18.2.1", + "@angular-devkit/schematics": "18.2.1", "@inquirer/prompts": "5.3.8", "@listr2/prompt-adapter-inquirer": "2.0.15", - "@schematics/angular": "18.2.0", + "@schematics/angular": "18.2.1", "@yarnpkg/lockfile": "1.1.0", "ini": "4.1.3", "jsonc-parser": "3.3.1", @@ -658,9 +701,9 @@ } }, "node_modules/@angular/common": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.0.tgz", - "integrity": "sha512-DELx/QYNqqjmiM+kE5PoVmyG4gPw5WB1bDDeg3hEuBCK3eS2KosgQH0/MQo3OSBZHOcAMFjfHMGqKgxndmYixQ==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.1.tgz", + "integrity": "sha512-N0ZJO1/iU9UhprplZRPvBcdRgA/i6l6Ng5gXs5ymHBJ0lxsB+mDVCmC4jISjR9gAWc426xXwLaOpuP5Gv3f/yg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -669,14 +712,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.0", + "@angular/core": "18.2.1", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.0.tgz", - "integrity": "sha512-RmGwQ7jRzotUKKMk0CwxTcIXFr5mRxSbJf9o5S3ujuIOo1lYop8SQZvjq67a5BuoYyD+1pX6iMmxZqlbKoihBQ==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.1.tgz", + "integrity": "sha512-5e9ygKEcsBoV6xpaGKVrtsLxLETlrM0oB7twl4qG/xuKYqCLj8cRQMcAKSqDfTPzWMOAQc7pHdk+uFVo/8dWHA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -685,7 +728,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.0" + "@angular/core": "18.2.1" }, "peerDependenciesMeta": { "@angular/core": { @@ -694,9 +737,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.0.tgz", - "integrity": "sha512-pPBFjMqNTNettrleLtEc6a/ysOZjG3F0Z5lyKYePcyNQmn2rpa9XmD2y6PhmzTmIhxeXrogWA84Xgg/vK5wBNw==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.1.tgz", + "integrity": "sha512-D+Qba0r6RfHfffzrebGYp54h05AxpkagLjit/GczKNgWSP1gIgZxSfi88D+GvFmeWvZxWN1ecAQ+yqft9hJqWg==", "license": "MIT", "dependencies": { "@babel/core": "7.25.2", @@ -717,14 +760,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.2.0", + "@angular/compiler": "18.2.1", "typescript": ">=5.4 <5.6" } }, "node_modules/@angular/core": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.0.tgz", - "integrity": "sha512-7+4wXfeAk1TnG3MGho2gpBI5XHxeSRWzLK2rC5qyyRbmMV+GrIgf1HqFjQ4S02rydkQvGpjqQHtO1PYJnyn4bg==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.1.tgz", + "integrity": "sha512-9KrSpJ65UlJZNXrE18NszcfOwb5LZgG+LYi5Doe7amt218R1bzb3trvuAm0ZzMaoKh4ugtUCkzEOd4FALPEX6w==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -738,9 +781,9 @@ } }, "node_modules/@angular/forms": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.0.tgz", - "integrity": "sha512-G+4BjNCUo4cRwg9NwisGLbtG/1AbIJNOO3RUejPJJbCcAkYMSzmDWSQ+LQEGW4vC/1xaDKO8AT71DI/I09bOxA==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.1.tgz", + "integrity": "sha512-T7z8KUuj2PoPxrMrAruQVJha+x4a9Y6IrKYtArgOQQlTwCEJuqpVYuOk5l3fwWpHE9bVEjvgkAMI1D5YXA/U6w==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -749,16 +792,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.0", - "@angular/core": "18.2.0", - "@angular/platform-browser": "18.2.0", + "@angular/common": "18.2.1", + "@angular/core": "18.2.1", + "@angular/platform-browser": "18.2.1", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/language-service": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-18.2.0.tgz", - "integrity": "sha512-brl5061YqfNnT7yZNMWmsgv6ve6p9+kfhX6mZWOGICcY2SYVtCNVHdqzwWTTwY7MvTVfycHxiAf9PEmc5lD4/g==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-18.2.1.tgz", + "integrity": "sha512-JI4oox9ELNdDVg0uJqCwgyFoK4XrowV14wSoNpGhpTLModRg3eDS6q+8cKn27cjTQRZvpReyYSTfiZMB8j4eqQ==", "dev": true, "license": "MIT", "engines": { @@ -766,9 +809,9 @@ } }, "node_modules/@angular/localize": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.2.0.tgz", - "integrity": "sha512-ul8yGmimiHkhUU87isDCst0790jTBHP1zPBMI2q7QHv7iDzSN5brV8zUMcRypxhh4mQOCnq2LtP84o5ybn4Sig==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.2.1.tgz", + "integrity": "sha512-nNdB6ehXCSBpQ75sTh6Gcwy2rgExfZEkGcPARJLpjqQlHO+Mk3b1y3ka6XT9M2qQYUeyukncTFUMEZWwHICsOA==", "license": "MIT", "dependencies": { "@babel/core": "7.25.2", @@ -785,21 +828,21 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.2.0", - "@angular/compiler-cli": "18.2.0" + "@angular/compiler": "18.2.1", + "@angular/compiler-cli": "18.2.1" } }, "node_modules/@angular/material": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.0.tgz", - "integrity": "sha512-lOXk8pAVP4Mr0/Q6YrNnVYQVTnR8aEG5D9QSEWjs+607gONuh/9n7ERBdzX7xO9D0vYyXq+Vil32zcF41/Q8Cg==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.1.tgz", + "integrity": "sha512-DBSJGqLttT9vYpLGWTuuRoOKd1mNelS0jnNo7jNZyMpjcGfuhNzmPtYiBkXfNsAl7YoXoUmX8+4uh1JZspQGqA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/animations": "^18.0.0 || ^19.0.0", - "@angular/cdk": "18.2.0", + "@angular/cdk": "18.2.1", "@angular/common": "^18.0.0 || ^19.0.0", "@angular/core": "^18.0.0 || ^19.0.0", "@angular/forms": "^18.0.0 || ^19.0.0", @@ -808,9 +851,9 @@ } }, "node_modules/@angular/platform-browser": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.0.tgz", - "integrity": "sha512-yhj281TuPz5a8CehwucwIVl29Oqte9KS4X/VQkMV++GpYQE2KKKcoff4FXSdF5RUcUYkK2li4IvawIqPmUSagg==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.1.tgz", + "integrity": "sha512-hQABX7QotGmCIR3EhCBCDh5ZTvQao+JkuK5CCw2G1PkRfJMBwEpjNqnyhz41hZhWiGlucp9jgbeypppW+mIQEw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -819,9 +862,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "18.2.0", - "@angular/common": "18.2.0", - "@angular/core": "18.2.0" + "@angular/animations": "18.2.1", + "@angular/common": "18.2.1", + "@angular/core": "18.2.1" }, "peerDependenciesMeta": { "@angular/animations": { @@ -830,9 +873,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.0.tgz", - "integrity": "sha512-izfaXKNC/kqOEzJG8eTnFu39VLI3vv3dTKoYOdEKRB7MTI0t0x66oZmABnHcihtkTSvXs/is+7lA5HmH2ZuIEQ==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.1.tgz", + "integrity": "sha512-tYJHtshbaKrtnRA15k3vrveSVBqkVUGhINvGugFA2vMtdTOfhfPw+hhzYrcwJibgU49rHogCfI9mkIbpNRYntA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -841,16 +884,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.0", - "@angular/compiler": "18.2.0", - "@angular/core": "18.2.0", - "@angular/platform-browser": "18.2.0" + "@angular/common": "18.2.1", + "@angular/compiler": "18.2.1", + "@angular/core": "18.2.1", + "@angular/platform-browser": "18.2.1" } }, "node_modules/@angular/router": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.0.tgz", - "integrity": "sha512-6/462hvC3HSwbps8VItECHpkdkiFqRmTKdn1Trik+FjnvdupYrKB6kBsveM3eP+gZD4zyMBMKzBWB9N/xA1clw==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.1.tgz", + "integrity": "sha512-gVyqW6fYnG7oq1DlZSXJMQ2Py2dJQB7g6XVtRcYB1gR4aeowx5N9ws7PjqAi0ih91ASq2MmP4OlSSWLq+eaMGg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -859,16 +902,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.0", - "@angular/core": "18.2.0", - "@angular/platform-browser": "18.2.0", + "@angular/common": "18.2.1", + "@angular/core": "18.2.1", + "@angular/platform-browser": "18.2.1", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/service-worker": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-18.2.0.tgz", - "integrity": "sha512-ngcALrgqMuAeIo5dgou6eBzdtgLvmVg5zwmZuTyrnNPZENEaKTj7u5pm9++gl62797sUWlMbL+fa/BOhntGs5A==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-18.2.1.tgz", + "integrity": "sha512-Is4arGy+4HjyvALmR/GsWI4SwXYVJ1IkauAgxPsQKvWLNHdX7a/CEgEEVQGXq96H46QX9O2OcW69PnPatmJIXg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -880,8 +923,8 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.0", - "@angular/core": "18.2.0" + "@angular/common": "18.2.1", + "@angular/core": "18.2.1" } }, "node_modules/@babel/code-frame": { @@ -898,9 +941,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", - "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", + "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1000,9 +1043,9 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.0.tgz", - "integrity": "sha512-GYM6BxeQsETc9mnct+nIIpf63SAyzvyYN7UB/IlTyd+MBg06afFGp0mIeUqGyWgS2mxad6vqbMrHVlaL3m70sQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.4.tgz", + "integrity": "sha512-ro/bFs3/84MDgDmMwbcHgDa8/E6J3QKNTk4xJJnVeFtGE+tL0K26E3pNxhYz2b67fJpt7Aphw5XcploKXuCvCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1011,7 +1054,7 @@ "@babel/helper-optimise-call-expression": "^7.24.7", "@babel/helper-replace-supers": "^7.25.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/traverse": "^7.25.0", + "@babel/traverse": "^7.25.4", "semver": "^6.3.1" }, "engines": { @@ -1270,12 +1313,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", - "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.4.tgz", + "integrity": "sha512-nq+eWrOgdtu3jG5Os4TQP3x3cLA8hR8TvJNjD8vnPa20WGycimcparWnLK4jJhElTK6SDyuJo1weMKO/5LpmLA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.25.2" + "@babel/types": "^7.25.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -1646,13 +1689,13 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", - "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.4.tgz", + "integrity": "sha512-uMOCoHVU52BsSWxPOMVv5qKRdeSlPuImUCB2dlPuBSU+W2/ROE7/Zg8F2Kepbk+8yBa68LlRKxO+xgEVWorsDg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1764,14 +1807,14 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", - "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.4.tgz", + "integrity": "sha512-nZeZHyCWPfjkdU5pA/uHiTaDAFUEqkpzf1YoQT2NeSynCGYq9rxfyI3XpQbfx/a0hSnFH6TGlEXvae5Vi7GD8g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-class-features-plugin": "^7.25.4", + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1799,17 +1842,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.0.tgz", - "integrity": "sha512-xyi6qjr/fYU304fiRwFbekzkqVJZ6A7hOjWZd+89FVcBqPV3S9Wuozz82xdpLspckeaafntbzglaW4pqpzvtSw==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.4.tgz", + "integrity": "sha512-oexUfaQle2pF/b6E0dwsxQtAol9TLSO88kQvym6HHBWFliV2lGdrPieX+WgMRLSJDVzdYywk7jXbLPuO2KLTLg==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-compilation-targets": "^7.25.2", "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-replace-supers": "^7.25.0", - "@babel/traverse": "^7.25.0", + "@babel/traverse": "^7.25.4", "globals": "^11.1.0" }, "engines": { @@ -2280,14 +2323,14 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz", - "integrity": "sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.4.tgz", + "integrity": "sha512-ao8BG7E2b/URaUQGqN3Tlsg+M3KlHY6rJ1O1gXAEUnZoyNQnvKyH87Kfg+FoxSeyWUB8ISZZsC91C44ZuBFytw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-class-features-plugin": "^7.25.4", + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -2517,14 +2560,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz", - "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.4.tgz", + "integrity": "sha512-qesBxiWkgN1Q+31xUE9RcMk79eOXXDCv6tfyGMRSs4RGlioSg2WVyQAm07k726cSE56pa+Kb0y9epX2qaXzTvA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.2", + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -2680,16 +2723,16 @@ } }, "node_modules/@babel/traverse": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", - "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.4.tgz", + "integrity": "sha512-VJ4XsrD+nOvlXyLzmLzUs/0qjFS4sK30te5yEFlvbbUNEgKaVb2BHZUpAL+ttLPQAHNrsI3zZisbfha5Cvr8vg==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.3", + "@babel/generator": "^7.25.4", + "@babel/parser": "^7.25.4", "@babel/template": "^7.25.0", - "@babel/types": "^7.25.2", + "@babel/types": "^7.25.4", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2697,10 +2740,25 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.25.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.5.tgz", + "integrity": "sha512-abd43wyLfbWoxC6ahM8xTkqLpGB2iWBVyuKC9/srhFunCd1SDNrV1s72bBpK4hLj8KLzHBBcOblvLQZBNw9r3w==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.4", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", - "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.4.tgz", + "integrity": "sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.24.8", @@ -3240,9 +3298,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz", - "integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3364,9 +3422,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.0.tgz", - "integrity": "sha512-hhetes6ZHP3BlXLxmd8K2SNgkhNSi+UcecbnwWKwpP7kyi/uC75DJ1lOOBO3xrC4jyojtGE3YxKZPHfk4yrgug==", + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz", + "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==", "dev": true, "license": "MIT", "engines": { @@ -5006,9 +5064,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.0.tgz", - "integrity": "sha512-a6hbkYzh/KUlI52huiU4vztqIuxzyddg6kJGcelUJx3Ju6MJeziu+XmJ6wqFRvfH89zmJeaSADKsGFQaBHtJLg==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.1.tgz", + "integrity": "sha512-v86U3jOoy5R9ZWe9Q0LbHRx/IBw1lbn0ldBU+gIIepREyVvb9CcH/vAyIb2Fw1zaYvvfG1OyzdrHyW8iGXjdnQ==", "dev": true, "license": "MIT", "engines": { @@ -5653,14 +5711,14 @@ ] }, "node_modules/@schematics/angular": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.0.tgz", - "integrity": "sha512-XePvx2ZnxCcAQw5lHVMUrJvm8MXqAWGcMyJDAuQUqNZrPCk3GpCaplWx2n+nPkinYVX2Q2v/DqtvWStQwgU4nA==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.1.tgz", + "integrity": "sha512-bBV7I+MCbdQmBPUFF4ECg37VReM0+AdQsxgwkjBBSYExmkErkDoDgKquwL/tH7stDCc5IfTd0g9BMeosRgDMug==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.0", - "@angular-devkit/schematics": "18.2.0", + "@angular-devkit/core": "18.2.1", + "@angular-devkit/schematics": "18.2.1", "jsonc-parser": "3.3.1" }, "engines": { @@ -6398,9 +6456,9 @@ } }, "node_modules/@types/node": { - "version": "22.4.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.2.tgz", - "integrity": "sha512-nAvM3Ey230/XzxtyDcJ+VjvlzpzoHwLsF7JaDRfoI0ytO0mVheerNmM45CtA0yOILXwXXxOrcUWH3wltX+7PSw==", + "version": "22.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz", + "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==", "dev": true, "license": "MIT", "dependencies": { @@ -6448,9 +6506,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.3", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", - "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "version": "18.3.4", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz", + "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==", "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -7019,9 +7077,9 @@ } }, "node_modules/ace-builds": { - "version": "1.35.5", - "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.35.5.tgz", - "integrity": "sha512-yh3V5BLHlN6gwbmk5sV00WRRvdEggJGJ3AIHhOOGHlgDWNWCSvOnHPO7Chb+AqaxxHuvpxOdXd7ZQesaiuJQZQ==", + "version": "1.36.0", + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.36.0.tgz", + "integrity": "sha512-7to4F86V5N13EY4M9LWaGo2Wmr9iWe5CrYpc28F+/OyYCf7yd+xBV5x9v/GB73EBGGoYd89m6JjeIUjkL6Yw+w==", "license": "BSD-3-Clause" }, "node_modules/acorn": { @@ -7301,9 +7359,9 @@ } }, "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "dev": true, "license": "MIT" }, @@ -8654,9 +8712,9 @@ } }, "node_modules/core-js-compat": { - "version": "3.38.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.0.tgz", - "integrity": "sha512-75LAicdLa4OJVwFxFbQR3NdnZjNgX6ILpVcVzcC4T2smerB5lELMrJQQQoWV6TiuC/vlaFqgU2tKQx9w5s0e0A==", + "version": "3.38.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", + "integrity": "sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==", "dev": true, "license": "MIT", "dependencies": { @@ -9714,9 +9772,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.8", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.8.tgz", - "integrity": "sha512-4Nx0gP2tPNBLTrFxBMHpkQbtn2hidPVr/+/FTtcCiBYTucqc70zRyVZiOLj17Ui3wTO7SQ1/N+hkHYzJjBzt6A==", + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz", + "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==", "license": "ISC" }, "node_modules/emittery": { @@ -9974,17 +10032,17 @@ } }, "node_modules/eslint": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.0.tgz", - "integrity": "sha512-JfiKJrbx0506OEerjK2Y1QlldtBxkAlLxT5OEcRF8uaQ86noDe2k31Vw9rnSWv+MXZHj7OOUV/dA0AhdLFcyvA==", + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz", + "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", - "@eslint/config-array": "^0.17.1", + "@eslint/config-array": "^0.18.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.9.0", + "@eslint/js": "9.9.1", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", @@ -12115,9 +12173,9 @@ } }, "node_modules/is-core-module": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", - "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "license": "MIT", "dependencies": { @@ -15567,9 +15625,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -15898,9 +15956,9 @@ "license": "MIT" }, "node_modules/monaco-editor": { - "version": "0.50.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.50.0.tgz", - "integrity": "sha512-8CclLCmrRRh+sul7C08BmPBP3P8wVWfBHomsTcndxg5NRCEPfu/mc2AGU8k37ajjDVXcXFc12ORAMUkmk+lkFA==", + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.51.0.tgz", + "integrity": "sha512-xaGwVV1fq343cM7aOYB6lVE4Ugf0UyimdD/x5PWcWBMKENwectaEu77FAN7c5sFiyumqeJdX1RPTh1ocioyDjw==", "license": "MIT" }, "node_modules/moo-color": { @@ -17212,9 +17270,9 @@ } }, "node_modules/postcss": { - "version": "8.4.40", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", - "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", "dev": true, "funding": [ { @@ -18764,12 +18822,12 @@ } }, "node_modules/simple-statistics": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/simple-statistics/-/simple-statistics-7.8.3.tgz", - "integrity": "sha512-JFvMY00t6SBGtwMuJ+nqgsx9ylkMiJ5JlK9bkj8AdvniIe5615wWQYkKHXe84XtSuc40G/tlrPu0A5/NlJvv8A==", + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/simple-statistics/-/simple-statistics-7.8.4.tgz", + "integrity": "sha512-KHC7X+4Dji2rFgnPU7FxPPp4GxPz9hvQCHx2x6JbjLYNKuSMHcoNZ54gF0xBBMOAvNtWmfCHcfC4MD2T89ffEA==", "license": "ISC", "engines": { - "node": "*" + "node": ">= 18" } }, "node_modules/sisteransi": { @@ -19012,9 +19070,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.18", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", - "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", "dev": true, "license": "CC0-1.0" }, @@ -19833,21 +19891,21 @@ } }, "node_modules/ts-jest": { - "version": "29.2.4", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.4.tgz", - "integrity": "sha512-3d6tgDyhCI29HlpwIq87sNuI+3Q6GLTTCeYRHCs7vDz+/3GCMwEtV9jezLyl4ZtnBgx00I7hm8PCP8cTksMGrw==", + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", "dev": true, "license": "MIT", "dependencies": { - "bs-logger": "0.x", + "bs-logger": "^0.2.6", "ejs": "^3.1.10", - "fast-json-stable-stringify": "2.x", + "fast-json-stable-stringify": "^2.1.0", "jest-util": "^29.0.0", "json5": "^2.2.3", - "lodash.memoize": "4.x", - "make-error": "1.x", - "semver": "^7.5.3", - "yargs-parser": "^21.0.1" + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" }, "bin": { "ts-jest": "cli.js" @@ -19951,9 +20009,9 @@ } }, "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "license": "0BSD" }, "node_modules/tsutils": { @@ -21022,9 +21080,9 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.3.0.tgz", - "integrity": "sha512-xD2qnNew+F6KwOGZR7kWdbIou/ud7cVqLEXeK1q0nHcNsX/u7ul/fSdlOTX4ntSL5FNFy7ZJJXbf0piF591JYw==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 1eda30265f5f..da3d4a60a893 100644 --- a/package.json +++ b/package.json @@ -13,18 +13,18 @@ "node_modules" ], "dependencies": { - "@angular/animations": "18.2.0", - "@angular/cdk": "18.2.0", - "@angular/common": "18.2.0", - "@angular/compiler": "18.2.0", - "@angular/core": "18.2.0", - "@angular/forms": "18.2.0", - "@angular/localize": "18.2.0", - "@angular/material": "18.2.0", - "@angular/platform-browser": "18.2.0", - "@angular/platform-browser-dynamic": "18.2.0", - "@angular/router": "18.2.0", - "@angular/service-worker": "18.2.0", + "@angular/animations": "18.2.1", + "@angular/cdk": "18.2.1", + "@angular/common": "18.2.1", + "@angular/compiler": "18.2.1", + "@angular/core": "18.2.1", + "@angular/forms": "18.2.1", + "@angular/localize": "18.2.1", + "@angular/material": "18.2.1", + "@angular/platform-browser": "18.2.1", + "@angular/platform-browser-dynamic": "18.2.1", + "@angular/router": "18.2.1", + "@angular/service-worker": "18.2.1", "@ctrl/ngx-emoji-mart": "9.2.0", "@danielmoncada/angular-datetime-picker": "18.1.0", "@fingerprintjs/fingerprintjs": "4.4.3", @@ -41,7 +41,7 @@ "@swimlane/ngx-charts": "20.5.0", "@swimlane/ngx-graph": "8.4.0", "@vscode/codicons": "0.0.36", - "ace-builds": "1.35.5", + "ace-builds": "1.36.0", "bootstrap": "5.3.3", "brace": "0.11.1", "compare-versions": "6.1.1", @@ -60,7 +60,7 @@ "jszip": "3.10.1", "lodash-es": "4.17.21", "mobile-drag-drop": "3.0.0-rc.0", - "monaco-editor": "0.50.0", + "monaco-editor": "0.51.0", "ngx-infinite-scroll": "18.0.0", "ngx-webstorage": "18.0.0", "papaparse": "5.4.1", @@ -69,12 +69,12 @@ "showdown": "2.1.0", "showdown-highlight": "3.1.0", "showdown-katex": "0.6.0", - "simple-statistics": "7.8.3", + "simple-statistics": "7.8.4", "smoothscroll-polyfill": "0.4.4", "sockjs-client": "1.6.1", "split.js": "1.6.5", "ts-cacheable": "1.0.10", - "tslib": "2.6.3", + "tslib": "2.7.0", "uuid": "10.0.0", "webstomp-client": "1.2.6", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", @@ -91,7 +91,6 @@ "@typescript-eslint/utils": { "eslint": "^9.9.0" }, - "axios": "1.7.4", "braces": "3.0.3", "critters": "0.0.24", "debug": "4.3.6", @@ -103,36 +102,35 @@ }, "jsdom": "24.1.1", "katex": "0.16.11", - "postcss": "8.4.40", + "postcss": "8.4.41", "semver": "7.6.3", "showdown-katex": { "showdown": "2.1.0" }, "tough-cookie": "4.1.4", - "undici": "6.19.5", - "webpack-dev-middleware": "7.3.0", + "webpack-dev-middleware": "7.4.2", "word-wrap": "1.2.5", "ws": "8.18.0", "yargs-parser": "21.1.1" }, "devDependencies": { "@angular-builders/jest": "18.0.0", - "@angular-devkit/build-angular": "18.2.0", + "@angular-devkit/build-angular": "18.2.1", "@angular-eslint/builder": "18.3.0", "@angular-eslint/eslint-plugin": "18.3.0", "@angular-eslint/eslint-plugin-template": "18.3.0", "@angular-eslint/schematics": "18.3.0", "@angular-eslint/template-parser": "18.3.0", - "@angular/cli": "18.2.0", - "@angular/compiler-cli": "18.2.0", - "@angular/language-service": "18.2.0", + "@angular/cli": "18.2.1", + "@angular/compiler-cli": "18.2.1", + "@angular/language-service": "18.2.1", "@sentry/types": "8.26.0", "@types/crypto-js": "4.2.2", "@types/d3-shape": "3.1.6", "@types/dompurify": "3.0.5", "@types/jest": "29.5.12", "@types/lodash-es": "4.17.12", - "@types/node": "22.4.2", + "@types/node": "22.5.0", "@types/papaparse": "5.3.14", "@types/showdown": "2.0.6", "@types/smoothscroll-polyfill": "0.3.4", @@ -140,7 +138,7 @@ "@types/uuid": "10.0.0", "@typescript-eslint/eslint-plugin": "8.2.0", "@typescript-eslint/parser": "8.2.0", - "eslint": "9.9.0", + "eslint": "9.9.1", "eslint-config-prettier": "9.1.0", "eslint-plugin-deprecation": "3.0.0", "eslint-plugin-jest": "28.8.0", @@ -159,7 +157,7 @@ "ng-mocks": "14.13.0", "prettier": "3.3.3", "sass": "1.77.8", - "ts-jest": "29.2.4", + "ts-jest": "29.2.5", "typescript": "5.5.4", "weak-napi": "2.0.2" }, From 19c6b58041365a39dcd8be5ed8175d3b211d3379 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Sun, 25 Aug 2024 22:57:47 +0300 Subject: [PATCH 04/15] Development: Improve contributing guidelines --- CONTRIBUTING.md | 41 +++++++++++++++++++- README.md | 99 ++++++++++++++++++++++++++----------------------- 2 files changed, 92 insertions(+), 48 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ea3d5c107479..482bdb00df6c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,43 @@ -# Contributing Guide for Artemis +# Contribution Guidelines for Artemis Read the [setup guide](https://docs.artemis.cit.tum.de/dev/setup.html) on how to set up your local development environment. -Before creating a pull request, please read the [guidelines to the development process](https://docs.artemis.cit.tum.de/dev/development-process/development-process.html) as well as the [coding and design guidelines](https://docs.artemis.cit.tum.de/dev/guidelines.html). +## Identity and Transparency + +To ensure a transparent and trustworthy environment, we have established different guidelines for members of our organization and external contributors. + +### For Members of Our Organization + +1. **Real Names Required**: As a member of our organization, you must use your full real name in your GitHub profile. This is a prerequisite for joining our organization. Using a real name is crucial for building trust within the team and the broader community. It fosters accountability and transparency, which are essential for collaborative work. When members use their real identities, it encourages open communication and strengthens professional relationships. Furthermore, it aligns with best practices in open-source communities, where transparency is key to ensuring the integrity and reliability of contributions. + +2. **Profile Picture**: Members are required to upload an authentic profile picture. Use a clear, professional image and avoid comic-like pictures, memojis, or other non-authentic picture styles. Using a professional and authentic profile picture is essential for establishing credibility and fostering trust within the community. It helps others easily identify and connect with you, which is crucial for effective collaboration. By using a real photo, you present yourself as a serious and committed contributor, which in turn encourages others to take your work and interactions seriously. Avoiding non-authentic images ensures that the focus remains on the substance of your contributions rather than on distractions or misunderstandings that might arise from informal or unprofessional visuals. + +3. **Direct Branching and PR Creation**: As a member, you are encourages to create branches and pull requests (PRs) directly within the repository. Please follow the internal branching and code review processes outlined in [guidelines to the development process](https://docs.artemis.cit.tum.de/dev/development-process/development-process.html) and [coding and design guidelines](https://docs.artemis.cit.tum.de/dev/guidelines.html). + +### For External Contributors + +1. **Identity Verification**: External contributions will only be considered if the contributor uses their real name and an authentic profile picture (see above). This ensures accountability and trustworthiness in all external contributions. + +2. **Forking the Repository**: External contributors fork the repository and work on changes in their own branches. + +3. **Submit a Pull Request**: Once your work is complete, submit a pull request for review. Ensure that your branch is up to date with the main branch before submitting. + +4. **Compliance**: Contributions from external contributors that do not adhere to these guidelines may not be accepted. + +### References and Best Practices + +- We align our guidelines with the [GitHub Acceptable Use Policies](https://docs.github.com/en/site-policy/acceptable-use-policies) which stress the importance of authenticity and transparency in user profiles. +- For more insights on contributing to open-source projects, we recommend reviewing the [Open Source Guides by GitHub](https://opensource.guide/). + +By following these guidelines, we foster a collaborative environment built on mutual trust and respect, essential for the success of our project. + +## Contribution Process + +1. **External contributors only**: Fork the Repository and create a branch. +2. **Create a feature branch**: Work on your changes in a separate branch. +3. **Submit a pull request**: Once your work is complete, submit a pull request for review. + +Thank you for your contributions and for helping us maintain a high standard of quality and trust in this project. + + diff --git a/README.md b/README.md index 30ec2419c16d..092d74dcdbb0 100644 --- a/README.md +++ b/README.md @@ -53,22 +53,68 @@ Artemis brings interactive learning to life with instant, individual feedback on ## Roadmap -The Artemis development team prioritizes the following issues in the future. We welcome feature requests from students, tutors, instructors, and administrators. We are happy to discuss any suggestions for improvements. +The Artemis development team prioritizes the following areas in the future. We welcome feature requests from students, tutors, instructors, and administrators. We are happy to discuss any suggestions for improvements. * **Short term**: Further improve the communication features with mobile apps for iOS and Android -* **Short term**: Improve the REST API of the server application +* **Short term**: Add the possibility to use Iris for questions on all exercise types and lectures (partly done) +* **Short term**: Provide GenAI based automatic feedback to modeling, text and programming exercise with Athena +* **Short term**: Improve the LTI integration with Moodle +* **Medium term**: Improve the REST API of the server application +* **Medium term**: Integrate an online IDE (e.g. Eclipse Theia) into Artemis for enhanced user experience * **Medium term**: Add more learning analytics features while preserving data privacy * **Medium term**: Improve the user experience, usability and navigation * **Medium term**: Add automatic generation of hints for programming exercises * **Medium term**: Add GenAI support for reviewing exercises for instructors -* **Medium term**: Add GenAI support for learning analytics -* **Medium term**: Add the possibility to use Iris for questions on all exercise types and lectures +* **Medium term**: Add GenAI support for learning analytics (partly done) * **Long term**: Explore the possibilities of microservices, Kubernetes based deployment, and micro frontends -* **Long term**: Integrated on online IDE (e.g. Eclipse Theia) into Artemis for enhanced user experience * **Long term**: Allow students to take notes on lecture slides and support the automatic updates of lecture slides * **Long term**: Develop an exchange platform for exercises -## Setup, guides, and contributing +## Contributing to This Project + +We welcome contributions from both members of our organization and external contributors. To maintain transparency and trust: + +- **Members**: Must use their full real names and upload a professional and authentic profile picture. Members can directly create branches and PRs in the repository. +- **External Contributors**: Must adhere to our identity guidelines, using real names and authentic profile pictures. Contributions will only be considered if these guidelines are followed. + +We adhere to best practices as recommended by [GitHub's Open Source Guides](https://opensource.guide/) and their [Acceptable Use Policies](https://docs.github.com/en/site-policy/acceptable-use-policies). Thank you for helping us create a respectful and professional environment for everyone involved. + +We follow a pull request contribution model. For detailed guidelines, please refer to our [CONTRIBUTING.md](./CONTRIBUTING.md). Once your pull request is ready to merge, notify the responsible feature maintainer on Slack: + +#### Maintainers + +The following members of the project management team are responsible for specific feature areas in Artemis. Contact them if you have questions or if you want to develop new features in this area. + +| Feature / Aspect | Responsible maintainer | +|--------------------------------|------------------------------------------------------------------------------------| +| Programming exercises | Stephan Krusche ([@krusche](https://github.com/krusche)) | +| Integrated code lifecycle | Stephan Krusche ([@krusche](https://github.com/krusche)) | +| Quiz exercises | Felix Dietrich ([@FelixTJDietrich](https://github.com/FelixTJDietrich)) | +| Modeling exercises (+ Apollon) | Stephan Krusche ([@krusche](https://github.com/krusche)) | +| Text exercises | Maximilian Sölch ([@maximiliansoelch](https://github.com/maximiliansoelch)) | +| File upload exercises | Maximilian Sölch ([@maximiliansoelch](https://github.com/maximiliansoelch)) | +| Exam mode | Stephan Krusche ([@krusche](https://github.com/krusche)) | +| Grading | Maximilian Sölch ([@maximiliansoelch](https://github.com/maximiliansoelch)) | +| Assessment | Maximilian Sölch ([@maximiliansoelch](https://github.com/maximiliansoelch)) | +| Communication | Ramona Beinstingel ([@rabeatwork](https://github.com/rabeatwork)) | +| Notifications | Ramona Beinstingel ([@rabeatwork](https://github.com/rabeatwork)) | +| Team Exercises | Stephan Krusche ([@krusche](https://github.com/krusche)) | +| Lectures | Maximilian Anzinger ([@maximiliananzinger](https://github.com/maximiliananzinger)) | +| Integrated Markdown Editor | Patrick Bassner ([@bassner](https://github.com/bassner)) | +| Plagiarism checks | Markus Paulsen ([@MarkusPaulsen](https://github.com/MarkusPaulsen)) | +| Learning analytics | Maximilian Anzinger ([@maximiliananzinger](https://github.com/maximiliananzinger)) | +| Adaptive learning | Maximilian Anzinger ([@maximiliananzinger](https://github.com/maximiliananzinger)) | +| Learning paths | Maximilian Anzinger ([@maximiliananzinger](https://github.com/maximiliananzinger)) | +| Tutorial Groups | Felix Dietrich ([@FelixTJDietrich](https://github.com/FelixTJDietrich)) | +| Iris | Patrick Bassner ([@bassner](https://github.com/bassner)) | +| Scalability | Matthias Linhuber ([@mtze](https://github.com/mtze)) | +| Usability | Ramona Beinstingel ([@rabeatwork](https://github.com/rabeatwork)) | +| Performance | Ramona Beinstingel ([@rabeatwork](https://github.com/rabeatwork)) | +| Infrastructure | Matthias Linhuber ([@mtze](https://github.com/mtze)) | +| Development process | Felix Dietrich ([@FelixTJDietrich](https://github.com/FelixTJDietrich)) | +| Mobile apps (iOS + Android) | Maximilian Sölch ([@maximiliansoelch](https://github.com/maximiliansoelch)) | + +## Setup and guidelines ### Development setup, coding, and design guidelines @@ -76,6 +122,7 @@ The Artemis development team prioritizes the following issues in the future. We * [Server coding and design guidelines](https://docs.artemis.cit.tum.de/dev/guidelines/server/) * [Client coding and design guidelines](https://docs.artemis.cit.tum.de/dev/guidelines/client/) * [Code Review Guidelines](https://docs.artemis.cit.tum.de/dev/development-process/#review) +* [Performance Guidelines](https://docs.artemis.cit.tum.de/dev/guidelines/performance/) ### Documentation @@ -96,46 +143,6 @@ Artemis uses these external tools for user management and the configuration of p If needed, you can configure self service [user registration](https://docs.artemis.cit.tum.de/admin/registration). -### Contributing - -Please read the guide on [how to contribute](CONTRIBUTING.md) to Artemis. - -Once your PR is ready to merge, notify the responsible feature maintainer on Slack: - -#### Maintainers - -The following members of the project management team are responsible for specific feature areas in Artemis. Contact them if you have questions or if you want to develop new features in this area. - -| Feature / Aspect | Maintainer | -|--------------------------------|-----------------------------------------------------------------------------------------------------| -| Programming exercises | [@krusche](https://github.com/krusche) | -| Integrated code lifecycle | [@krusche](https://github.com/krusche) | -| Quiz exercises | [@FelixTJDietrich](https://github.com/FelixTJDietrich) | -| Modeling exercises (+ Apollon) | [@krusche](https://github.com/krusche) | -| Text exercises | [@maximiliansoelch](https://github.com/maximiliansoelch) | -| File upload exercises | [@maximiliansoelch](https://github.com/maximiliansoelch) | -| Exam mode | [@krusche](https://github.com/krusche) | -| Grading | [@maximiliansoelch](https://github.com/maximiliansoelch) | -| Assessment | [@maximiliansoelch](https://github.com/maximiliansoelch) | -| Communication | [@rabeatwork](https://github.com/rabeatwork) | -| Notifications | [@rabeatwork](https://github.com/rabeatwork) | -| Team Exercises | [@krusche](https://github.com/krusche) | -| Lectures | [@maximiliananzinger](https://github.com/maximiliananzinger) | -| Integrated Markdown Editor | [@maximiliansoelch](https://github.com/maximiliansoelch) [@bassner](https://github.com/bassner) | -| Plagiarism checks | [@MarkusPaulsen](https://github.com/MarkusPaulsen) | -| Learning analytics | [@bassner](https://github.com/bassner) | -| Adaptive learning | [@bassner](https://github.com/bassner) [@maximiliananzinger](https://github.com/maximiliananzinger) | -| Learning paths | [@maximiliananzinger](https://github.com/maximiliananzinger) | -| Tutorial Groups | [@FelixTJDietrich](https://github.com/FelixTJDietrich) | -| Iris | [@bassner](https://github.com/bassner) | -| Scalability | [@mtze](https://github.com/mtze) | -| Usability | [@rabeatwork](https://github.com/rabeatwork) | -| Performance | [@rabeatwork](https://github.com/rabeatwork) | -| Infrastructure | [@mtze](https://github.com/mtze) | -| Development process | [@FelixTJDietrich](https://github.com/FelixTJDietrich) | -| Mobile apps (iOS + Android) | [@krusche](https://github.com/krusche) [@maximiliansoelch](https://github.com/maximiliansoelch) | - - ### Building for production To build and optimize the Artemis application for production, run: From 8411fae090ee0c5a1be78672fcc85b6161a9a6cf Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Sun, 25 Aug 2024 23:02:39 +0300 Subject: [PATCH 05/15] Development: Update code of conduct --- CODE_OF_CONDUCT.md | 155 +++++++++++++++++++++++++++++++-------------- 1 file changed, 106 insertions(+), 49 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 89ee014c27e2..d4887bcca43e 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,76 +1,133 @@ -# Contributor Covenant Code of Conduct + +# Artemis Code of Conduct ## Our Pledge -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to creating a positive environment -include: +Examples of behavior that contributes to a positive environment for our +community include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission +* Publishing others' private information, such as a physical or email address, + without their explicit permission * Other conduct which could reasonably be considered inappropriate in a - professional setting + professional setting -## Our Responsibilities +## Enforcement Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. ## Scope -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project lead Stephan Krusche at krusche@tum.de. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. +reported to the community leaders responsible for enforcement at +[INSERT CONTACT METHOD]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. -[homepage]: https://www.contributor-covenant.org +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations From 215b72f747e78c6d1ee2de1b8b52f4925ff76f89 Mon Sep 17 00:00:00 2001 From: Asli Aykan <56061820+asliayk@users.noreply.github.com> Date: Mon, 26 Aug 2024 11:18:06 +0200 Subject: [PATCH 06/15] Development: Replace the old translation feature in the client with the new desired approach (#8803) --- .../lti-configuration.component.html | 14 +- ...ification-management-update.component.html | 2 +- ...tem-notification-management.component.html | 12 +- ...pcoming-exams-and-exercises.component.html | 18 +- .../user-management.component.html | 50 ++-- .../assessment-complaint-alert.component.html | 4 +- .../assessment-warning.component.ts | 4 +- ...tructions-assessment-layout.component.html | 6 +- .../complaints-student-view.component.html | 10 +- .../complaints-for-tutor.component.html | 44 +--- .../form/complaints-form.component.html | 26 +- .../list-of-complaints.component.html | 63 +++-- .../request/complaint-request.component.html | 15 +- .../app/core/about-us/about-us.component.html | 20 +- ...-export-confirmation-dialog.component.html | 2 +- .../data-export/data-export.component.html | 6 +- .../competencies-popover.component.html | 10 +- .../competency-card.component.html | 2 +- .../competency-relation-graph.component.html | 10 +- .../import-all-competencies.component.html | 6 +- ...tency-recommendation-detail.component.html | 8 +- .../course-description-form.component.html | 12 +- .../import/competency-search.component.html | 24 +- .../course-scores.component.html | 12 +- ...sment-dashboard-information.component.html | 32 +-- .../assessment-dashboard.component.html | 28 +-- .../exam-assessment-buttons.component.html | 2 +- .../learning-path-legend.component.html | 20 +- .../learning-path.component.html | 2 +- .../learning-path-management.component.html | 2 +- .../course-lti-configuration.component.html | 34 +-- ...it-course-lti-configuration.component.html | 2 +- ...management-exercises-search.component.html | 28 +-- ...course-management-exercises.component.html | 4 +- .../manage/course-management.component.html | 8 +- .../manage/course-update.component.html | 34 ++- .../course-detail-line-chart.component.html | 21 +- .../course-management-card.component.html | 40 ++-- ...rse-management-exercise-row.component.html | 12 +- ...agement-overview-statistics.component.html | 2 +- ...case-instructor-detail-view.component.html | 20 +- ...arism-cases-instructor-view.component.html | 18 +- ...sm-case-student-detail-view.component.html | 10 +- .../tutorial-group-detail.component.html | 2 +- ...al-group-free-days-overview.component.html | 5 +- .../tutorial-group-session-row.component.html | 4 +- ...torial-group-sessions-table.component.html | 34 +-- .../tutorial-group-row.component.html | 4 +- .../tutorial-groups-table.component.html | 30 +-- .../registered-students.component.html | 11 +- ...-tutorial-group-free-period.component.html | 2 +- ...-tutorial-group-free-period.component.html | 2 +- ...rial-group-free-period-form.component.html | 60 ++--- ...oup-free-period-row-buttons.component.html | 2 +- ...oup-free-periods-management.component.html | 12 +- ...al-group-free-periods-table.component.html | 12 +- ...eate-tutorial-group-session.component.html | 2 +- ...edit-tutorial-group-session.component.html | 2 +- ...tutorial-group-session-form.component.html | 42 +--- .../cancellation-modal.component.html | 24 +- ...l-group-session-row-buttons.component.html | 2 +- ...l-group-sessions-management.component.html | 10 +- .../tutorial-groups-checklist.component.html | 26 +- ...torial-groups-configuration.component.html | 6 +- ...torial-groups-configuration.component.html | 2 +- ...l-groups-configuration-form.component.html | 63 +++-- .../create-tutorial-group.component.html | 2 +- .../edit-tutorial-group.component.html | 2 +- .../schedule-form.component.html | 64 ++--- .../tutorial-group-form.component.html | 92 +++----- .../tutorial-group-row-buttons.component.html | 6 +- ...l-groups-course-information.component.html | 4 +- ...torial-groups-import-button.component.html | 6 +- ...-registration-import-dialog.component.html | 25 +- .../tutorial-groups-management.component.html | 10 +- ...scores-average-scores-graph.component.html | 2 +- .../exam-scores/exam-scores.component.html | 145 +++++------- .../manage/exam-management.component.html | 30 +-- .../exam/manage/exam-status.component.html | 46 ++-- .../exam-checklist.component.html | 172 +++++++------- .../exam-edit-working-time.component.html | 2 +- .../manage/exams/exam-detail.component.html | 22 +- .../exam-import/exam-import.component.html | 8 +- .../exam-mode-picker.component.html | 17 +- .../manage/exams/exam-update.component.html | 14 +- .../exercise-group-update.component.html | 4 +- .../exercise-groups.component.html | 56 +++-- .../student-exams.component.html | 4 +- .../students/exam-students.component.html | 2 +- ...udents-upload-images-dialog.component.html | 2 +- ...m-students-attendance-check.component.html | 18 +- .../plagiarism-cases-overview.component.html | 28 ++- .../suspicious-behavior.component.html | 20 +- ...uspicious-sessions-overview.component.html | 14 +- .../test-run-management.component.html | 6 +- .../exam-participation-cover.component.html | 4 +- .../exam-navigation-bar.component.html | 25 +- .../exam-participation.component.html | 8 +- ...file-upload-exam-submission.component.html | 17 +- .../text/text-exam-submission.component.html | 8 +- .../exam-general-information.component.html | 8 +- .../exam-result-summary.component.html | 8 +- .../quiz-exam-summary.component.html | 4 +- .../events/exam-live-event.component.html | 17 +- .../file-upload-assessment.component.html | 4 +- ...file-upload-exercise-update.component.html | 26 +- .../file-upload-submission.component.html | 4 +- ...example-modeling-submission.component.html | 12 +- .../modeling-exercise-update.component.html | 48 ++-- ...ution-entry-generation-step.component.html | 4 +- .../git-diff-report-modal.component.html | 10 +- ...a-category-distribution-chart.component.ts | 47 ++-- ...est-case-distribution-chart.component.html | 49 ++-- ...-exercise-configure-grading.component.html | 6 +- ...xercise-grading-tasks-table.component.html | 4 +- ...ercise-instruction-analysis.component.html | 4 +- .../task-count-warning.component.html | 2 +- ...programming-exercise-detail.component.html | 2 +- ...rogramming-exercise-grading.component.html | 18 +- ...rogramming-exercise-problem.component.html | 12 +- ...de-editor-student-container.component.html | 20 +- .../programming-submission-policy-status.ts | 20 +- .../status/code-editor-status.component.html | 12 +- ...ise-instruction-step-wizard.component.html | 4 +- ...apollon-diagram-create-form.component.html | 22 +- .../apollon-diagram-detail.component.html | 13 +- .../apollon-diagram-list.component.html | 2 +- ...drag-and-drop-question-edit.component.html | 12 +- ...match-percentage-info-modal.component.html | 10 +- ...ltiple-choice-question-edit.component.html | 6 +- .../quiz-exercise-update.component.html | 12 +- .../quiz/manage/quiz-exercise.component.html | 12 +- .../quiz-scoring-info-modal.component.html | 14 +- ...te-multiple-choice-question.component.html | 6 +- .../quiz-re-evaluate.component.html | 4 +- .../short-answer-question-edit.component.html | 48 ++-- .../drag-and-drop-question.component.html | 20 +- .../multiple-choice-question.component.html | 14 +- ...iple-choice-visual-question.component.html | 4 +- .../short-answer-question.component.html | 8 +- .../assessment-progress-label.component.html | 22 +- ...ercise-assessment-dashboard.component.html | 146 ++++-------- .../difficulty-picker.component.html | 24 +- .../example-solution.component.html | 4 +- .../example-submission-import.component.html | 4 +- .../example-submissions.component.html | 2 +- ...rcise-detail-common-actions.component.html | 2 +- ...-exercise-page-with-details.component.html | 29 +-- .../header-participation-page.component.html | 6 +- .../exercise-hint-expandable.component.html | 4 +- ...xercise-hint-student-dialog.component.html | 9 +- .../exercise-info.component.html | 2 +- .../exercise-scores.component.html | 37 ++- .../exercise-update-warning.component.html | 4 +- .../shared/feedback/feedback.component.html | 32 +-- ...ded-in-overall-score-picker.component.html | 15 +- .../participation.component.html | 8 +- .../exercise-update-plagiarism.component.html | 13 +- .../plagiarism-header.component.html | 10 +- .../plagiarism-inspector.component.html | 16 +- .../plagiarism-run-details.component.html | 42 ++-- .../plagiarism-sidebar.component.html | 8 +- .../split-pane-header.component.html | 2 +- .../text-submission-viewer.component.html | 13 +- .../rating-list/rating-list.component.html | 20 +- .../shared/result/result.component.html | 35 +-- ...rading-instructions-details.component.html | 12 +- .../submission-policy-update.component.ts | 6 +- .../team-participation-table.component.html | 12 +- .../teams-import-dialog.component.html | 36 +-- .../shared/team/teams.component.html | 4 +- .../unreferenced-feedback.component.html | 11 +- .../text-assessment-area.component.html | 13 +- .../example-text-submission.component.html | 22 +- .../text-exercise-update.component.html | 22 +- .../feature-overview.component.html | 4 +- .../grading-system/bonus/bonus.component.html | 34 ++- .../detailed-grading-system.component.html | 24 +- .../grading-key-overview.component.html | 2 +- .../grading-key-table.component.html | 10 +- .../grading-system-info-modal.component.html | 26 +- .../grading-system.component.html | 12 +- .../interval-grading-system.component.html | 14 +- .../guided-tour/guided-tour.component.html | 2 +- .../iris/about-iris/about-iris.component.html | 8 +- .../iris-base-chatbot.component.html | 2 +- ...-common-sub-settings-update.component.html | 20 +- .../shared/iris-enabled.component.html | 10 +- .../lecture-attachments.component.html | 6 +- .../attachment-unit-form.component.html | 30 +-- .../attachment-units.component.html | 8 +- .../create-exercise-unit.component.html | 27 ++- .../lecture-unit-layout.component.html | 2 +- .../lecture-unit-management.component.html | 2 +- .../online-unit-form.component.html | 28 +-- .../text-unit-form.component.html | 12 +- .../video-unit-form.component.html | 34 +-- .../app/lecture/lecture-update.component.html | 14 +- .../webapp/app/lecture/lecture.component.html | 2 +- ...lecture-wizard-competencies.component.html | 12 +- ...course-competencies-details.component.html | 6 +- .../course-competencies.component.html | 10 +- ...nversations-code-of-conduct.component.html | 2 +- .../course-conversations.component.html | 4 +- .../course-wide-search.component.html | 8 +- .../channel-form/channel-form.component.html | 74 +++--- .../channels-create-dialog.component.html | 4 +- .../channel-item/channel-item.component.html | 38 ++- .../channels-overview-dialog.component.html | 12 +- ...conversation-add-users-form.component.html | 24 +- ...nversation-add-users-dialog.component.html | 4 +- .../conversation-detail-dialog.component.html | 41 +++- .../conversation-info.component.html | 32 ++- .../conversation-member-row.component.html | 27 ++- .../conversation-members.component.html | 18 +- .../conversation-settings.component.html | 27 ++- ...generic-confirmation-dialog.component.html | 2 +- ...update-text-property-dialog.component.html | 2 +- .../group-chat-create-dialog.component.html | 12 +- ...e-to-one-chat-create-dialog.component.html | 2 +- .../conversation-header.component.html | 10 +- .../conversation-messages.component.html | 17 +- .../course-dashboard.component.html | 2 +- .../course-exercise-row.component.html | 2 +- .../course-lecture-details.component.html | 30 ++- .../course-lecture-row.component.html | 2 +- .../overview/course-overview.component.html | 4 +- ...course-prerequisites-button.component.html | 4 +- .../course-registration-button.component.html | 4 +- .../course-registration-detail.component.html | 8 +- .../course-prerequisites-modal.component.html | 2 +- .../course-registration.component.html | 8 +- .../course-statistics.component.html | 222 +++++++++--------- .../course-tutorial-group-card.component.html | 11 +- .../course-unenrollment-modal.component.html | 20 +- .../app/overview/courses.component.html | 12 +- .../discussion-section.component.html | 28 +-- .../course-exercise-details.component.html | 10 +- .../problem-statement.component.html | 2 +- .../app/overview/header-course.component.html | 4 +- .../submission-result-status.component.html | 14 +- .../exercise-scores-chart.component.html | 6 +- .../code-button/code-button.component.html | 9 +- .../not-released-tag.component.html | 5 +- .../open-code-editor-button.component.html | 4 +- .../reset-repo-button.component.html | 6 +- .../start-practice-mode-button.component.html | 6 +- .../connection-warning.component.html | 4 +- .../consistency-check.component.html | 2 +- .../course-users-selector.component.html | 12 +- .../tutor-leaderboard.component.html | 18 +- .../date-time-picker.component.html | 2 +- .../exercise-categories.component.html | 2 +- .../layouts/navbar/navbar.component.html | 2 +- ...loading-indicator-container.component.html | 2 +- .../markdown-editor.component.html | 20 +- .../message-inline-input.component.html | 6 +- .../message-reply-inline-input.component.html | 6 +- .../posting-content-part.component.html | 4 +- .../posting-content.component.html | 8 +- .../post-create-edit-modal.component.html | 4 +- .../notification-sidebar.component.html | 24 +- ...icipant-scores-distribution.component.html | 8 +- ...tistics-average-score-graph.component.html | 18 +- ...cs-score-distribution-graph.component.html | 2 +- ...ype-ahead-user-search-field.component.html | 2 +- .../users-import-dialog.component.html | 12 +- .../account-information.component.html | 30 +-- .../notification-settings.component.html | 5 +- .../science-settings.component.html | 8 +- .../user-settings-container.component.html | 30 +-- ...sessment-complaint-alert.component.spec.ts | 9 +- .../code-editor-status.component.spec.ts | 33 ++- .../complaints-for-tutor.component.spec.ts | 11 +- ...res-average-scores-graph.component.spec.ts | 5 +- .../exercise-group-update.component.spec.ts | 5 +- .../exam-navigation-bar.component.spec.ts | 5 +- .../text-exam-submission.component.spec.ts | 2 + .../events/exam-live-event.component.spec.ts | 12 +- .../test-run-management.component.spec.ts | 1 + .../component/exercises/shared/result.spec.ts | 3 +- .../detailed-grading-system.component.spec.ts | 2 + ...versation-add-users-form.component.spec.ts | 5 +- .../course-statistics.component.spec.ts | 18 +- ...ramming-exercise-grading.component.spec.ts | 2 + .../shared/code-button.component.spec.ts | 2 + .../course-users-selector.component.spec.ts | 119 +++++----- .../notification-sidebar.component.spec.ts | 9 +- .../component/shared/result.component.spec.ts | 22 +- .../conversation-options.component.spec.ts | 5 +- ...tics-average-score-graph.component.spec.ts | 5 +- .../text-assessment-area.component.spec.ts | 11 +- ...xt-submission-assessment.component.spec.ts | 4 +- ...urse-tutorial-group-card.component.spec.ts | 17 +- ...ial-group-sessions-table.component.spec.ts | 12 +- .../tutorial-groups-table.component.spec.ts | 3 + ...l-group-free-period-form.component.spec.ts | 11 +- ...-free-periods-management.component.spec.ts | 4 +- ...orial-group-session-form.component.spec.ts | 11 +- ...roups-configuration-form.component.spec.ts | 11 +- ...gistration-import-dialog.component.spec.ts | 5 +- 301 files changed, 2269 insertions(+), 2614 deletions(-) diff --git a/src/main/webapp/app/admin/lti-configuration/lti-configuration.component.html b/src/main/webapp/app/admin/lti-configuration/lti-configuration.component.html index 6e026c952a34..32fb9e3112d8 100644 --- a/src/main/webapp/app/admin/lti-configuration/lti-configuration.component.html +++ b/src/main/webapp/app/admin/lti-configuration/lti-configuration.component.html @@ -8,7 +8,7 @@
diff --git a/src/main/webapp/app/admin/system-notification-management/system-notification-management.component.html b/src/main/webapp/app/admin/system-notification-management/system-notification-management.component.html index d6b7c7feca64..867f0c697bb2 100644 --- a/src/main/webapp/app/admin/system-notification-management/system-notification-management.component.html +++ b/src/main/webapp/app/admin/system-notification-management/system-notification-management.component.html @@ -30,19 +30,13 @@

@switch (getNotificationState(notification)) { @case (ACTIVE) { - - {{ 'artemisApp.systemNotification.active' | artemisTranslate }} - + } @case (EXPIRED) { - - {{ 'artemisApp.systemNotification.expired' | artemisTranslate }} - + } @case (SCHEDULED) { - - {{ 'artemisApp.systemNotification.scheduled' | artemisTranslate }} - + } } diff --git a/src/main/webapp/app/admin/upcoming-exams-and-exercises/upcoming-exams-and-exercises.component.html b/src/main/webapp/app/admin/upcoming-exams-and-exercises/upcoming-exams-and-exercises.component.html index 1451541419e2..9d928688216a 100644 --- a/src/main/webapp/app/admin/upcoming-exams-and-exercises/upcoming-exams-and-exercises.component.html +++ b/src/main/webapp/app/admin/upcoming-exams-and-exercises/upcoming-exams-and-exercises.component.html @@ -10,16 +10,16 @@

- {{ 'artemisApp.upcomingExamsAndExercises.exercise' | artemisTranslate }} + - {{ 'artemisApp.upcomingExamsAndExercises.course' | artemisTranslate }} + - {{ 'artemisApp.upcomingExamsAndExercises.releaseDate' | artemisTranslate }} + - {{ 'artemisApp.upcomingExamsAndExercises.dueDate' | artemisTranslate }} + @@ -61,19 +61,19 @@

- {{ 'artemisApp.upcomingExamsAndExercises.exam' | artemisTranslate }} + - {{ 'artemisApp.upcomingExamsAndExercises.course' | artemisTranslate }} + - {{ 'artemisApp.examManagement.visibleDate' | artemisTranslate }} + - {{ 'artemisApp.examManagement.startDate' | artemisTranslate }} + - {{ 'artemisApp.examManagement.endDate' | artemisTranslate }} + diff --git a/src/main/webapp/app/admin/user-management/user-management.component.html b/src/main/webapp/app/admin/user-management/user-management.component.html index 1c824ea864d0..60a62a3cd70c 100644 --- a/src/main/webapp/app/admin/user-management/user-management.component.html +++ b/src/main/webapp/app/admin/user-management/user-management.component.html @@ -63,9 +63,11 @@

[ngClass]="{ 'btn-secondary': !filters.numberOfAppliedFilters, 'btn-success': !!filters.numberOfAppliedFilters }" > - {{ - 'artemisApp.userManagement.filter.modal.open' | artemisTranslate: { num: filters.numberOfAppliedFilters } - }} +

@@ -227,14 +229,14 @@

@@ -304,7 +300,7 @@
{{ 'artemisApp.userManagement.filter.authority.title' | artemis
-
{{ 'artemisApp.userManagement.filter.origin.title' | artemisTranslate: { num: this.filters.originFilter.size } }}
+
@@ -321,16 +317,18 @@
{{ 'artemisApp.userManagement.filter.origin.title' | artemisTra }
  • - +
  • -
    {{ 'artemisApp.userManagement.filter.registrationNumber.title' | artemisTranslate: { num: this.filters.registrationNumberFilter.size } }}
    +
    @@ -360,16 +358,14 @@
    {{ 'artemisApp.userManagement.filter.registrationNumber.title' (click)="this.toggleRegistrationNumberFilter()" [checked]="this.filters.registrationNumberFilter.size === 0" /> - +
    -
    {{ 'artemisApp.userManagement.filter.status.title' | artemisTranslate: { num: this.filters.statusFilter.size } }}
    +
    @@ -386,9 +382,7 @@
    {{ 'artemisApp.userManagement.filter.status.title' | artemisTra }
  • - +
  • @@ -396,9 +390,7 @@
    {{ 'artemisApp.userManagement.filter.status.title' | artemisTra
    } diff --git a/src/main/webapp/app/complaints/complaints-for-tutor/complaints-for-tutor.component.html b/src/main/webapp/app/complaints/complaints-for-tutor/complaints-for-tutor.component.html index 0997b352dbbd..97bd1ae26257 100644 --- a/src/main/webapp/app/complaints/complaints-for-tutor/complaints-for-tutor.component.html +++ b/src/main/webapp/app/complaints/complaints-for-tutor/complaints-for-tutor.component.html @@ -1,25 +1,20 @@ @if (isLoading) {
    - {{ 'loading' | artemisTranslate }} +
    } @if (!isLoading && complaint) { -

    - {{ complaint.complaintType === ComplaintType.MORE_FEEDBACK ? ('artemisApp.moreFeedback.review' | artemisTranslate) : ('artemisApp.complaint.review' | artemisTranslate) }} -

    +

    } @if (!isLoading && complaint) {
    @if (handled) { -
    - {{ - complaint.complaintType === ComplaintType.MORE_FEEDBACK - ? ('artemisApp.moreFeedback.alreadyHandled' | artemisTranslate) - : ('artemisApp.complaint.complaintAlreadyHandled' | artemisTranslate) - }} -
    +
    }
    @if (showLockDuration) { @@ -35,21 +30,13 @@

    } @if (lockedByCurrentUser) { - + }

    - {{ - complaint.complaintType === ComplaintType.MORE_FEEDBACK - ? ('artemisApp.moreFeedback.title' | artemisTranslate) - : ('artemisApp.complaint.title' | artemisTranslate) - }} - + @if (handled) { @if (complaint?.accepted) { @@ -71,13 +58,9 @@

    @if (handled || isAllowedToRespond) {
    -

    - {{ - complaint.complaintType === ComplaintType.MORE_FEEDBACK - ? ('artemisApp.moreFeedbackResponse.title' | artemisTranslate) - : ('artemisApp.complaintResponse.title' | artemisTranslate) - }} -

    +

    diff --git a/src/main/webapp/app/complaints/list-of-complaints/list-of-complaints.component.html b/src/main/webapp/app/complaints/list-of-complaints/list-of-complaints.component.html index 0651cc2ad7e5..ff642a21befc 100644 --- a/src/main/webapp/app/complaints/list-of-complaints/list-of-complaints.component.html +++ b/src/main/webapp/app/complaints/list-of-complaints/list-of-complaints.component.html @@ -3,10 +3,10 @@

    @if (complaintType === ComplaintType.COMPLAINT) { - {{ 'artemisApp.complaint.listOfComplaints.title' | artemisTranslate }} + } @if (complaintType === ComplaintType.MORE_FEEDBACK) { - {{ 'artemisApp.moreFeedback.list.title' | artemisTranslate }} + }

    @@ -15,16 +15,16 @@

    @if (!allComplaintsForTutorLoaded && complaintType === ComplaintType.COMPLAINT) { - {{ 'artemisApp.complaint.listOfComplaints.loadAllComplaintsExplanation' | artemisTranslate }} + } @if (!allComplaintsForTutorLoaded && complaintType === ComplaintType.MORE_FEEDBACK) { - {{ 'artemisApp.moreFeedback.list.loadAllRequestsExplanation' | artemisTranslate }} + } @if (allComplaintsForTutorLoaded && complaintType === ComplaintType.COMPLAINT) { - {{ 'artemisApp.complaint.listOfComplaints.allComplaintsLoaded' | artemisTranslate }} + } @if (allComplaintsForTutorLoaded && complaintType === ComplaintType.MORE_FEEDBACK) { - {{ 'artemisApp.moreFeedback.list.allRequestsLoaded' | artemisTranslate }} + } @if (!allComplaintsForTutorLoaded) { } @@ -59,13 +59,12 @@

    />

    @@ -80,48 +79,48 @@

    - {{ 'artemisApp.complaint.listOfComplaints.exercise' | artemisTranslate }} + - {{ 'artemisApp.complaint.listOfComplaints.submissionId' | artemisTranslate }} + - {{ 'artemisApp.complaint.listOfComplaints.assessorName' | artemisTranslate }} + @if (course?.isAtLeastInstructor) { - {{ 'artemisApp.complaint.listOfComplaints.studentLogin' | artemisTranslate }} + - {{ 'artemisApp.complaint.listOfComplaints.studentName' | artemisTranslate }} + - {{ 'artemisApp.complaint.listOfComplaints.reviewerName' | artemisTranslate }} + } - {{ 'artemisApp.complaint.listOfComplaints.dateAndTime' | artemisTranslate }} + - {{ 'artemisApp.complaint.listOfComplaints.responseTime' | artemisTranslate }} + - {{ 'artemisApp.complaint.listOfComplaints.status' | artemisTranslate }} + - {{ 'artemisApp.locks.lockStatus' | artemisTranslate }} + - {{ 'artemisApp.complaint.listOfComplaints.actions' | artemisTranslate }} + @@ -166,16 +165,16 @@

    @if (complaint.accepted === undefined) { - {{ 'artemisApp.complaint.listOfComplaints.noReply' | artemisTranslate }} + } @if (complaint.accepted === true && complaintType === ComplaintType.COMPLAINT) { - {{ 'artemisApp.complaint.listOfComplaints.accepted' | artemisTranslate }} + } @if (complaint.accepted === true && complaintType === ComplaintType.MORE_FEEDBACK) { - {{ 'artemisApp.moreFeedback.accepted' | artemisTranslate }} + } @if (complaint.accepted === false) { - {{ 'artemisApp.complaint.listOfComplaints.rejected' | artemisTranslate }} + } @@ -185,10 +184,10 @@

    @@ -199,10 +198,10 @@

    @if (complaintType === ComplaintType.COMPLAINT) { - {{ 'artemisApp.exerciseAssessmentDashboard.noComplaints' | artemisTranslate }} + } @if (complaintType === ComplaintType.MORE_FEEDBACK) { - {{ 'artemisApp.exerciseAssessmentDashboard.noMoreFeedbackRequests' | artemisTranslate }} + }

    diff --git a/src/main/webapp/app/complaints/request/complaint-request.component.html b/src/main/webapp/app/complaints/request/complaint-request.component.html index 5c3b8f448f1c..2f86094c7345 100644 --- a/src/main/webapp/app/complaints/request/complaint-request.component.html +++ b/src/main/webapp/app/complaints/request/complaint-request.component.html @@ -7,18 +7,13 @@ }} {{ complaint.submittedTime | artemisTimeAgo }} @if (complaint.accepted === true) { - - {{ - complaint.complaintType === ComplaintType.COMPLAINT - ? ('artemisApp.complaint.acceptedLong' | artemisTranslate) - : ('artemisApp.moreFeedback.acceptedLong' | artemisTranslate) - }} - + } @if (complaint.accepted === false) { - - {{ 'artemisApp.complaint.rejectedLong' | artemisTranslate }} - + }

    - +
    - +

    [ngModel]="hideOptional" (ngModelChange)="triggerOptionalExercises()" /> - +

    } @@ -120,33 +116,33 @@

    - {{ 'artemisApp.assessmentDashboard.exerciseType' | artemisTranslate }} + - {{ 'artemisApp.assessmentDashboard.exercise' | artemisTranslate }} + @if (!isTestRun) { - {{ 'artemisApp.assessmentDashboard.yourStatus' | artemisTranslate }} + } - {{ 'artemisApp.assessmentDashboard.exerciseAverageRating' | artemisTranslate }} + @if (!isExamMode) { - {{ 'artemisApp.assessmentDashboard.exerciseDueDate' | artemisTranslate }} + } @if (!isExamMode) { - {{ 'artemisApp.assessmentDashboard.assessmentsDueDate' | artemisTranslate }} + } - {{ 'artemisApp.assessmentDashboard.actions' | artemisTranslate }} + @@ -241,7 +237,7 @@

    @if (course && course.isAtLeastInstructor && tutorIssues.length > 0) {
    -

    {{ 'artemisApp.assessmentDashboard.tutorPerformanceIssues.title' | artemisTranslate }}

    +

    @for (issue of tutorIssues; track issue) {
      @if (issue.averageTutorValue < issue.allowedRange.lowerBound) { @@ -278,10 +274,10 @@

      {{ 'artemisApp.assessmentDashboard.tutorPerformanceIssues.title' | artemisTr }
      @if (!isExamMode) { -

      {{ 'artemisApp.assessmentDashboard.tutorLeaderboard.courseTitle' | artemisTranslate }}

      +

      } @if (isExamMode) { -

      {{ 'artemisApp.assessmentDashboard.tutorLeaderboard.examTitle' | artemisTranslate }}

      +

      }
      diff --git a/src/main/webapp/app/course/dashboards/assessment-dashboard/exam-assessment-buttons/exam-assessment-buttons.component.html b/src/main/webapp/app/course/dashboards/assessment-dashboard/exam-assessment-buttons/exam-assessment-buttons.component.html index 988ad7835bce..1bae6b366f55 100644 --- a/src/main/webapp/app/course/dashboards/assessment-dashboard/exam-assessment-buttons/exam-assessment-buttons.component.html +++ b/src/main/webapp/app/course/dashboards/assessment-dashboard/exam-assessment-buttons/exam-assessment-buttons.component.html @@ -3,7 +3,7 @@ - {{ 'artemisApp.examManagement.gradingSystem' | artemisTranslate }} +

    } @@ -13,9 +11,7 @@
    - - {{ 'artemisApp.learningPath.graph.legend.matchStart.title' | artemisTranslate }} - +
    } @@ -23,9 +19,7 @@
    - - {{ 'artemisApp.learningPath.graph.legend.matchEnd.title' | artemisTranslate }} - +
    } @@ -33,9 +27,7 @@
    - - {{ 'artemisApp.learningPath.graph.legend.learningObject.title' | artemisTranslate }} - +
    } @@ -43,9 +35,7 @@
    - - {{ 'artemisApp.learningPath.graph.legend.completedLearningObject.title' | artemisTranslate }} - +
    } diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path.component.html b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path.component.html index 396b2735560b..864bcc2920b0 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/learning-path.component.html @@ -1,7 +1,7 @@ @if (isLoading) {
    - {{ 'loading' | artemisTranslate }} +
    } @else { diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html index 061c611f6b93..c8a10304da15 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html @@ -1,7 +1,7 @@ @if (isLoading) {
    - {{ 'loading' | artemisTranslate }} +
    } diff --git a/src/main/webapp/app/course/manage/course-lti-configuration/course-lti-configuration.component.html b/src/main/webapp/app/course/manage/course-lti-configuration/course-lti-configuration.component.html index 8c267652beff..b7252be6dc5d 100644 --- a/src/main/webapp/app/course/manage/course-lti-configuration/course-lti-configuration.component.html +++ b/src/main/webapp/app/course/manage/course-lti-configuration/course-lti-configuration.component.html @@ -16,28 +16,28 @@