diff --git a/i18n/en.ts b/i18n/en.ts index 0fb1c11111f2d7a22d387374ffb5920964e04bcf..6c1e49add5af9d8d9edb8cd2f63cd8741bbd0bd2 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 1b0c034681dced7f66ea361604ae0efe5cc530b4..4857d5926d532fbb992b33cfc2b3e8d476975579 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 e7819a6b8fb546266426f8d5abba33f5539e38c3..eee20352990381df018750be7f07c52ea705ece8 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 2aab6faae9af24e32fb2bc5ba7b5fb47482b6776..af063dbcc553a2b2b5742de83a573a9e60106f8b 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 7dab23cb61b242351ece456d8e3baa8e3a321423..739a52401ec3dcc39e6e75d44c3fa29f6bc9bf3e 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 9ce11a393d9f50102d922730d30eed97d68fbe01..1a8b8afd397ea589b81a6febcb90e572135ddd4d 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 b5d799523b175cac1ca823fdfe5d0823c6726f02..077918c19906e652e7bd474c3b9ca5cc37efe2a3 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,