Merge pull request #11322 from Bouncey/fix/Next-Step-Unlocked
Next step unlocked persistencepull/11998/head
commit
09b62bcbcf
|
@ -6,12 +6,13 @@ import PureComponent from 'react-pure-render/component';
|
|||
import LightBox from 'react-images';
|
||||
|
||||
import {
|
||||
stepForward,
|
||||
stepBackward,
|
||||
closeLightBoxImage,
|
||||
completeAction,
|
||||
submitChallenge,
|
||||
openLightBoxImage,
|
||||
closeLightBoxImage
|
||||
stepBackward,
|
||||
stepForward,
|
||||
submitChallenge,
|
||||
updateUnlockedSteps
|
||||
} from '../../redux/actions';
|
||||
import { challengeSelector } from '../../redux/selectors';
|
||||
import { Button, Col, Image, Row } from 'react-bootstrap';
|
||||
|
@ -42,12 +43,30 @@ const mapStateToProps = createSelector(
|
|||
);
|
||||
|
||||
const dispatchActions = {
|
||||
stepForward,
|
||||
stepBackward,
|
||||
closeLightBoxImage,
|
||||
completeAction,
|
||||
submitChallenge,
|
||||
openLightBoxImage,
|
||||
closeLightBoxImage
|
||||
stepBackward,
|
||||
stepForward,
|
||||
submitChallenge,
|
||||
updateUnlockedSteps
|
||||
};
|
||||
|
||||
const propTypes = {
|
||||
closeLightBoxImage: PropTypes.func.isRequired,
|
||||
completeAction: PropTypes.func.isRequired,
|
||||
currentIndex: PropTypes.number,
|
||||
isActionCompleted: PropTypes.bool,
|
||||
isLastStep: PropTypes.bool,
|
||||
isLightBoxOpen: PropTypes.bool,
|
||||
numOfSteps: PropTypes.number,
|
||||
openLightBoxImage: PropTypes.func.isRequired,
|
||||
step: PropTypes.array,
|
||||
steps: PropTypes.array,
|
||||
stepBackward: PropTypes.func,
|
||||
stepForward: PropTypes.func,
|
||||
submitChallenge: PropTypes.func.isRequired,
|
||||
updateUnlockedSteps: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export class StepChallenge extends PureComponent {
|
||||
|
@ -55,22 +74,6 @@ export class StepChallenge extends PureComponent {
|
|||
super(...args);
|
||||
this.handleLightBoxOpen = this.handleLightBoxOpen.bind(this);
|
||||
}
|
||||
static displayName = 'StepChallenge';
|
||||
static propTypes = {
|
||||
currentIndex: PropTypes.number,
|
||||
step: PropTypes.array,
|
||||
steps: PropTypes.array,
|
||||
isActionCompleted: PropTypes.bool,
|
||||
isLastStep: PropTypes.bool,
|
||||
numOfSteps: PropTypes.number,
|
||||
stepForward: PropTypes.func,
|
||||
stepBackward: PropTypes.func,
|
||||
completeAction: PropTypes.func.isRequired,
|
||||
submitChallenge: PropTypes.func.isRequired,
|
||||
isLightBoxOpen: PropTypes.bool,
|
||||
openLightBoxImage: PropTypes.func.isRequired,
|
||||
closeLightBoxImage: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
handleLightBoxOpen(e) {
|
||||
if (!(e.ctrlKey || e.metaKey)) {
|
||||
|
@ -79,6 +82,23 @@ export class StepChallenge extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const { updateUnlockedSteps } = this.props;
|
||||
updateUnlockedSteps([]);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { updateUnlockedSteps } = this.props;
|
||||
updateUnlockedSteps([]);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { steps, updateUnlockedSteps } = this.props;
|
||||
if (nextProps.steps !== steps) {
|
||||
updateUnlockedSteps([]);
|
||||
}
|
||||
}
|
||||
|
||||
renderActionButton(action, completeAction) {
|
||||
const isApiAction = action === '#';
|
||||
const buttonCopy = isApiAction ?
|
||||
|
@ -260,4 +280,7 @@ export class StepChallenge extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
StepChallenge.displayName = 'StepChallenge';
|
||||
StepChallenge.propTypes = propTypes;
|
||||
|
||||
export default connect(mapStateToProps, dispatchActions)(StepChallenge);
|
||||
|
|
|
@ -7,8 +7,12 @@ import types from './types';
|
|||
// step
|
||||
export const stepForward = createAction(types.stepForward);
|
||||
export const stepBackward = createAction(types.stepBackward);
|
||||
export const goToStep = createAction(types.goToStep);
|
||||
export const goToStep = createAction(
|
||||
types.goToStep,
|
||||
(step, isUnlocked) => ({ step, isUnlocked })
|
||||
);
|
||||
export const completeAction = createAction(types.completeAction);
|
||||
export const updateUnlockedSteps = createAction(types.updateUnlockedSteps);
|
||||
export const openLightBoxImage = createAction(types.openLightBoxImage);
|
||||
export const closeLightBoxImage = createAction(types.closeLightBoxImage);
|
||||
|
||||
|
|
|
@ -44,7 +44,8 @@ const initialUiState = {
|
|||
shouldShakeQuestion: false,
|
||||
shouldShowQuestions: false,
|
||||
isChallengeModalOpen: false,
|
||||
successMessage: 'Happy Coding!'
|
||||
successMessage: 'Happy Coding!',
|
||||
unlockedSteps: []
|
||||
};
|
||||
const initialState = {
|
||||
isCodeLocked: false,
|
||||
|
@ -143,17 +144,20 @@ const mainReducer = handleActions(
|
|||
}),
|
||||
|
||||
// step
|
||||
[types.goToStep]: (state, { payload: step = 0 }) => ({
|
||||
[types.goToStep]: (state, { payload: { step = 0, isUnlocked }}) => ({
|
||||
...state,
|
||||
currentIndex: step,
|
||||
previousIndex: state.currentIndex,
|
||||
isActionCompleted: false
|
||||
isActionCompleted: isUnlocked
|
||||
}),
|
||||
|
||||
[types.completeAction]: state => ({
|
||||
...state,
|
||||
isActionCompleted: true
|
||||
}),
|
||||
[types.updateUnlockedSteps]: (state, { payload }) => ({
|
||||
...state,
|
||||
unlockedSteps: payload
|
||||
}),
|
||||
[types.openLightBoxImage]: state => ({
|
||||
...state,
|
||||
isLightBoxOpen: true
|
||||
|
|
|
@ -1,26 +1,44 @@
|
|||
import types from './types';
|
||||
import { goToStep, submitChallenge } from './actions';
|
||||
import { goToStep, submitChallenge, updateUnlockedSteps } from './actions';
|
||||
import { challengeSelector } from './selectors';
|
||||
import getActionsOfType from '../../../../utils/get-actions-of-type';
|
||||
|
||||
function unlockStep(step, unlockedSteps) {
|
||||
if (!step) {
|
||||
return null;
|
||||
}
|
||||
const updatedSteps = [ ...unlockedSteps ];
|
||||
updatedSteps[step] = true;
|
||||
return updateUnlockedSteps(updatedSteps);
|
||||
}
|
||||
|
||||
export default function stepChallengeEpic(actions, getState) {
|
||||
return getActionsOfType(
|
||||
actions,
|
||||
types.stepForward,
|
||||
types.stepBackward
|
||||
types.stepBackward,
|
||||
types.completeAction
|
||||
)
|
||||
.map(({ type }) => {
|
||||
const state = getState();
|
||||
const { challenge: { description = [] } } = challengeSelector(state);
|
||||
const { challengesApp: { currentIndex } } = state;
|
||||
const { challengesApp: { currentIndex, unlockedSteps } } = state;
|
||||
const numOfSteps = description.length;
|
||||
const isLastStep = currentIndex + 1 >= numOfSteps;
|
||||
const stepFwd = currentIndex + 1;
|
||||
const stepBwd = currentIndex - 1;
|
||||
const isLastStep = stepFwd >= numOfSteps;
|
||||
if (type === types.completeAction) {
|
||||
return unlockStep(currentIndex, unlockedSteps);
|
||||
}
|
||||
if (type === types.stepForward) {
|
||||
if (isLastStep) {
|
||||
return submitChallenge();
|
||||
}
|
||||
return goToStep(currentIndex + 1);
|
||||
return goToStep(stepFwd, !!unlockedSteps[stepFwd]);
|
||||
}
|
||||
return goToStep(currentIndex - 1);
|
||||
if (type === types.stepBackward) {
|
||||
return goToStep(stepBwd, !!unlockedSteps[stepBwd]);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -32,7 +32,12 @@ test(file, function(t) {
|
|||
});
|
||||
t.test('steps back', t => {
|
||||
const actions = Observable.of({ type: types.stepBackward });
|
||||
const state = { challengesApp: { currentIndex: 1 } };
|
||||
const state = {
|
||||
challengesApp: {
|
||||
currentIndex: 1,
|
||||
unlockedSteps: [ true, undefined ] // eslint-disable-line no-undefined
|
||||
}
|
||||
};
|
||||
const onNextSpy = sinon.spy();
|
||||
challengeSelectorStub.challengeSelector = sinon.spy(_state => {
|
||||
t.assert(_state === state, 'challenge selector not called with state');
|
||||
|
@ -56,7 +61,7 @@ test(file, function(t) {
|
|||
t.assert(
|
||||
onNextSpy.calledWithMatch({
|
||||
type: types.goToStep,
|
||||
payload: 0
|
||||
payload: { step: 0, isUnlocked: true }
|
||||
}),
|
||||
'Epic did not return the expected action'
|
||||
);
|
||||
|
@ -67,7 +72,12 @@ test(file, function(t) {
|
|||
});
|
||||
t.test('steps forward', t => {
|
||||
const actions = Observable.of({ type: types.stepForward });
|
||||
const state = { challengesApp: { currentIndex: 0 } };
|
||||
const state = {
|
||||
challengesApp: {
|
||||
currentIndex: 0,
|
||||
unlockedSteps: []
|
||||
}
|
||||
};
|
||||
const onNextSpy = sinon.spy();
|
||||
challengeSelectorStub.challengeSelector = sinon.spy(_state => {
|
||||
t.assert(_state === state, 'challenge selector not called with state');
|
||||
|
@ -91,7 +101,7 @@ test(file, function(t) {
|
|||
t.assert(
|
||||
onNextSpy.calledWithMatch({
|
||||
type: types.goToStep,
|
||||
payload: 1
|
||||
payload: { step: 1, isUnlocked: false }
|
||||
}),
|
||||
'Epic did not return the expected action'
|
||||
);
|
||||
|
|
|
@ -8,6 +8,7 @@ export default createTypes([
|
|||
'completeAction',
|
||||
'openLightBoxImage',
|
||||
'closeLightBoxImage',
|
||||
'updateUnlockedSteps',
|
||||
|
||||
// challenges
|
||||
'fetchChallenge',
|
||||
|
|
Loading…
Reference in New Issue