feat: disable submission until it's done (#51150)

This commit is contained in:
Oliver Eyton-Williams
2023-08-07 17:23:23 +02:00
committed by GitHub
parent ada027798e
commit c387bbe43d
7 changed files with 68 additions and 11 deletions
@@ -21,7 +21,8 @@ import {
isCompletionModalOpenSelector,
successMessageSelector,
challengeFilesSelector,
challengeMetaSelector
challengeMetaSelector,
isSubmittingSelector
} from '../redux/selectors';
import ProgressBar from '../../../components/ProgressBar';
import GreenPass from '../../../assets/icons/green-pass';
@@ -36,7 +37,7 @@ const mapStateToProps = createSelector(
isSignedInSelector,
allChallengesInfoSelector,
successMessageSelector,
isSubmittingSelector,
(
challengeFiles: ChallengeFiles,
{ dashedName }: { dashedName: string },
@@ -44,7 +45,8 @@ const mapStateToProps = createSelector(
isOpen: boolean,
isSignedIn: boolean,
allChallengesInfo: AllChallengesInfo,
message: string
message: string,
isSubmitting: boolean
) => ({
challengeFiles,
dashedName,
@@ -52,7 +54,8 @@ const mapStateToProps = createSelector(
isOpen,
isSignedIn,
allChallengesInfo,
message
message,
isSubmitting
})
);
@@ -147,6 +150,7 @@ class CompletionModal extends Component<
close,
isOpen,
isSignedIn,
isSubmitting,
message,
t,
dashedName,
@@ -159,6 +163,7 @@ class CompletionModal extends Component<
return (
<Modal
data-cy='completion-modal'
animation={false}
bsSize='lg'
dialogClassName='challenge-success-modal'
@@ -193,6 +198,8 @@ class CompletionModal extends Component<
block={true}
bsSize='large'
bsStyle='primary'
disabled={isSubmitting}
data-cy='submit-challenge'
onClick={() => submitChallenge()}
>
{isSignedIn ? t('buttons.submit-and-go') : t('buttons.go-to-next')}
@@ -1,4 +1,4 @@
import { createTypes } from '../../../utils/create-types';
import { createAsyncTypes, createTypes } from '../../../utils/create-types';
export const CURRENT_CHALLENGE_KEY = 'currentChallengeId';
@@ -43,10 +43,10 @@ export const actionTypes = createTypes(
'executeChallenge',
'resetChallenge',
'stopResetting',
'submitChallenge',
'resetAttempts',
'setEditorFocusability',
'toggleVisibleEditor'
'toggleVisibleEditor',
...createAsyncTypes('submitChallenge')
],
ns
);
@@ -71,6 +71,12 @@ export const executeChallenge = createAction(actionTypes.executeChallenge);
export const resetChallenge = createAction(actionTypes.resetChallenge);
export const stopResetting = createAction(actionTypes.stopResetting);
export const submitChallenge = createAction(actionTypes.submitChallenge);
export const submitChallengeComplete = createAction(
actionTypes.submitChallengeComplete
);
export const submitChallengeError = createAction(
actionTypes.submitChallengeError
);
export const resetAttempts = createAction(actionTypes.resetAttempts);
export const setEditorFocusability = createAction(
@@ -36,7 +36,9 @@ import { actionTypes } from './action-types';
import {
closeModal,
updateSolutionFormValues,
setIsAdvancing
setIsAdvancing,
submitChallengeComplete,
submitChallengeError
} from './actions';
import {
challengeFilesSelector,
@@ -73,14 +75,15 @@ function postChallenge(update, username) {
},
savedChallenges: mapFilesToChallengeFiles(savedChallenges)
}),
updateComplete()
updateComplete(),
submitChallengeComplete()
];
// TODO(Post-MVP): separate endpoint for trophy submission?
if (isTrophyMissing)
actions.push(createFlashMessage(trophyMissingMessage));
return of(...actions);
}),
catchError(() => of(updateFailed(update)))
catchError(() => of(updateFailed(update), submitChallengeError()))
);
return saveChallenge;
}
+14 -1
View File
@@ -52,7 +52,8 @@ const initialState = {
projectFormValues: {},
successMessage: 'Happy Coding!',
isAdvancing: false,
chapterSlug: ''
chapterSlug: '',
isSubmitting: false
};
export const epics = [completionEpic, createQuestionEpic, codeStorageEpic];
@@ -64,6 +65,18 @@ export const sagas = [
export const reducer = handleActions(
{
[actionTypes.submitChallenge]: state => ({
...state,
isSubmitting: true
}),
[actionTypes.submitChallengeComplete]: state => ({
...state,
isSubmitting: false
}),
[actionTypes.submitChallengeError]: state => ({
...state,
isSubmitting: false
}),
[actionTypes.createFiles]: (state, { payload }) => ({
...state,
challengeFiles: payload,
@@ -33,6 +33,7 @@ export const isFinishExamModalOpenSelector = state =>
export const isProjectPreviewModalOpenSelector = state =>
state[ns].modal.projectPreview;
export const isShortcutsModalOpenSelector = state => state[ns].modal.shortcuts;
export const isSubmittingSelector = state => state[ns].isSubmitting;
export const isResettingSelector = state => state[ns].isResetting;
export const isBuildEnabledSelector = state => state[ns].isBuildEnabled;
@@ -221,4 +221,31 @@ describe('project submission', () => {
);
}
);
it('should not be possible to submit twice in quick succession', () => {
const { superBlock, block, challenges } = pythonProjects;
const { slug } = challenges[0];
cy.intercept('http://localhost:3000/project-completed', req => {
req.continue(_res => {
// delay the response by 0.5 seconds
const wait = new Promise<void>(resolve => setTimeout(resolve, 500));
return wait;
});
});
const url = `/learn/${superBlock}/${block}/${slug}`;
cy.visit(url);
cy.get('#dynamic-front-end-form')
.get('#solution')
.type('https://replit.com/@camperbot/python-project#main.py');
cy.contains("I've completed this challenge").click();
cy.get('[data-cy=submit-challenge]').as('submitChallenge');
cy.get('@submitChallenge').click();
cy.get('@submitChallenge').should('be.disabled');
// After the api responds, the button is enabled, but since the modal leaves
// the DOM we just check for that.
cy.get('[data-cy=completion-modal]').should('not.exist');
});
});