mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat: disable submission until it's done (#51150)
This commit is contained in:
committed by
GitHub
parent
ada027798e
commit
c387bbe43d
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user