From 5181690796b2da93400e3219e65cd3efff26558b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Janis=20Daniel=20Da=CC=88hne?=
 <janis.daehne2@student.uni-halle.de>
Date: Mon, 13 Apr 2020 15:53:10 +0200
Subject: [PATCH] - fixes issue #140 - tutor view: router will block/prompt
 whenthe assessment changed

---
 i18n/en.ts                                    |  3 +
 i18n/i18nRoot.ts                              |  4 +
 .../sites/tutorViewSite/headerBarContent.tsx  | 96 +++++++++++++++----
 src/constants.ts                              |  2 +-
 .../crud/getSingleAssessmentReducer.ts        |  6 +-
 .../crud/updateSingleAssessmentReducer.ts     |  3 +-
 .../tutorViewSite/tutorViewSiteReducer.ts     | 14 ++-
 7 files changed, 101 insertions(+), 27 deletions(-)

diff --git a/i18n/en.ts b/i18n/en.ts
index 0fb1c111..6c1e49ad 100644
--- a/i18n/en.ts
+++ b/i18n/en.ts
@@ -132,6 +132,9 @@ export const lang_en: LangObj = {
   "Detach file" : "Detach file",
   "Are you sure you want to detach the file? If this file is not connected to another exercise the fille will get deleted!" : "Are you sure you want to detach the file? If this file is not connected to another exercise the fille will get deleted!",
 
+  "Unsaved changes": "Unsaved changes",
+  "The assessment has unsaved changes. Do you really want to quit?": "The assessment has unsaved changes. Do you really want to quit?",
+
   //--- abbreviations
   "h": "h",
   "m": "m",
diff --git a/i18n/i18nRoot.ts b/i18n/i18nRoot.ts
index 1b0c0346..4857d592 100644
--- a/i18n/i18nRoot.ts
+++ b/i18n/i18nRoot.ts
@@ -135,6 +135,10 @@ export interface LangObj {
   "Detach file": string
   "Are you sure you want to detach the file? If this file is not connected to another exercise the fille will get deleted!": string
 
+
+  "Unsaved changes": string
+  "The assessment has unsaved changes. Do you really want to quit?": string
+
   //--- abbreviations
   "h": string
   "m": string,
diff --git a/src/components/sites/tutorViewSite/headerBarContent.tsx b/src/components/sites/tutorViewSite/headerBarContent.tsx
index e7819a6b..eee20352 100644
--- a/src/components/sites/tutorViewSite/headerBarContent.tsx
+++ b/src/components/sites/tutorViewSite/headerBarContent.tsx
@@ -41,6 +41,10 @@ import {justRunProgramArgsMaxLength} from '../../../state/reducers/doExerciseSit
 import {ProgramArgsInputView} from '../../helpers/programArgsInputView'
 import {SolutionFileDoExerciseFullBase} from '../../../types/exerciseSolution'
 import {SimpleDivider} from '../../helpers/simpleDivider'
+import {isEqual} from 'lodash-es'
+import {askDialog} from '../../../helpers/dialogHelper'
+import {upState} from '../../../helpers/typeHelpers'
+import {Prompt} from 'react-router'
 
 //const css = require('./styles.styl');
 
@@ -60,6 +64,7 @@ const mapStateToProps = (rootState: RootState /*, props: MyProps*/) => {
     testTypes: rootState.testTypesState.testTypes,
 
     assessment: rootState.tutorViewSiteState.assessment,
+    assessmentOnLoad: rootState.tutorViewSiteState.assessmentOnLoad,
 
     isSomeDialogDisplayed: rootState.tutorViewSiteState.isViewTestDialogDisplayed
       || rootState.tutorViewSiteState.isCodeEditorSettingsDialogDisplayed,
@@ -116,6 +121,34 @@ const dispatchProps = returntypeof(mapDispatchToProps);
 type Props = typeof stateProps & typeof dispatchProps;
 
 class HeaderBar extends React.Component<Props, any> {
+
+
+  hasUnsavedAssessmentChanges(): boolean {
+    //when we touched (ace editor) string they will get from null to '' we want to ignore this
+
+    //this is only null initially or when a get assessment fails (in this case the assessment is also null)
+    if (this.props.assessmentOnLoad === null) return false
+
+
+    const assessmentCopy = upState(this.props.assessment, {
+      ...this.props.assessment,
+      feedbackForStudent: this.props.assessment.feedbackForStudent ?? '',
+      normalTestPoints: this.props.assessment.normalTestPoints ?? '',
+    })
+
+    const assessmentOnLoadCopy = upState(this.props.assessmentOnLoad, {
+      ...this.props.assessmentOnLoad,
+      feedbackForStudent: this.props.assessmentOnLoad.feedbackForStudent ?? '',
+      normalTestPoints: this.props.assessmentOnLoad.normalTestPoints ?? '',
+    })
+
+    if (isEqual(assessmentCopy, assessmentOnLoadCopy) === false) {
+      return true
+    }
+
+    return false
+  }
+
   render(): JSX.Element {
 
 
@@ -147,9 +180,14 @@ class HeaderBar extends React.Component<Props, any> {
       p => p.id === this.props.selectedTabIdTutorEditor)
 
 
+    const hasUnsavedAssessmentChanges = this.hasUnsavedAssessmentChanges()
+
     const leftArea = (
       <div>
 
+        <Prompt when={hasUnsavedAssessmentChanges}
+                message={getI18n(this.props.langId, 'The assessment has unsaved changes. Do you really want to quit?')}/>
+
         <div className="flexed">
 
           {
@@ -200,17 +238,23 @@ class HeaderBar extends React.Component<Props, any> {
             <SimpleVDivider/>
           }
 
-          <Link className={canSwitchToNextSubmission && previousUserAssessment !== null
+          <div className={canSwitchToNextSubmission && previousUserAssessment !== null
                            ? 'clickable mar-right-half'
                            : 'div-disabled mar-right-half'}
-                to={getTutorViewForSubmission(this.props.comesFromOwnExercisesSite, this.props.exerciseRelease.id,
-                                              previousUserAssessment !== null
-                                              ? previousUserAssessment.userId
-                                              : 0,
-                                              previousUserAssessment !== null
-                                              ? previousUserAssessment.pLangId
-                                              : 0
-                )}
+               onClick={async () => {
+
+                 //handled by router prompt
+                 // if (await this.canLeaveIfHasUnsavedChanges() === false) return
+
+                 history.push(getTutorViewForSubmission(this.props.comesFromOwnExercisesSite, this.props.exerciseRelease.id,
+                   previousUserAssessment !== null
+                   ? previousUserAssessment.userId
+                   : 0,
+                   previousUserAssessment !== null
+                   ? previousUserAssessment.pLangId
+                   : 0
+                 ))
+               }}
           >
             <Icon name="arrow left"/>
             <span>
@@ -218,7 +262,7 @@ class HeaderBar extends React.Component<Props, any> {
               getI18n(this.props.langId, 'Previous')
             }
           </span>
-          </Link>
+          </div>
 
           {
             canSwitchToNextSubmission &&
@@ -270,17 +314,24 @@ class HeaderBar extends React.Component<Props, any> {
             </div>
           }
 
-          <Link className={canSwitchToNextSubmission && nextUserAssessment !== null
+          <div className={canSwitchToNextSubmission && nextUserAssessment !== null
                            ? 'clickable mar-left-half'
                            : 'div-disabled mar-left-half'}
-                to={getTutorViewForSubmission(this.props.comesFromOwnExercisesSite, this.props.exerciseRelease.id,
-                                              nextUserAssessment !== null
-                                              ? nextUserAssessment.userId
-                                              : 0,
-                                              nextUserAssessment !== null
-                                              ? nextUserAssessment.pLangId
-                                              : 0
-                )}
+                onClick={async () => {
+
+                  //handled by router prompt
+                  // if (await this.canLeaveIfHasUnsavedChanges() === false) return
+
+                  history.push(getTutorViewForSubmission(this.props.comesFromOwnExercisesSite, this.props.exerciseRelease.id,
+                    nextUserAssessment !== null
+                    ? nextUserAssessment.userId
+                    : 0,
+                    nextUserAssessment !== null
+                    ? nextUserAssessment.pLangId
+                    : 0
+                  ))
+
+                }}
           >
             <span>
             {
@@ -288,7 +339,7 @@ class HeaderBar extends React.Component<Props, any> {
             }
             </span>
             <Icon name="arrow right" style={{marginLeft: '0.25rem'}}/>
-          </Link>
+          </div>
 
 
           <SimpleVDivider/>
@@ -430,7 +481,10 @@ class HeaderBar extends React.Component<Props, any> {
 
           <SimpleVDivider/>
 
-          <div className="clickable" onClick={() => {
+          <div className="clickable" onClick={async () => {
+
+            //handled by router prompt
+            // if (await this.canLeaveIfHasUnsavedChanges() === false) return
 
             if (this.props.comesFromOwnExercisesSite === null) {
               //go back to assessment statistic
diff --git a/src/constants.ts b/src/constants.ts
index 2aab6faa..af063dbc 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -13,7 +13,7 @@ import Logger from './helpers/logger'
  * y - breaking changes / new features
  * z - fixes, small changes
  */
-export const versionString = '2.8.0'
+export const versionString = '2.8.1'
 
 
 export const supportMail = 'yapex@informatik.uni-halle.de'
diff --git a/src/state/reducers/tutorViewSite/crud/getSingleAssessmentReducer.ts b/src/state/reducers/tutorViewSite/crud/getSingleAssessmentReducer.ts
index 7dab23cb..739a5240 100644
--- a/src/state/reducers/tutorViewSite/crud/getSingleAssessmentReducer.ts
+++ b/src/state/reducers/tutorViewSite/crud/getSingleAssessmentReducer.ts
@@ -79,13 +79,15 @@ export function reducer(state: State = initial, action: AllActions): State {
       return {
         ...state,
         isLoading: false,
-        assessment: action.payload
+        assessment: action.payload,
+        assessmentOnLoad: JSON.parse(JSON.stringify(action.payload)),  //deep copy
       }
     case ActionType.GET_singleAssessment_REJECTED:
       return {
         ...state,
         isLoading: false,
-        assessment: null
+        assessment: null,
+        assessmentOnLoad: null,
       }
 
     default:
diff --git a/src/state/reducers/tutorViewSite/crud/updateSingleAssessmentReducer.ts b/src/state/reducers/tutorViewSite/crud/updateSingleAssessmentReducer.ts
index 9ce11a39..1a8b8afd 100644
--- a/src/state/reducers/tutorViewSite/crud/updateSingleAssessmentReducer.ts
+++ b/src/state/reducers/tutorViewSite/crud/updateSingleAssessmentReducer.ts
@@ -53,7 +53,8 @@ export function reducer(state: State = initial, action: AllActions): State {
     case ActionType.UPDATE_singleAssessment_FULFILLED:
       return {
         ...state,
-        isLoading: false
+        isLoading: false,
+        assessmentOnLoad: JSON.parse(JSON.stringify(state.assessment)) //deep copy, so this is saved -> update assessment on load
       }
     case ActionType.UPDATE_singleAssessment_REJECTED:
       return {
diff --git a/src/state/reducers/tutorViewSite/tutorViewSiteReducer.ts b/src/state/reducers/tutorViewSite/tutorViewSiteReducer.ts
index b5d79952..077918c1 100644
--- a/src/state/reducers/tutorViewSite/tutorViewSiteReducer.ts
+++ b/src/state/reducers/tutorViewSite/tutorViewSiteReducer.ts
@@ -138,13 +138,22 @@ export type State = {
 
   /**
    * null on error
-   * set via CRUD
+   * set via CRUD {@link GET_singleAssessment_FULFILLEDAction}
    * managed via setAssessmentReducer
    *
    * this is never null because backend returns some basic data in case there is no assessment yet
    *   if the assessment in the backend was null then the property {@link AssessmentFullBase.hasAssessment} is set to false (else true)
    */
-  readonly assessment: AssessmentFullBase
+  readonly assessment: AssessmentFullBase | null
+
+  /**
+   * the assessment when we loaded the page (deep copy) see {@link GET_singleAssessment_FULFILLEDAction}
+   * all changes are applied to {@link assessment}
+   *
+   * we use this to check if something change
+   * then notify the user he should save...
+   */
+  readonly assessmentOnLoad: AssessmentFullBase | null
 
   /**
    * true: go back to own exercises, false: back to group exercises
@@ -300,6 +309,7 @@ export type State = {
 export const initial: State = {
 
   assessment: initialAssessment,
+  assessmentOnLoad: null,
   comesFromOwnExercisesSite: true,
   exercise: initalDoExercise,
 
-- 
GitLab