* fix(files): Decouple files from challenges * feat(server/react): Remove action logger use redux remote devtools instead! * feat(Challenges): Disable js on edit, enable on execute * feat(Challenge/Preview): Show message when js is disabled * refactor(frameEpic): Reduce code by using lodash * feat(frameEpic): Disable js in preview by state * feat(frameEpic): Colocate epic in Challenges/redux * refactor(ExecuteChallengeEpic): CoLocated with Challenges * refactor(executeChallengesEpic): Separate tests from main logic * feat(Challenge/Preview): Update main on edit * feat(frameEpuc): Replace frame on edit/execute This allows for sandbox to work properly * fix(Challenges/Utils): Require utisl * revert(frameEpic): Hoist function to mount code in frame * fix(frameEpic): Ensure new frame is given classname * feat(executeChallenge): Update main on code unlocked * fix(frameEpic): Filter out empty test message * fix(Challenge/Preview): Remove unnessary quote in classname * feat(codeStorageEpic): Separate localstorage from solutions loading * fix(fetchUser): Merge user actions into one prefer many effects from one action over one action to one effect * fix(themes): Centralize theme utils and defs * fix(entities.user): Fix user reducer namespacing * feat(frame): Refactor frameEpic to util * feat(Challenges.redux): Should not attempt to update main from storage * fix(loadPreviousChallengeEpic): Refactor for RFR * fix(Challenges.Modern): Show preview plane
133 lines
4.2 KiB
JavaScript
133 lines
4.2 KiB
JavaScript
import _ from 'lodash';
|
|
import { Observable, Subject } from 'rx';
|
|
import { combineEpics, ofType } from 'redux-epic';
|
|
|
|
import {
|
|
types,
|
|
|
|
initOutput,
|
|
updateOutput,
|
|
updateTests,
|
|
checkChallenge,
|
|
|
|
codeLockedSelector,
|
|
showPreviewSelector,
|
|
testsSelector
|
|
} from './';
|
|
import {
|
|
buildFromFiles,
|
|
buildBackendChallenge
|
|
} from '../utils/build.js';
|
|
import {
|
|
runTestsInTestFrame,
|
|
createTestFramer,
|
|
createMainFramer
|
|
} from '../utils/frame.js';
|
|
import {
|
|
createErrorObservable,
|
|
|
|
challengeSelector
|
|
} from '../../../redux';
|
|
|
|
import { filesSelector } from '../../../files';
|
|
|
|
const executeDebounceTimeout = 750;
|
|
export function updateMainEpic(actions, { getState }, { document }) {
|
|
return Observable.of(document)
|
|
// if document is not defined then none of this epic will run
|
|
// this prevents issues during SSR
|
|
.filter(Boolean)
|
|
.flatMapLatest(() => {
|
|
const proxyLogger = new Subject();
|
|
const frameMain = createMainFramer(document, getState, proxyLogger);
|
|
const buildAndFrameMain = actions::ofType(
|
|
types.unlockUntrustedCode,
|
|
types.modernEditorUpdated,
|
|
types.classicEditorUpdated,
|
|
types.executeChallenge,
|
|
types.challengeUpdated
|
|
)
|
|
.debounce(executeDebounceTimeout)
|
|
// if isCodeLocked do not run challenges
|
|
.filter(() => (
|
|
!codeLockedSelector(getState()) &&
|
|
showPreviewSelector(getState())
|
|
))
|
|
.map(getState)
|
|
.flatMapLatest(state => {
|
|
const files = filesSelector(state);
|
|
const { required = [] } = challengeSelector(state);
|
|
return buildFromFiles(files, required, true)
|
|
.map(frameMain)
|
|
.ignoreElements()
|
|
.catch(createErrorObservable);
|
|
});
|
|
return Observable.merge(buildAndFrameMain, proxyLogger.map(updateOutput));
|
|
});
|
|
}
|
|
|
|
|
|
export function executeChallengeEpic(actions, { getState }, { document }) {
|
|
return Observable.of(document)
|
|
// if document is not defined then none of this epic will run
|
|
// this prevents issues during SSR
|
|
.filter(Boolean)
|
|
.flatMapLatest(() => {
|
|
const frameReady = new Subject();
|
|
const frameTests = createTestFramer(document, getState, frameReady);
|
|
const challengeResults = frameReady
|
|
.pluck('checkChallengePayload')
|
|
.map(checkChallengePayload => ({
|
|
checkChallengePayload,
|
|
tests: testsSelector(getState())
|
|
}))
|
|
.flatMap(({ checkChallengePayload, tests }) => {
|
|
const postTests = Observable.of(
|
|
updateOutput('// tests completed'),
|
|
checkChallenge(checkChallengePayload)
|
|
).delay(250);
|
|
// run the tests within the test iframe
|
|
return runTestsInTestFrame(document, tests)
|
|
.flatMap(tests => {
|
|
return Observable.from(tests)
|
|
.map(({ message }) => message)
|
|
// make sure that the test message is a non empty string
|
|
.filter(_.overEvery(_.isString, Boolean))
|
|
.map(updateOutput)
|
|
.concat(Observable.of(updateTests(tests)));
|
|
})
|
|
.concat(postTests);
|
|
});
|
|
const buildAndFrameChallenge = actions::ofType(types.executeChallenge)
|
|
.debounce(executeDebounceTimeout)
|
|
// if isCodeLocked do not run challenges
|
|
.filter(() => !codeLockedSelector(getState()))
|
|
.flatMapLatest(() => {
|
|
const state = getState();
|
|
const files = filesSelector(state);
|
|
const {
|
|
required = [],
|
|
type: challengeType
|
|
} = challengeSelector(state);
|
|
if (challengeType === 'backend') {
|
|
return buildBackendChallenge(state)
|
|
.do(frameTests)
|
|
.ignoreElements()
|
|
.startWith(initOutput('// running test'))
|
|
.catch(createErrorObservable);
|
|
}
|
|
return buildFromFiles(files, required, false)
|
|
.do(frameTests)
|
|
.ignoreElements()
|
|
.startWith(initOutput('// running test'))
|
|
.catch(createErrorObservable);
|
|
});
|
|
return Observable.merge(
|
|
buildAndFrameChallenge,
|
|
challengeResults
|
|
);
|
|
});
|
|
}
|
|
|
|
export default combineEpics(executeChallengeEpic, updateMainEpic);
|