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