feat(updates): Store updates if the server is not responding
This commit is contained in:
committed by
Mrugesh Mohapatra
parent
54e14b9433
commit
e4e41e6fe3
@ -73,9 +73,10 @@
|
|||||||
"lint:src": "eslint ./src . --fix",
|
"lint:src": "eslint ./src . --fix",
|
||||||
"lint:utils": "eslint ./utils . --fix",
|
"lint:utils": "eslint ./utils . --fix",
|
||||||
"pretty": "yarn format && yarn lint",
|
"pretty": "yarn format && yarn lint",
|
||||||
"test": "jest src",
|
"test": "jest src && jest utils",
|
||||||
"test:ci": "yarn format && jest src",
|
"test:ci": "yarn format && jest src",
|
||||||
"test:watch": "jest --watch src"
|
"test:src": "jest src",
|
||||||
|
"test:watch-src": "jest --watch src"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.0.0",
|
"@babel/cli": "^7.0.0",
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import './offline-warning.css';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
isOnline: PropTypes.bool.isRequired,
|
||||||
|
isSignedIn: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
function OfflineWarning({ isOnline, isSignedIn }) {
|
||||||
|
return !isSignedIn || isOnline ? null : (
|
||||||
|
<div className='offline-warning'>
|
||||||
|
We cannot reach the server to update your progress.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OfflineWarning.displayName = 'OfflineWarning';
|
||||||
|
OfflineWarning.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default OfflineWarning;
|
1
packages/learn/src/components/OfflineWarning/index.js
Normal file
1
packages/learn/src/components/OfflineWarning/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './OfflineWarning';
|
@ -0,0 +1,7 @@
|
|||||||
|
.offline-warning {
|
||||||
|
height: 38px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: lightblue;
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Fragment, PureComponent } from 'react';
|
import React, { Fragment, Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
@ -8,13 +8,21 @@ import ga from '../analytics';
|
|||||||
|
|
||||||
import Header from '../components/Header';
|
import Header from '../components/Header';
|
||||||
import DonationModal from '../components/Donation';
|
import DonationModal from '../components/Donation';
|
||||||
import { fetchUser, userSelector } from '../redux/app';
|
import OfflineWarning from '../components/OfflineWarning';
|
||||||
|
import {
|
||||||
|
fetchUser,
|
||||||
|
userSelector,
|
||||||
|
onlineStatusChange,
|
||||||
|
isOnlineSelector,
|
||||||
|
isSignedInSelector
|
||||||
|
} from '../redux/app';
|
||||||
|
|
||||||
import 'prismjs/themes/prism.css';
|
import 'prismjs/themes/prism.css';
|
||||||
import 'react-reflex/styles.css';
|
import 'react-reflex/styles.css';
|
||||||
import './global.css';
|
import './global.css';
|
||||||
import './layout.css';
|
import './layout.css';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import { isBrowser } from '../../utils';
|
||||||
|
|
||||||
const metaKeywords = [
|
const metaKeywords = [
|
||||||
'javascript',
|
'javascript',
|
||||||
@ -42,46 +50,72 @@ const metaKeywords = [
|
|||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
userSelector,
|
userSelector,
|
||||||
({ theme = 'default' }) => ({ theme })
|
isSignedInSelector,
|
||||||
|
isOnlineSelector,
|
||||||
|
({ theme = 'default' }, isSignedIn, isOnline) => ({
|
||||||
|
theme,
|
||||||
|
isSignedIn,
|
||||||
|
isOnline
|
||||||
|
})
|
||||||
);
|
);
|
||||||
const mapDispatchToProps = dispatch =>
|
const mapDispatchToProps = dispatch =>
|
||||||
bindActionCreators({ fetchUser }, dispatch);
|
bindActionCreators({ fetchUser, onlineStatusChange }, dispatch);
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
children: PropTypes.object,
|
children: PropTypes.object,
|
||||||
fetchUser: PropTypes.func.isRequired,
|
fetchUser: PropTypes.func.isRequired,
|
||||||
|
isOnline: PropTypes.bool.isRequired,
|
||||||
|
isSignedIn: PropTypes.bool.isRequired,
|
||||||
|
onlineStatusChange: PropTypes.func.isRequired,
|
||||||
theme: PropTypes.string
|
theme: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
class Layout extends PureComponent {
|
class Layout extends Component {
|
||||||
state = {
|
state = {
|
||||||
location: ''
|
location: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.fetchUser();
|
this.props.fetchUser();
|
||||||
const url = window.location.pathname + window.location.search;
|
const url = window.location.pathname + window.location.search;
|
||||||
ga.pageview(url);
|
ga.pageview(url);
|
||||||
|
|
||||||
|
window.addEventListener('online', this.updateOnlineStatus);
|
||||||
|
window.addEventListener('offline', this.updateOnlineStatus);
|
||||||
|
|
||||||
/* eslint-disable react/no-did-mount-set-state */
|
/* eslint-disable react/no-did-mount-set-state */
|
||||||
// this is for local location tracking only, no re-rendering required
|
// this is for local location tracking only, no re-rendering required
|
||||||
this.setState(state => ({
|
this.setState({
|
||||||
...state,
|
|
||||||
location: url
|
location: url
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
const url = window.location.pathname + window.location.search;
|
const url = window.location.pathname + window.location.search;
|
||||||
if (url !== this.state.location) {
|
if (url !== this.state.location) {
|
||||||
ga.pageview(url);
|
ga.pageview(url);
|
||||||
/* eslint-disable react/no-did-update-set-state */
|
/* eslint-disable react/no-did-update-set-state */
|
||||||
// this is for local location tracking only, no re-rendering required
|
// this is for local location tracking only, no re-rendering required
|
||||||
this.setState(state => ({
|
this.setState({
|
||||||
...state,
|
|
||||||
location: url
|
location: url
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener('online', this.updateOnlineStatus);
|
||||||
|
window.removeEventListener('offline', this.updateOnlineStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOnlineStatus = () => {
|
||||||
|
const { onlineStatusChange } = this.props;
|
||||||
|
const isOnline =
|
||||||
|
isBrowser() && 'navigator' in window ? window.navigator.onLine : null;
|
||||||
|
return typeof isOnline === 'boolean' ? onlineStatusChange(isOnline) : null;
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { children, theme } = this.props;
|
const { children, theme, isOnline, isSignedIn } = this.props;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Helmet
|
<Helmet
|
||||||
@ -97,6 +131,7 @@ class Layout extends PureComponent {
|
|||||||
/>
|
/>
|
||||||
<Header />
|
<Header />
|
||||||
<div className={'app-wrapper ' + theme}>
|
<div className={'app-wrapper ' + theme}>
|
||||||
|
<OfflineWarning isOnline={isOnline} isSignedIn={isSignedIn} />
|
||||||
<main>{children}</main>
|
<main>{children}</main>
|
||||||
</div>
|
</div>
|
||||||
<DonationModal />
|
<DonationModal />
|
||||||
|
74
packages/learn/src/redux/app/failed-updates-epic.js
Normal file
74
packages/learn/src/redux/app/failed-updates-epic.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { merge } from 'rxjs/observable/merge';
|
||||||
|
import { empty } from 'rxjs/observable/empty';
|
||||||
|
import {
|
||||||
|
tap,
|
||||||
|
filter,
|
||||||
|
map,
|
||||||
|
ignoreElements,
|
||||||
|
switchMap,
|
||||||
|
catchError
|
||||||
|
} from 'rxjs/operators';
|
||||||
|
import { ofType } from 'redux-observable';
|
||||||
|
import store from 'store';
|
||||||
|
import uuid from 'uuid/v4';
|
||||||
|
|
||||||
|
import { types, onlineStatusChange, isOnlineSelector } from './';
|
||||||
|
import postUpdate$ from '../../templates/Challenges/utils/postUpdate$';
|
||||||
|
|
||||||
|
const key = 'fcc-failed-updates';
|
||||||
|
|
||||||
|
function delay(time = 0, fn) {
|
||||||
|
return setTimeout(fn, time);
|
||||||
|
}
|
||||||
|
|
||||||
|
function failedUpdateEpic(action$, { getState }) {
|
||||||
|
const storeUpdates = action$.pipe(
|
||||||
|
ofType(types.updateFailed),
|
||||||
|
tap(({ payload }) => {
|
||||||
|
const failures = store.get(key) || [];
|
||||||
|
payload.id = uuid();
|
||||||
|
store.set(key, [...failures, payload]);
|
||||||
|
}),
|
||||||
|
map(() => onlineStatusChange(false))
|
||||||
|
);
|
||||||
|
|
||||||
|
const flushUpdates = action$.pipe(
|
||||||
|
ofType(types.fetchUserComplete, types.updateComplete),
|
||||||
|
filter(() => store.get(key)),
|
||||||
|
filter(() => isOnlineSelector(getState())),
|
||||||
|
tap(() => {
|
||||||
|
const failures = store.get(key) || [];
|
||||||
|
let delayTime = 0;
|
||||||
|
const batch = failures.map(update => {
|
||||||
|
delayTime += 300;
|
||||||
|
// we stagger the updates here so we don't hammer the server
|
||||||
|
return delay(delayTime, () =>
|
||||||
|
postUpdate$(update)
|
||||||
|
.pipe(
|
||||||
|
switchMap(response => {
|
||||||
|
if (response && response.message) {
|
||||||
|
// the request completed successfully
|
||||||
|
const failures = store.get(key) || [];
|
||||||
|
const newFailures = failures.filter(x => x.id !== update.id);
|
||||||
|
store.set(key, newFailures);
|
||||||
|
}
|
||||||
|
return empty();
|
||||||
|
}),
|
||||||
|
catchError(() => empty())
|
||||||
|
)
|
||||||
|
.toPromise()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Promise.all(batch)
|
||||||
|
.then(() => console.info('progress updates processed where possible'))
|
||||||
|
.catch(err =>
|
||||||
|
console.warn('unable to process progress updates', err.message)
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
ignoreElements()
|
||||||
|
);
|
||||||
|
|
||||||
|
return merge(storeUpdates, flushUpdates);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default failedUpdateEpic;
|
@ -5,10 +5,17 @@ import { createTypes } from '../../../utils/stateManagement';
|
|||||||
import { types as challenge } from '../../templates/Challenges/redux';
|
import { types as challenge } from '../../templates/Challenges/redux';
|
||||||
import fetchUserEpic from './fetch-user-epic';
|
import fetchUserEpic from './fetch-user-epic';
|
||||||
import hardGoToEpic from './hard-go-to-epic';
|
import hardGoToEpic from './hard-go-to-epic';
|
||||||
|
import failedUpdatesEpic from './failed-updates-epic';
|
||||||
|
import updateCompleteEpic from './update-complete-epic';
|
||||||
|
|
||||||
const ns = 'app';
|
const ns = 'app';
|
||||||
|
|
||||||
export const epics = [fetchUserEpic, hardGoToEpic];
|
export const epics = [
|
||||||
|
fetchUserEpic,
|
||||||
|
hardGoToEpic,
|
||||||
|
failedUpdatesEpic,
|
||||||
|
updateCompleteEpic
|
||||||
|
];
|
||||||
|
|
||||||
export const types = createTypes(
|
export const types = createTypes(
|
||||||
[
|
[
|
||||||
@ -19,7 +26,12 @@ export const types = createTypes(
|
|||||||
'hardGoTo',
|
'hardGoTo',
|
||||||
'updateUserSignedIn',
|
'updateUserSignedIn',
|
||||||
'openDonationModal',
|
'openDonationModal',
|
||||||
'closeDonationModal'
|
'closeDonationModal',
|
||||||
|
|
||||||
|
'onlineStatusChange',
|
||||||
|
|
||||||
|
'updateComplete',
|
||||||
|
'updateFailed'
|
||||||
],
|
],
|
||||||
ns
|
ns
|
||||||
);
|
);
|
||||||
@ -30,7 +42,8 @@ const initialState = {
|
|||||||
showLoading: true,
|
showLoading: true,
|
||||||
isSignedIn: false,
|
isSignedIn: false,
|
||||||
user: {},
|
user: {},
|
||||||
showDonationModal: false
|
showDonationModal: false,
|
||||||
|
isOnline: true
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchUser = createAction(types.fetchUser);
|
export const fetchUser = createAction(types.fetchUser);
|
||||||
@ -45,9 +58,17 @@ export const closeDonationModal = createAction(types.closeDonationModal);
|
|||||||
|
|
||||||
export const updateUserSignedIn = createAction(types.updateUserSignedIn);
|
export const updateUserSignedIn = createAction(types.updateUserSignedIn);
|
||||||
|
|
||||||
|
export const onlineStatusChange = createAction(types.onlineStatusChange);
|
||||||
|
|
||||||
|
export const updateComplete = createAction(types.updateComplete);
|
||||||
|
export const updateFailed = createAction(types.updateFailed);
|
||||||
|
|
||||||
export const completionCountSelector = state => state[ns].completionCount;
|
export const completionCountSelector = state => state[ns].completionCount;
|
||||||
export const isDonationModalOpenSelector = state => state[ns].showDonationModal;
|
export const isDonationModalOpenSelector = state => state[ns].showDonationModal;
|
||||||
export const isSignedInSelector = state => state[ns].isSignedIn;
|
export const isSignedInSelector = state => state[ns].isSignedIn;
|
||||||
|
|
||||||
|
export const isOnlineSelector = state => state[ns].isOnline;
|
||||||
|
|
||||||
export const userSelector = state => state[ns].user || {};
|
export const userSelector = state => state[ns].user || {};
|
||||||
export const userStateLoadingSelector = state => state[ns].showLoading;
|
export const userStateLoadingSelector = state => state[ns].showLoading;
|
||||||
export const completedChallengesSelector = state =>
|
export const completedChallengesSelector = state =>
|
||||||
@ -100,6 +121,12 @@ export const reducer = handleActions(
|
|||||||
...state,
|
...state,
|
||||||
isSignedIn: payload
|
isSignedIn: payload
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
[types.onlineStatusChange]: (state, { payload: isOnline }) => ({
|
||||||
|
...state,
|
||||||
|
isOnline
|
||||||
|
}),
|
||||||
|
|
||||||
[challenge.submitComplete]: (state, { payload: { id } }) => ({
|
[challenge.submitComplete]: (state, { payload: { id } }) => ({
|
||||||
...state,
|
...state,
|
||||||
completionCount: state.completionCount + 1,
|
completionCount: state.completionCount + 1,
|
||||||
|
12
packages/learn/src/redux/app/update-complete-epic.js
Normal file
12
packages/learn/src/redux/app/update-complete-epic.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { ofType } from 'redux-observable';
|
||||||
|
import { mapTo, filter } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { types, onlineStatusChange, isOnlineSelector } from './';
|
||||||
|
|
||||||
|
export default function updateCompleteEpic(action$, { getState }) {
|
||||||
|
return action$.pipe(
|
||||||
|
ofType(types.updateComplete),
|
||||||
|
filter(() => !isOnlineSelector(getState())),
|
||||||
|
mapTo(onlineStatusChange(true))
|
||||||
|
);
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
import { of } from 'rxjs/observable/of';
|
import { of } from 'rxjs/observable/of';
|
||||||
|
import { _if } from 'rxjs/observable/if';
|
||||||
import { empty } from 'rxjs/observable/empty';
|
import { empty } from 'rxjs/observable/empty';
|
||||||
import {
|
import {
|
||||||
switchMap,
|
switchMap,
|
||||||
retry,
|
retry,
|
||||||
map,
|
|
||||||
catchError,
|
catchError,
|
||||||
concat,
|
concat,
|
||||||
filter,
|
filter,
|
||||||
@ -12,8 +12,6 @@ import {
|
|||||||
import { ofType } from 'redux-observable';
|
import { ofType } from 'redux-observable';
|
||||||
import { navigate } from 'gatsby';
|
import { navigate } from 'gatsby';
|
||||||
|
|
||||||
import { _csrf as csrfToken } from '../../../redux/cookieValues';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
backendFormValuesSelector,
|
backendFormValuesSelector,
|
||||||
projectFormValuesSelector,
|
projectFormValuesSelector,
|
||||||
@ -29,26 +27,34 @@ import {
|
|||||||
userSelector,
|
userSelector,
|
||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
openDonationModal,
|
openDonationModal,
|
||||||
shouldShowDonationSelector
|
shouldShowDonationSelector,
|
||||||
|
updateComplete,
|
||||||
|
updateFailed,
|
||||||
|
isOnlineSelector
|
||||||
} from '../../../redux/app';
|
} from '../../../redux/app';
|
||||||
|
|
||||||
import { postJSON$ } from '../utils/ajax-stream';
|
import postUpdate$ from '../utils/postUpdate$';
|
||||||
import { challengeTypes, submitTypes } from '../../../../utils/challengeTypes';
|
import { challengeTypes, submitTypes } from '../../../../utils/challengeTypes';
|
||||||
|
|
||||||
function postChallenge(url, username, _csrf, challengeInfo) {
|
function postChallenge(update, username) {
|
||||||
const body = { ...challengeInfo, _csrf };
|
const saveChallenge = postUpdate$(update).pipe(
|
||||||
const saveChallenge = postJSON$(url, body).pipe(
|
|
||||||
retry(3),
|
retry(3),
|
||||||
map(({ points }) =>
|
switchMap(({ points }) =>
|
||||||
|
of(
|
||||||
submitComplete({
|
submitComplete({
|
||||||
username,
|
username,
|
||||||
points,
|
points,
|
||||||
...challengeInfo
|
...update.payload
|
||||||
})
|
}),
|
||||||
|
updateComplete()
|
||||||
|
)
|
||||||
),
|
),
|
||||||
catchError(err => {
|
catchError(({ _body, _endpoint }) => {
|
||||||
console.error(err);
|
let payload = _body;
|
||||||
return of({ type: 'here is an error' });
|
if (typeof _body === 'string') {
|
||||||
|
payload = JSON.parse(_body);
|
||||||
|
}
|
||||||
|
return of(updateFailed({ endpoint: _endpoint, payload }));
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return saveChallenge;
|
return saveChallenge;
|
||||||
@ -65,14 +71,18 @@ function submitModern(type, state) {
|
|||||||
const { id } = challengeMetaSelector(state);
|
const { id } = challengeMetaSelector(state);
|
||||||
const files = challengeFilesSelector(state);
|
const files = challengeFilesSelector(state);
|
||||||
const { username } = userSelector(state);
|
const { username } = userSelector(state);
|
||||||
return postChallenge(
|
const challengeInfo = {
|
||||||
'/external/modern-challenge-completed',
|
|
||||||
username,
|
|
||||||
csrfToken,
|
|
||||||
{
|
|
||||||
id,
|
id,
|
||||||
files
|
files
|
||||||
}
|
};
|
||||||
|
const update = {
|
||||||
|
endpoint: '/external/modern-challenge-completed',
|
||||||
|
payload: challengeInfo
|
||||||
|
};
|
||||||
|
return _if(
|
||||||
|
() => isOnlineSelector(state),
|
||||||
|
postChallenge(update, username),
|
||||||
|
of(updateFailed(update))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -91,13 +101,17 @@ function submitProject(type, state) {
|
|||||||
if (challengeType === challengeTypes.backEndProject) {
|
if (challengeType === challengeTypes.backEndProject) {
|
||||||
challengeInfo.githubLink = githubLink;
|
challengeInfo.githubLink = githubLink;
|
||||||
}
|
}
|
||||||
return postChallenge(
|
|
||||||
'/external/project-completed',
|
const update = {
|
||||||
username,
|
endpoint: '/external/project-completed',
|
||||||
csrfToken,
|
payload: challengeInfo
|
||||||
challengeInfo
|
};
|
||||||
).pipe(
|
return _if(
|
||||||
|
() => isOnlineSelector(state),
|
||||||
|
postChallenge(update, username).pipe(
|
||||||
concat(of(updateProjectFormValues({})))
|
concat(of(updateProjectFormValues({})))
|
||||||
|
),
|
||||||
|
of(updateFailed(update))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,11 +125,15 @@ function submitBackendChallenge(type, state) {
|
|||||||
state
|
state
|
||||||
);
|
);
|
||||||
const challengeInfo = { id, solution };
|
const challengeInfo = { id, solution };
|
||||||
return postChallenge(
|
|
||||||
'/external/backend-challenge-completed',
|
const update = {
|
||||||
username,
|
endpoint: '/external/backend-challenge-completed',
|
||||||
csrfToken,
|
payload: challengeInfo
|
||||||
challengeInfo
|
};
|
||||||
|
return _if(
|
||||||
|
() => isOnlineSelector(state),
|
||||||
|
postChallenge(update, username),
|
||||||
|
of(updateFailed(update))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,43 @@
|
|||||||
|
import { _if } from 'rxjs/observable/if';
|
||||||
|
import { of } from 'rxjs/observable/of';
|
||||||
import { ofType } from 'redux-observable';
|
import { ofType } from 'redux-observable';
|
||||||
|
|
||||||
import { types } from './';
|
import { types } from './';
|
||||||
import { filter, switchMap, catchError, mapTo } from 'rxjs/operators';
|
import { filter, switchMap, catchError, mapTo } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
currentChallengeIdSelector
|
currentChallengeIdSelector,
|
||||||
|
updateComplete,
|
||||||
|
updateFailed,
|
||||||
|
isOnlineSelector
|
||||||
} from '../../../redux/app';
|
} from '../../../redux/app';
|
||||||
import { postJSON$ } from '../utils/ajax-stream';
|
import postUpdate$ from '../utils/postUpdate$';
|
||||||
import { _csrf } from '../../../redux/cookieValues';
|
|
||||||
import { of } from 'rxjs/observable/of';
|
|
||||||
|
|
||||||
function currentChallengeEpic(action$, { getState }) {
|
function currentChallengeEpic(action$, { getState }) {
|
||||||
return action$.pipe(
|
return action$.pipe(
|
||||||
ofType(types.challengeMounted),
|
ofType(types.challengeMounted),
|
||||||
filter(() => isSignedInSelector(getState())),
|
filter(() => isSignedInSelector(getState())),
|
||||||
filter(({ payload }) => payload !== currentChallengeIdSelector(getState())),
|
filter(({ payload }) => payload !== currentChallengeIdSelector(getState())),
|
||||||
switchMap(({ payload }) =>
|
switchMap(({ payload }) => {
|
||||||
postJSON$('/external/update-my-current-challenge', {
|
const update = {
|
||||||
currentChallengeId: payload,
|
endpoint: '/external/update-my-current-challenge',
|
||||||
_csrf
|
payload: {
|
||||||
|
currentChallengeId: payload
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return _if(
|
||||||
|
() => isOnlineSelector(getState()),
|
||||||
|
postUpdate$(update).pipe(mapTo(updateComplete())),
|
||||||
|
of(updateFailed(update))
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
catchError(({ _body, _endpoint }) => {
|
||||||
|
let payload = _body;
|
||||||
|
if (typeof _body === 'string') {
|
||||||
|
payload = JSON.parse(_body);
|
||||||
|
}
|
||||||
|
return of(updateFailed({ endpoint: _endpoint, payload }));
|
||||||
})
|
})
|
||||||
),
|
|
||||||
mapTo({ type: 'currentChallengeUpdateComplete' }),
|
|
||||||
catchError(() => of({ type: 'current-challenge-update-error' }))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,15 +93,24 @@ function normalizeAjaxSuccessEvent(e, xhr, settings) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeAjaxErrorEvent(e, xhr, type) {
|
function createNormalizeAjaxErrorEvent(options) {
|
||||||
return {
|
let _body = {};
|
||||||
|
let _endpoint = '';
|
||||||
|
if (typeof options === 'string') {
|
||||||
|
_endpoint = options;
|
||||||
|
} else {
|
||||||
|
_body = options.body;
|
||||||
|
_endpoint = options.url;
|
||||||
|
}
|
||||||
|
return (e, xhr, type) => ({
|
||||||
|
_body,
|
||||||
|
_endpoint,
|
||||||
type: type,
|
type: type,
|
||||||
status: xhr.status,
|
status: xhr.status,
|
||||||
xhr: xhr,
|
xhr: xhr,
|
||||||
originalEvent: e
|
originalEvent: e
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Creates an observable for an Ajax request with either a settings object
|
* Creates an observable for an Ajax request with either a settings object
|
||||||
* with url, headers, etc or a string for a URL.
|
* with url, headers, etc or a string for a URL.
|
||||||
@ -131,7 +140,7 @@ export function ajax$(options) {
|
|||||||
createXHR: function() {
|
createXHR: function() {
|
||||||
return this.crossDomain ? getCORSRequest() : getXMLHttpRequest();
|
return this.crossDomain ? getCORSRequest() : getXMLHttpRequest();
|
||||||
},
|
},
|
||||||
normalizeError: normalizeAjaxErrorEvent,
|
normalizeError: createNormalizeAjaxErrorEvent(options),
|
||||||
normalizeSuccess: normalizeAjaxSuccessEvent
|
normalizeSuccess: normalizeAjaxSuccessEvent
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -144,9 +153,7 @@ export function ajax$(options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const { normalizeError, normalizeSuccess } = settings;
|
||||||
var normalizeError = settings.normalizeError;
|
|
||||||
var normalizeSuccess = settings.normalizeSuccess;
|
|
||||||
|
|
||||||
if (!settings.crossDomain && !settings.headers['X-Requested-With']) {
|
if (!settings.crossDomain && !settings.headers['X-Requested-With']) {
|
||||||
settings.headers['X-Requested-With'] = 'XMLHttpRequest';
|
settings.headers['X-Requested-With'] = 'XMLHttpRequest';
|
||||||
@ -157,6 +164,12 @@ export function ajax$(options) {
|
|||||||
var isDone = false;
|
var isDone = false;
|
||||||
var xhr;
|
var xhr;
|
||||||
|
|
||||||
|
const throwWithMeta = err => {
|
||||||
|
err._body = options.body || {};
|
||||||
|
err._endpoint = options.url;
|
||||||
|
observer.error(err);
|
||||||
|
};
|
||||||
|
|
||||||
var processResponse = function(xhr, e) {
|
var processResponse = function(xhr, e) {
|
||||||
var status = xhr.status === 1223 ? 204 : xhr.status;
|
var status = xhr.status === 1223 ? 204 : xhr.status;
|
||||||
if ((status >= 200 && status <= 300) || status === 0 || status === '') {
|
if ((status >= 200 && status <= 300) || status === 0 || status === '') {
|
||||||
@ -164,7 +177,7 @@ export function ajax$(options) {
|
|||||||
observer.next(normalizeSuccess(e, xhr, settings));
|
observer.next(normalizeSuccess(e, xhr, settings));
|
||||||
observer.complete();
|
observer.complete();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
observer.error(err);
|
throwWithMeta(err);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
observer.error(normalizeError(e, xhr, 'error'));
|
observer.error(normalizeError(e, xhr, 'error'));
|
||||||
@ -175,7 +188,7 @@ export function ajax$(options) {
|
|||||||
try {
|
try {
|
||||||
xhr = settings.createXHR();
|
xhr = settings.createXHR();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
observer.error(err);
|
throwWithMeta(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -239,7 +252,7 @@ export function ajax$(options) {
|
|||||||
debug('ajax$ sending content', settings.hasContent && settings.body);
|
debug('ajax$ sending content', settings.hasContent && settings.body);
|
||||||
xhr.send((settings.hasContent && settings.body) || null);
|
xhr.send((settings.hasContent && settings.body) || null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
observer.error(err);
|
throwWithMeta(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
return function() {
|
return function() {
|
||||||
|
@ -19,7 +19,7 @@ import { createFileStream, pipe } from './polyvinyl';
|
|||||||
const jQuery = {
|
const jQuery = {
|
||||||
src: 'https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js'
|
src: 'https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js'
|
||||||
};
|
};
|
||||||
const frameRunner = {
|
export const frameRunner = {
|
||||||
src: '/js/frame-runner.js',
|
src: '/js/frame-runner.js',
|
||||||
crossDomain: false,
|
crossDomain: false,
|
||||||
cacheBreaker: true
|
cacheBreaker: true
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
import { postJSON$ } from './ajax-stream';
|
||||||
|
|
||||||
|
export default function postUpdate$({ endpoint, payload }) {
|
||||||
|
return postJSON$(endpoint, payload);
|
||||||
|
}
|
@ -23,3 +23,7 @@ exports.unDasherize = function unDasherize(name) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
exports.descriptionRegex = /\<blockquote|\<ol|\<h4|\<table/;
|
exports.descriptionRegex = /\<blockquote|\<ol|\<h4|\<table/;
|
||||||
|
|
||||||
|
exports.isBrowser = function isBrowser() {
|
||||||
|
return typeof window !== 'undefined';
|
||||||
|
};
|
||||||
|
Reference in New Issue
Block a user