Merge pull request #11322 from Bouncey/fix/Next-Step-Unlocked

Next step unlocked persistence
pull/11998/head
Berkeley Martinez 2017-01-07 13:42:38 -08:00 committed by GitHub
commit 09b62bcbcf
6 changed files with 99 additions and 39 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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;
});
}

View File

@ -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'
);

View File

@ -8,6 +8,7 @@ export default createTypes([
'completeAction',
'openLightBoxImage',
'closeLightBoxImage',
'updateUnlockedSteps',
// challenges
'fetchChallenge',