fix(combineEpics): Compine new block epics in to one

This commit is contained in:
Stuart Taylor
2018-03-08 15:47:48 +00:00
parent 9f7084f7b7
commit 6965d04b80
15 changed files with 232 additions and 311 deletions

View File

@ -191,11 +191,11 @@ export default composeReducers(
() => ({
[
combineActions(
app.fetchChallenges.complete,
app.fetchNewBlock.complete,
map.fetchMapUi.complete
)
]: (state, { payload: { entities } }) => merge({}, state, entities),
[app.fetchChallenges.complete]:
[app.fetchNewBlock.complete]:
(state, { payload: { entities: { block }}}) => ({
...state,
fullBlocks: union(state.fullBlocks, [ Object.keys(block)[0] ])

View File

@ -1,62 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './overlayLoader-styles';
function LoaderCircle({ delay, origin }, i) {
return (
<circle
className='innerCircle'
cx='50%'
cy='50%'
fill='transparent'
key={ i }
r='5%'
stroke='#006400'
strokeWidth='3'
style={{ animationDelay: '' + delay, transformOrigin: '' + origin }}
/>
);
}
LoaderCircle.propTypes = {
delay: PropTypes.string.isRequired,
origin: PropTypes.string.isRequired
};
LoaderCircle.displayName = 'LoaderCircle';
const animationProps = [
{
delay: '0.24s',
origin: '0% 0%'
},
{
delay: '0.95s',
origin: '0% 100%'
},
{
delay: '0.67s',
origin: '100% 0%'
},
{
delay: '1.33s',
origin: '100% 100%'
}
];
function OverlayLoader() {
return (
<div className='svg-container'>
<style dangerouslySetInnerHTML={{ __html: styles }} />
<svg className='svg' height='100%' width='100%'>
{
animationProps.map(LoaderCircle)
}
</svg>
</div>
);
}
OverlayLoader.displayName = 'OverlayLoader';
export default OverlayLoader;

View File

@ -0,0 +1,26 @@
import React from 'react';
import styles from './skeletonStyles';
function SkeletonSprite() {
return (
<div className='sprite-container'>
<style dangerouslySetInnerHTML={ { __html: styles } } />
<svg className='sprite-svg'>
<rect
className='sprite'
fill='#ccc'
height='100%'
stroke='#ccc'
width='2px'
x='0'
y='0'
/>
</svg>
</div>
);
}
SkeletonSprite.displayName = 'SkeletonSprite';
export default SkeletonSprite;

View File

@ -1,5 +1,5 @@
export { default as ButtonSpacer } from './ButtonSpacer.jsx';
export { default as FullWidthRow } from './FullWidthRow.jsx';
export { default as Loader } from './Loader.jsx';
export { default as OverlayLoader } from './OverlayLoader.jsx';
export { default as SkeletonSprite } from './SkeletonSprite.jsx';
export { default as Spacer } from './Spacer.jsx';
export { default as ButtonSpacer } from './ButtonSpacer.jsx';

View File

@ -1,74 +0,0 @@
export default `
.svg-container {
position: fixed;
top: 0;
left: 0;
z-index:5;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
height: 100vh;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
}
.svg-container + div {
-webkit-filter: blur(5px);
filter: blur(5px);
}
@-webkit-keyframes overlay-loader {
0% {
-webkit-transform: scale(0.1);
transform: scale(0.1);
opacity: 0;
}
50% {
-webkit-transform: scale(0.8);
transform: scale(0.8);
opacity: 0.8;
}
70% {
opacity: 1;
}
100% {
opacity: 0;
-webkit-transform: scale(1.2);
transform: scale(1.2);
}
}
@keyframes overlay-loader {
0% {
-webkit-transform: scale(0.1);
transform: scale(0.1);
opacity: 0;
}
70% {
opacity: 1;
}
100% {
opacity: 0.0;
-webkit-transform: scale(1);
transform: scale(1);
}
}
.innerCircle {
-webkit-animation-duration: 2s;
animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
-webkit-animation-name: overlay-loader;
animation-name: overlay-loader;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
opacity: 0;
}
`;

View File

@ -0,0 +1,60 @@
export default `
.sprite-container {
height: 100%;
width: 100%;
}
.sprite-svg {
height: 100%;
width: 100%;
background: #aaa;
}
@-webkit-keyframes shimmer{
0% {
-webkit-transform: translateX(0%);
transform: translateX(0%);
stroke-width: 2px;
}
35% {
stroke-width: 30px;
}
100% {
-webkit-transform: translateX(100%);
transform: translateX(100%);
stroke-width: 2px;
}
}
@keyframes shimmer{
0% {
-webkit-transform: translateX(0%);
transform: translateX(0%);
stroke-width: 2px;
}
35% {
stroke-width: 30px;
}
100% {
-webkit-transform: translateX(100%);
transform: translateX(100%);
stroke-width: 2px;
}
}
.sprite {
-webkit-animation-name: shimmer;
animation-name: shimmer;
width: 2px;
-webkit-animation-duration: 2s;
animation-duration: 2s;
-webkit-animation-timing-function: ease-in-out;
animation-timing-function: ease-in-out;
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
-webkit-animation-direction: normal;
animation-direction: normal;
}
`;

View File

@ -1,5 +1,6 @@
import { Observable } from 'rx';
import { combineEpics, ofType } from 'redux-epic';
import _ from 'lodash';
import debug from 'debug';
import {
@ -9,8 +10,7 @@ import {
delayedRedirect,
fetchChallengeCompleted,
fetchChallengesCompleted,
fetchNewBlock,
fetchNewBlockComplete,
challengeSelector,
nextChallengeSelector
} from './';
@ -21,7 +21,7 @@ import {
import { shapeChallenges } from './utils';
import { types as challenge } from '../routes/Challenges/redux';
import { langSelector } from '../Router/redux';
import { langSelector, paramsSelector } from '../Router/redux';
const isDev = debug.enabled('fcc:*');
@ -60,61 +60,59 @@ export function fetchChallengesForBlockEpic(
{ getState },
{ services }
) {
return actions::ofType(
types.appMounted,
types.updateChallenges,
types.fetchNewBlock.start
)
.flatMapLatest(({ type, payload }) => {
const fetchAnotherBlock = type === types.fetchNewBlock.start;
const state = getState();
let {
block: blockName = 'basic-html-and-html5'
} = challengeSelector(state);
const lang = langSelector(state);
if (fetchAnotherBlock) {
const fullBlocks = fullBlocksSelector(state);
if (fullBlocks.includes(payload)) {
return Observable.of(null);
}
blockName = payload;
}
const onAppMount = actions::ofType(types.appMounted)
.map(() => {
const {
block = 'basic-html-and-html5'
} = challengeSelector(getState());
return block;
});
const onNewChallenge = actions::ofType(challenge.moveToNextChallenge)
.map(() => {
const {
isNewBlock,
isNewSuperBlock,
nextChallenge
} = nextChallengeSelector(getState());
const isNewBlockRequired = isNewBlock || isNewSuperBlock && nextChallenge;
return isNewBlockRequired ? nextChallenge.block : null;
});
const onBlockSelect = actions::ofType(types.fetchNewBlock.start)
.map(({ payload }) => payload);
return Observable.merge(onAppMount, onNewChallenge, onBlockSelect)
.filter(block => {
const fullBlocks = fullBlocksSelector(getState());
return block && !fullBlocks.includes(block);
})
.flatMapLatest(blockName => {
const lang = langSelector(getState());
const options = {
params: { lang, blockName },
service: 'challenge'
};
return services.readService$(options)
.retry(3)
.map(fetchChallengesCompleted)
.startWith({ type: types.fetchChallenges.start })
.catch(createErrorObservable);
})
.filter(Boolean);
}
function fetchChallengesForNextBlockEpic(action$, { getState }) {
return action$::ofType(challenge.checkForNextBlock)
.map(() => {
const {
nextChallenge,
isNewBlock,
isNewSuperBlock
} = nextChallengeSelector(getState());
const isNewBlockRequired = (
(isNewBlock || isNewSuperBlock) &&
nextChallenge &&
!nextChallenge.description
.map(newBlockData => {
const { dashedName } = paramsSelector(getState());
const { entities: { challenge } } = newBlockData;
const currentChallengeInNewBlock = _.pickBy(
challenge,
newChallenge => newChallenge.dashedName === dashedName
);
return isNewBlockRequired ?
fetchNewBlock(nextChallenge.block) :
null;
return fetchNewBlockComplete({
...newBlockData,
meta: {
challenge: currentChallengeInNewBlock
}
});
})
.filter(Boolean);
}
.catch(createErrorObservable);
});
}
export default combineEpics(
fetchChallengeEpic,
fetchChallengesForBlockEpic,
fetchChallengesForNextBlockEpic
fetchChallengesForBlockEpic
);

View File

@ -15,14 +15,18 @@ import updateMyCurrentChallengeEpic from './update-my-challenge-epic.js';
import fetchChallengesEpic from './fetch-challenges-epic.js';
import nightModeEpic from './night-mode-epic.js';
import { createFilesMetaCreator } from '../files';
import { updateThemeMetacreator, entitiesSelector } from '../entities';
import {
updateThemeMetacreator,
entitiesSelector,
fullBlocksSelector
} from '../entities';
import { utils } from '../Flash/redux';
import { paramsSelector } from '../Router/redux';
import { types as challenges } from '../routes/Challenges/redux';
import { types as map } from '../Map/redux';
import {
challengeToFiles,
createCurrentChallengeMeta,
challengeToFilesMetaCreator,
getFirstChallengeOfNextBlock,
getFirstChallengeOfNextSuperBlock,
getNextChallenge
@ -113,7 +117,7 @@ export const fetchChallengeCompleted = createAction(
null,
meta => ({
...meta,
...flow(challengeToFiles, createFilesMetaCreator)(meta.challenge)
...challengeToFilesMetaCreator(meta.challenge)
})
);
export const fetchChallenges = createAction('' + types.fetchChallenges);
@ -124,7 +128,8 @@ export const fetchChallengesCompleted = createAction(
export const fetchNewBlock = createAction(types.fetchNewBlock.start);
export const fetchNewBlockComplete = createAction(
types.fetchNewBlock.complete,
({ entities }) => entities
({ entities }) => ({ entities }),
({ meta: { challenge } }) => ({ ...createCurrentChallengeMeta(challenge) })
);
export const updateChallenges = createAction(types.updateChallenges);
@ -239,6 +244,12 @@ export const challengeSelector = state => {
return challengeMap[challengeName] || {};
};
export const isCurrentBlockCompleteSelector = state => {
const { block } = paramsSelector(state);
const fullBlocks = fullBlocksSelector(state);
return fullBlocks.includes(block);
};
export const previousSolutionSelector = state => {
const { id } = challengeSelector(state);
const { challengeMap = {} } = userSelector(state);

View File

@ -1,21 +1,45 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Col, Row } from 'react-bootstrap';
import ns from './ns.json';
import { isCurrentBlockCompleteSelector } from '../../redux';
import { SkeletonSprite } from '../../helperComponents';
const mapStateToProps = createSelector(
isCurrentBlockCompleteSelector,
blockComplete => ({
showLoading: !blockComplete
})
);
const propTypes = {
children: PropTypes.array
children: PropTypes.array,
showLoading: PropTypes.bool
};
export default function ChallengeDescription({ children }) {
function ChallengeDescription({ children, showLoading }) {
return (
<Row>
<Col
className={ `${ns}-instructions` }
xs={ 12 }
>
{ children }
{
showLoading ?
children
.map((_, i) => (
<div
key={ '' + i + 'description' }
style={{ height: '36px', margin: '9px 0px' }}
>
<SkeletonSprite />
</div>
)) :
children
}
</Col>
</Row>
);
@ -23,3 +47,5 @@ export default function ChallengeDescription({ children }) {
ChallengeDescription.displayName = 'ChallengeDescription';
ChallengeDescription.propTypes = propTypes;
export default connect(mapStateToProps)(ChallengeDescription);

View File

@ -1,15 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import ns from './ns.json';
import { isCurrentBlockCompleteSelector } from '../../redux';
import { SkeletonSprite } from '../../helperComponents';
const mapStateToProps = createSelector(
isCurrentBlockCompleteSelector,
blockComplete => ({
showLoading: !blockComplete
})
);
const propTypes = {
children: PropTypes.string,
isCompleted: PropTypes.bool
isCompleted: PropTypes.bool,
showLoading: PropTypes.bool
};
export default function ChallengeTitle({ children, isCompleted }) {
function ChallengeTitle({ children, isCompleted, showLoading }) {
let icon = null;
if (showLoading) {
return (
<h4 style={{ height: '35px', marginBottom: '9px' }}>
<SkeletonSprite />
<hr />
</h4>
);
}
if (isCompleted) {
icon = (
<i
@ -29,3 +48,5 @@ export default function ChallengeTitle({ children, isCompleted }) {
ChallengeTitle.displayName = 'ChallengeTitle';
ChallengeTitle.propTypes = propTypes;
export default connect(mapStateToProps)(ChallengeTitle);

View File

@ -1,57 +1,25 @@
import React, { PureComponent } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { challengeSelector } from '../../redux';
import { challengeUpdated } from './redux';
import CompletionModal from './Completion-Modal.jsx';
import AppChildContainer from '../../Child-Container.jsx';
import { OverlayLoader } from '../../helperComponents';
import { fullBlocksSelector } from '../../entities';
import { paramsSelector } from '../../Router/redux';
const mapStateToProps = createSelector(
challengeSelector,
fullBlocksSelector,
paramsSelector,
(challenge, fullBlocks, { block }) => ({
challenge,
showLoading: !fullBlocks.includes(block)
})
);
const mapDispatchToProps = { challengeUpdated };
const propTypes = {
challenge: PropTypes.object,
challengeUpdated: PropTypes.func.isRequired,
children: PropTypes.node,
showLoading: PropTypes.bool
};
class ChildContainer extends PureComponent {
componentDidUpdate(prevProps) {
const { challenge = {}, challengeUpdated } = this.props;
if (prevProps.showLoading && !this.props.showLoading) {
challengeUpdated(challenge);
}
}
render() {
const { children, showLoading, ...props } = this.props;
function ChildContainer(props) {
const { children, ...restProps } = props;
return (
<AppChildContainer { ...props }>
{
showLoading ? <OverlayLoader /> : null
}
<AppChildContainer { ...restProps }>
{ children }
<CompletionModal />
</AppChildContainer>
);
}
}
ChildContainer.propTypes = propTypes;
export default connect(mapStateToProps, mapDispatchToProps)(ChildContainer);
export default ChildContainer;

View File

@ -1,8 +1,6 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Grid, Col, Row } from 'react-bootstrap';
import ns from './ns.json';
import { SkeletonSprite } from '../../helperComponents';
const propTypes = {
content: PropTypes.string
@ -10,19 +8,6 @@ const propTypes = {
export default class CodeMirrorSkeleton extends PureComponent {
renderLine(line, i) {
return (
<div className={ `${ns}-shimmer` } key={ i }>
<Row>
<Col xs={ 12 }>
<div className='sprite-wrapper'>
<div className='sprite' />
</div>
</Col>
</Row>
</div>
);
}
render() {
const {
@ -39,18 +24,12 @@ export default class CodeMirrorSkeleton extends PureComponent {
className='CodeMirror-sizer'
style={
{
minHeight: (editorLines.length * 18) + 'px',
height: (editorLines.length * 18) + 'px',
overflow: 'hidden'
}
}
>
<div className='CodeMirror-lines'>
<div className='CodeMirror-code'>
<Grid>
{ editorLines.map(this.renderLine) }
</Grid>
</div>
</div>
<SkeletonSprite />
</div>
</div>
</div>

View File

@ -142,48 +142,6 @@
word-wrap: break-word;
}
@keyframes skeletonShimmer{
0% {
transform: translateX(-48px);
}
100% {
transform: translateX(1000px);
}
}
.@{ns}-shimmer {
position: relative;
min-height: 18px;
.row {
height: 18px;
.col-xs-12 {
padding-right: 12px;
height: 17px;
}
}
.sprite-wrapper {
background-color: #333;
height: 17px;
width: 75%;
}
.sprite {
animation-name: skeletonShimmer;
animation-duration: 2.5s;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-direction: normal;
background: white;
box-shadow: 0 0 3px 2px;
height: 17px;
width: 2px;
z-index: 5;
}
}
.@{ns}-success-modal {
display: flex;
flex-direction: column;

View File

@ -28,7 +28,8 @@ import {
submitTypes,
viewTypes,
getFileKey,
challengeToFiles
challengeToFilesMetaCreator
} from '../utils';
import {
types as app,
@ -36,14 +37,11 @@ import {
} from '../../../redux';
import { html } from '../../../utils/challengeTypes.js';
import blockNameify from '../../../utils/blockNameify.js';
import { updateFileMetaCreator, createFilesMetaCreator } from '../../../files';
import { updateFileMetaCreator } from '../../../files';
// this is not great but is ok until we move to a different form type
export projectNormalizer from '../views/project/redux';
const challengeToFilesMetaCreator =
_.flow(challengeToFiles, createFilesMetaCreator);
export const epics = [
modalEpic,
challengeEpic,

View File

@ -3,6 +3,7 @@ import _ from 'lodash';
import * as challengeTypes from '../../../utils/challengeTypes.js';
import { createPoly, updateFileFromSpec } from '../../../../utils/polyvinyl.js';
import { decodeScriptTags } from '../../../../utils/encode-decode.js';
import { createFilesMetaCreator } from '../../../files';
// turn challengeType to file ext
const pathsMap = {
@ -113,6 +114,17 @@ export function challengeToFiles(challenge, files) {
};
}
export const challengeToFilesMetaCreator =
_.flow(challengeToFiles, createFilesMetaCreator);
// ({ dashedName: { Challenge } }) => ({ meta: Files }) || {}
export function createCurrentChallengeMeta(challenge) {
if (_.isEmpty(challenge)) {
return {};
}
return challengeToFilesMetaCreator(_.values(challenge)[0]);
}
export function createTests({ tests = [] }) {
return tests
.map(test => {