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