From 056d749ddd27541503cbeba28e7ddb8eb781c471 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 25 Feb 2016 18:30:10 -0800 Subject: [PATCH] Wrap up flux actions --- client/sagas/local-storage-saga.js | 67 ++ common/app/routes/Hikes/redux/types.js | 6 +- common/app/routes/Jobs/components/Jobs.jsx | 3 +- common/app/routes/Jobs/components/NewJob.jsx | 715 +++++++++--------- common/app/routes/Jobs/components/Preview.jsx | 7 +- common/app/routes/Jobs/components/Show.jsx | 3 +- common/app/routes/Jobs/index.js | 4 +- common/app/routes/Jobs/redux/actions.js | 20 + .../app/routes/Jobs/redux/apply-promo-saga.js | 39 + .../app/routes/Jobs/redux/fetch-jobs-saga.js | 49 +- common/app/routes/Jobs/redux/index.js | 9 +- common/app/routes/Jobs/redux/reducer.js | 79 ++ common/app/routes/Jobs/redux/save-job-saga.js | 26 + common/app/routes/Jobs/redux/types.js | 17 +- common/app/sagas.js | 5 +- 15 files changed, 655 insertions(+), 394 deletions(-) create mode 100644 client/sagas/local-storage-saga.js create mode 100644 common/app/routes/Jobs/redux/apply-promo-saga.js create mode 100644 common/app/routes/Jobs/redux/save-job-saga.js diff --git a/client/sagas/local-storage-saga.js b/client/sagas/local-storage-saga.js new file mode 100644 index 0000000000..7913dcf42f --- /dev/null +++ b/client/sagas/local-storage-saga.js @@ -0,0 +1,67 @@ +import { + saveForm, + clearForm, + loadSavedForm +} from '../common/app/routes/Jobs/redux/types'; + +import { + loadSavedFormCompleted +} from '../common/app/routes/Jobs/redux/actions'; + +const formKey = 'newJob'; +let enabled = false; +let store = typeof window !== 'undefined' ? + window.localStorage : + false; + +try { + const testKey = '__testKey__'; + store.setItem(testKey, testKey); + enabled = store.getItem(testKey) !== testKey; + store.removeItem(testKey); +} catch (e) { + enabled = !e; +} + +if (!enabled) { + console.error(new Error('No localStorage found')); +} + +export default () => ({ dispatch }) => next => { + return function localStorageSaga(action) { + if (!enabled) { return next(action); } + + if (action.type === saveForm) { + const form = action.payload; + try { + store.setItem(formKey, JSON.stringify(form)); + return null; + } catch (e) { + return dispatch({ + type: 'app.handleError', + error: new Error('could not parse form data') + }); + } + } + + if (action.type === clearForm) { + store.removeItem(formKey); + return null; + } + + if (action.type === loadSavedForm) { + const formString = store.getItem(formKey); + try { + const form = JSON.parse(formString); + return dispatch(loadSavedFormCompleted(form)); + } catch (err) { + return dispatch({ + type: 'app.handleError', + error: new Error('could not parse form data') + }); + } + } + + return next(action); + }; +}; diff --git a/common/app/routes/Hikes/redux/types.js b/common/app/routes/Hikes/redux/types.js index 2e51441926..da3a622adc 100644 --- a/common/app/routes/Hikes/redux/types.js +++ b/common/app/routes/Hikes/redux/types.js @@ -21,5 +21,7 @@ const types = [ 'goToNextHike' ]; -export default types - .reduce((types, type) => ({ ...types, [type]: `videos.${type}` }), {}); +export default types.reduce((types, type) => { + types[type] = `videos.${type}`; + return types; +}, {}); diff --git a/common/app/routes/Jobs/components/Jobs.jsx b/common/app/routes/Jobs/components/Jobs.jsx index a315227d70..bea7d205b0 100644 --- a/common/app/routes/Jobs/components/Jobs.jsx +++ b/common/app/routes/Jobs/components/Jobs.jsx @@ -1,5 +1,6 @@ import React, { cloneElement, PropTypes } from 'react'; -import { connect, compose } from 'redux'; +import { compose } from 'redux'; +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { push } from 'react-router-redux'; diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index 43f2dfb6ef..1111057def 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -1,8 +1,8 @@ import { helpers } from 'rx'; import React, { PropTypes } from 'react'; -import { History } from 'react-router'; -import { contain } from 'thundercats-react'; -import debugFactory from 'debug'; +import { reduxForm } from 'redux-form'; +import { connector } from 'react-redux'; +import debug from 'debug'; import dedent from 'dedent'; import normalizeUrl from 'normalize-url'; @@ -26,7 +26,7 @@ import { isURL } from 'validator'; -const debug = debugFactory('fcc:jobs:newForm'); +const log = debug('fcc:jobs:newForm'); const checkValidity = [ 'position', @@ -100,396 +100,369 @@ function makeRequired(validator) { return (val) => !!val && validator(val); } -export default contain({ - store: 'appStore', - actions: 'jobActions', - map({ jobsApp: { form = {} } }) { - const { - position, - locale, - description, - email, - url, - logo, - company, - isFrontEndCert = true, - isBackEndCert, - isHighlighted, - isRemoteOk, - howToApply - } = form; - return { - position: formatValue(position, makeRequired(isAscii)), - locale: formatValue(locale, makeRequired(isAscii)), - description: formatValue(description, makeRequired(helpers.identity)), - email: formatValue(email, makeRequired(isEmail)), - url: formatValue(formatUrl(url), isValidURL), - logo: formatValue(formatUrl(logo), isValidURL), - company: formatValue(company, makeRequired(isAscii)), - isHighlighted: formatValue(isHighlighted, null, 'bool'), - isRemoteOk: formatValue(isRemoteOk, null, 'bool'), - howToApply: formatValue(howToApply, makeRequired(isAscii)), - isFrontEndCert, - isBackEndCert - }; - }, - subscribeOnWillMount() { - return typeof window !== 'undefined'; - } - }, - React.createClass({ - displayName: 'NewJob', +const formOptions = { + fields: [ + 'position', + 'locale', + 'description', + 'email', + 'url', + 'logo', + 'company', + 'isHighlighted', + 'isRemoteOk', + 'isFrontEndCert', + 'isBackEndCert', + 'howToApply' + ] +} - propTypes: { - jobActions: PropTypes.object, - position: PropTypes.object, - locale: PropTypes.object, - description: PropTypes.object, - email: PropTypes.object, - url: PropTypes.object, - logo: PropTypes.object, - company: PropTypes.object, - isHighlighted: PropTypes.object, - isRemoteOk: PropTypes.object, - isFrontEndCert: PropTypes.bool, - isBackEndCert: PropTypes.bool, - howToApply: PropTypes.object - }, +export class NewJob extends React.Component { + static displayName = 'NewJob'; - mixins: [History], + static propTypes = { + jobActions: PropTypes.object, + fields: PropTypes.object, + onSubmit: PropTypes.func + }; - handleSubmit(e) { - e.preventDefault(); - const pros = this.props; - let valid = true; - checkValidity.forEach((prop) => { - // if value exist, check if it is valid - if (pros[prop].value && pros[prop].type !== 'boolean') { - valid = valid && !!pros[prop].valid; - } - }); - - if ( - !valid || - !pros.isFrontEndCert && - !pros.isBackEndCert - ) { - debug('form not valid'); - return; + handleSubmit(e) { + e.preventDefault(); + const pros = this.props; + let valid = true; + checkValidity.forEach((prop) => { + // if value exist, check if it is valid + if (pros[prop].value && pros[prop].type !== 'boolean') { + valid = valid && !!pros[prop].valid; } + }); - const { - jobActions, + if ( + !valid || + !pros.isFrontEndCert && + !pros.isBackEndCert + ) { + debug('form not valid'); + return; + } - // form values - position, - locale, - description, - email, - url, - logo, - company, - isFrontEndCert, - isBackEndCert, - isHighlighted, - isRemoteOk, - howToApply - } = this.props; + const { + jobActions, - // sanitize user output - const jobValues = { - position: inHTMLData(position.value), - locale: inHTMLData(locale.value), - description: inHTMLData(description.value), - email: inHTMLData(email.value), - url: formatUrl(uriInSingleQuotedAttr(url.value), false), - logo: formatUrl(uriInSingleQuotedAttr(logo.value), false), - company: inHTMLData(company.value), - isHighlighted: !!isHighlighted.value, - isRemoteOk: !!isRemoteOk.value, - howToApply: inHTMLData(howToApply.value), - isFrontEndCert, - isBackEndCert - }; + // form values + position, + locale, + description, + email, + url, + logo, + company, + isFrontEndCert, + isBackEndCert, + isHighlighted, + isRemoteOk, + howToApply + } = this.props; - const job = Object.keys(jobValues).reduce((accu, prop) => { - if (jobValues[prop]) { - accu[prop] = jobValues[prop]; - } - return accu; - }, {}); + // sanitize user output + const jobValues = { + position: inHTMLData(position.value), + locale: inHTMLData(locale.value), + description: inHTMLData(description.value), + email: inHTMLData(email.value), + url: formatUrl(uriInSingleQuotedAttr(url.value), false), + logo: formatUrl(uriInSingleQuotedAttr(logo.value), false), + company: inHTMLData(company.value), + isHighlighted: !!isHighlighted.value, + isRemoteOk: !!isRemoteOk.value, + howToApply: inHTMLData(howToApply.value), + isFrontEndCert, + isBackEndCert + }; - job.postedOn = new Date(); - debug('job sanitized', job); - jobActions.saveForm(job); + const job = Object.keys(jobValues).reduce((accu, prop) => { + if (jobValues[prop]) { + accu[prop] = jobValues[prop]; + } + return accu; + }, {}); - this.history.pushState(null, '/jobs/new/preview'); - }, + job.postedOn = new Date(); + debug('job sanitized', job); + jobActions.saveForm(job); - componentDidMount() { - const { jobActions } = this.props; - jobActions.getSavedForm(); - }, + this.history.pushState(null, '/jobs/new/preview'); + }, - handleChange(name, { target: { value } }) { - const { jobActions: { handleForm } } = this.props; - handleForm({ [name]: value }); - }, + componentDidMount() { + const { jobActions } = this.props; + jobActions.getSavedForm(); + }, - handleCertClick(name) { - const { jobActions: { handleForm } } = this.props; - const otherButton = name === 'isFrontEndCert' ? - 'isBackEndCert' : - 'isFrontEndCert'; + handleChange(name, { target: { value } }) { + const { jobActions: { handleForm } } = this.props; + handleForm({ [name]: value }); + }, - handleForm({ - [name]: true, - [otherButton]: false - }); - }, + handleCertClick(name) { + const { jobActions: { handleForm } } = this.props; + const otherButton = name === 'isFrontEndCert' ? + 'isBackEndCert' : + 'isFrontEndCert'; - render() { - const { - position, - locale, - description, - email, - url, - logo, - company, - isHighlighted, - isRemoteOk, - howToApply, - isFrontEndCert, - isBackEndCert, - jobActions: { handleForm } - } = this.props; + handleForm({ + [name]: true, + [otherButton]: false + }); + }, - const { handleChange } = this; - const labelClass = 'col-sm-offset-1 col-sm-2'; - const inputClass = 'col-sm-6'; + render() { + const { + position, + locale, + description, + email, + url, + logo, + company, + isHighlighted, + isRemoteOk, + howToApply, + isFrontEndCert, + isBackEndCert, + jobActions: { handleForm } + } = this.props; - return ( -
- - -
-
+ const { handleChange } = this; + const labelClass = 'col-sm-offset-1 col-sm-2'; + const inputClass = 'col-sm-6'; -
-

First, select your ideal applicant:

+ return ( +
+ + +
+ + +
+

First, select your ideal applicant:

+
+ + + + + + +
+ + + + + +
+

Tell us about the position

+
+
+ handleChange('position', e) } + placeholder={ + 'e.g. Full Stack Developer, Front End Developer, etc.' + } + required={ true } + type='text' + value={ position.value } + wrapperClassName={ inputClass } /> + handleChange('locale', e) } + placeholder='e.g. San Francisco, Remote, etc.' + required={ true } + type='text' + value={ locale.value } + wrapperClassName={ inputClass } /> + handleChange('description', e) } + required={ true } + rows='10' + type='textarea' + value={ description.value } + wrapperClassName={ inputClass } /> + handleForm({ + isRemoteOk: !!checked + }) + } + type='checkbox' + wrapperClassName={ checkboxClass } /> +
+ +
+ +
+

How should they apply?

+ handleChange('howToApply', e) } + placeholder={ howToApplyCopy } + required={ true } + rows='2' + type='textarea' + value={ howToApply.value } + wrapperClassName={ inputClass } /> +
+
+
+
+

Tell us about your organization

+
+ handleChange('company', e) } + type='text' + value={ company.value } + wrapperClassName={ inputClass } /> + handleChange('email', e) } + placeholder='This is how we will contact you' + required={ true } + type='email' + value={ email.value } + wrapperClassName={ inputClass } /> + handleChange('url', e) } + placeholder='http://yourcompany.com' + type='url' + value={ url.value } + wrapperClassName={ inputClass } /> + handleChange('logo', e) } + placeholder='http://yourcompany.com/logo.png' + type='url' + value={ logo.value } + wrapperClassName={ inputClass } /> + +
+
+
+
+

Make it stand out

+
+
- - - -
- - - - - -
-

Tell us about the position

-
-
- handleChange('position', e) } - placeholder={ - 'e.g. Full Stack Developer, Front End Developer, etc.' - } - required={ true } - type='text' - value={ position.value } - wrapperClassName={ inputClass } /> - handleChange('locale', e) } - placeholder='e.g. San Francisco, Remote, etc.' - required={ true } - type='text' - value={ locale.value } - wrapperClassName={ inputClass } /> - handleChange('description', e) } - required={ true } - rows='10' - type='textarea' - value={ description.value } - wrapperClassName={ inputClass } /> - handleForm({ - isRemoteOk: !!checked - }) - } - type='checkbox' - wrapperClassName={ checkboxClass } /> -
- -
- -
-

How should they apply?

-
- handleChange('howToApply', e) } - placeholder={ howToApplyCopy } - required={ true } - rows='2' - type='textarea' - value={ howToApply.value } - wrapperClassName={ inputClass } /> -
- -
-
-
-

Tell us about your organization

-
- handleChange('company', e) } - type='text' - value={ company.value } - wrapperClassName={ inputClass } /> - handleChange('email', e) } - placeholder='This is how we will contact you' - required={ true } - type='email' - value={ email.value } - wrapperClassName={ inputClass } /> - handleChange('url', e) } - placeholder='http://yourcompany.com' - type='url' - value={ url.value } - wrapperClassName={ inputClass } /> - handleChange('logo', e) } - placeholder='http://yourcompany.com/logo.png' - type='url' - value={ logo.value } - wrapperClassName={ inputClass } /> - -
-
-
-
-

Make it stand out

-
-
- - + md={ 6 } + mdOffset={ 3 }> Highlight this ad to give it extra attention.
- Featured listings receive more clicks and more applications. - -
-
- - handleForm({ - isHighlighted: !!checked - }) - } - type='checkbox' - wrapperClassName={ - checkboxClass.replace('text-left', '') - } /> - -
- - - - + Featured listings receive more clicks and more applications. - -
- - -
- ); - } - }) -); +
+ + handleForm({ + isHighlighted: !!checked + }) + } + type='checkbox' + wrapperClassName={ + checkboxClass.replace('text-left', '') + } /> + +
+ + + + + + + +
+ + +
+ ); + } +} + +export default reduxForm( + formOptions, + mapStateToProps, + bindableActions +)(NewJob); diff --git a/common/app/routes/Jobs/components/Preview.jsx b/common/app/routes/Jobs/components/Preview.jsx index a50eca2794..cfdec61ddb 100644 --- a/common/app/routes/Jobs/components/Preview.jsx +++ b/common/app/routes/Jobs/components/Preview.jsx @@ -1,6 +1,6 @@ import React, { PropTypes } from 'react'; import { Button, Row, Col } from 'react-bootstrap'; -import { connect } from 'redux'; +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import PureComponent from 'react-pure-render/component'; import { goBack, push } from 'react-router-redux'; @@ -11,8 +11,9 @@ import JobNotFound from './JobNotFound.jsx'; import { clearSavedForm, saveJobToDb } from '../redux/actions'; const mapStateToProps = createSelector( - state => state.jobsApp.form, - (job = {}) => ({ job }) + state => state.jobsApp.previewJob, + state => state.jobsApp.jobs.entities + (job, jobsMap) => ({ job: jobsMap[job] || {} }) ); const bindableActions = { diff --git a/common/app/routes/Jobs/components/Show.jsx b/common/app/routes/Jobs/components/Show.jsx index 84ca630b92..05e9320a45 100644 --- a/common/app/routes/Jobs/components/Show.jsx +++ b/common/app/routes/Jobs/components/Show.jsx @@ -1,5 +1,6 @@ import React, { PropTypes } from 'react'; -import { connect, compose } from 'redux'; +import { compose } from 'redux'; +import { connect } from 'react-redux'; import { push } from 'react-router-redux'; import PureComponent from 'react-pure-render/component'; import { createSelector } from 'reselect'; diff --git a/common/app/routes/Jobs/index.js b/common/app/routes/Jobs/index.js index 9d8ee8c2dc..b388bdc577 100644 --- a/common/app/routes/Jobs/index.js +++ b/common/app/routes/Jobs/index.js @@ -2,7 +2,7 @@ import Jobs from './components/Jobs.jsx'; import NewJob from './components/NewJob.jsx'; import Show from './components/Show.jsx'; import Preview from './components/Preview.jsx'; -import GoToPayPal from './components/GoToPayPal.jsx'; +import JobTotal from './components/JobTotal.jsx'; import NewJobCompleted from './components/NewJobCompleted.jsx'; /* @@ -23,7 +23,7 @@ export default { component: Preview }, { path: 'jobs/new/check-out', - component: GoToPayPal + component: JobTotal }, { path: 'jobs/new/completed', component: NewJobCompleted diff --git a/common/app/routes/Jobs/redux/actions.js b/common/app/routes/Jobs/redux/actions.js index e69de29bb2..0212a2a375 100644 --- a/common/app/routes/Jobs/redux/actions.js +++ b/common/app/routes/Jobs/redux/actions.js @@ -0,0 +1,20 @@ +import { createAction } from 'redux-actions'; + +import types from './types'; + +export const fetchJobs = createAction(types.fetchJobs); +export const fetchJobsCompleted = createAction( + types.fetchJobsCompleted, + (currentJob, jobs) => ({ currentJob, jobs }) +); + +export const findJob = createAction(types.findJob); + +export const saveJob = createAction(types.saveJob); +export const saveJobCompleted = createAction(types.saveJobCompleted); + +export const saveForm = createAction(types.saveForm); +export const clearForm = createAction(types.clearSavedForm); +export const loadSavedFormCompleted = createAction( + types.loadSavedFormCompleted +); diff --git a/common/app/routes/Jobs/redux/apply-promo-saga.js b/common/app/routes/Jobs/redux/apply-promo-saga.js new file mode 100644 index 0000000000..e5b182baf5 --- /dev/null +++ b/common/app/routes/Jobs/redux/apply-promo-saga.js @@ -0,0 +1,39 @@ +import { Observable } from 'rx'; + +import { testPromo } from './types'; +import { applyPromo } from './actions'; +import { postJSON$ } from '../../../../utils/ajax-stream'; + +export default () => ({ dispatch }) => next => { + return function applyPromoSaga(action) { + if (action.type !== testPromo) { + return next(action); + } + + const { id, code = '', type = null } = action.payload; + + const body = { + id, + code: code.replace(/[^\d\w\s]/, '') + }; + + if (type) { + body.type = type; + } + + return postJSON$('/api/promos/getButton', body) + .retry(3) + .map(({ promo }) => { + if (!promo || !promo.buttonId) { + throw new Error('No promo returned by server'); + } + + return applyPromo(promo); + }) + .catch(error => Observable.just({ + type: 'app.handleError', + error + })) + .doOnNext(dispatch); + }; +}; diff --git a/common/app/routes/Jobs/redux/fetch-jobs-saga.js b/common/app/routes/Jobs/redux/fetch-jobs-saga.js index 2712d2e47d..e7fec1a429 100644 --- a/common/app/routes/Jobs/redux/fetch-jobs-saga.js +++ b/common/app/routes/Jobs/redux/fetch-jobs-saga.js @@ -1,7 +1,50 @@ -import { fetchJobs, fetchJobsCompleted } from './types'; +import { Observable } from 'rx'; +import { normalize, Schema, arrayOf } from 'normalizr'; -export default ({ services }) => ({ dispatch, getState }) => next => { +import { fetchJobsCompleted } from './actions'; +import { fetchJobs } from './types'; +import { handleError } from '../../../redux/types'; + +const job = new Schema('job', { idAttribute: 'id' }); + +export default ({ services }) => ({ dispatch }) => next => { return function fetchJobsSaga(action) { - return next(action); + if (action.type !== fetchJobs) { + return next(action); + } + + const { payload: id } = action; + const data = { service: 'jobs' }; + if (id) { + data.id = id; + } + + return services.readService$(data) + .map(jobs => { + if (!Array.isArray(jobs)) { + jobs = [jobs]; + } + + const { entities, result } = normalize( + { jobs }, + { jobs: arrayOf(job) } + ); + + + return fetchJobsCompleted( + result.jobs[0], + { + entities: entities.jobs, + results: result.jobs + } + ); + }) + .catch(error => { + return Observable.just({ + type: handleError, + error + }); + }) + .doOnNext(dispatch); }; }; diff --git a/common/app/routes/Jobs/redux/index.js b/common/app/routes/Jobs/redux/index.js index 0936f320ae..b0b05159f1 100644 --- a/common/app/routes/Jobs/redux/index.js +++ b/common/app/routes/Jobs/redux/index.js @@ -1 +1,8 @@ -export default from './Actions'; +export actions from './actions'; +export reducer from './reducer'; +export types from './types'; + +import fetchJobsSaga from './fetch-jobs-saga'; +import saveJobSaga from './save-job-saga'; + +export const sagas = [ fetchJobsSaga, saveJobSaga ]; diff --git a/common/app/routes/Jobs/redux/reducer.js b/common/app/routes/Jobs/redux/reducer.js index e69de29bb2..2fa43101ee 100644 --- a/common/app/routes/Jobs/redux/reducer.js +++ b/common/app/routes/Jobs/redux/reducer.js @@ -0,0 +1,79 @@ +import { handleActions } from 'redux-actions'; + +import types from './types'; + +const replaceMethod = ''.replace; +function replace(str) { + if (!str) { return ''; } + return replaceMethod.call(str, /[^\d\w\s]/, ''); +} + +const initialState = { + currentJob: '', + newJob: {}, + jobs: { + entities: {}, + results: [] + } +}; + +export default handleActions( + { + [types.findJob]: (state, { payload: id }) => { + const currentJob = state.jobs.entities[id]; + return { + ...state, + currentJob: currentJob && currentJob.id ? + currentJob.id : + state.currentJob + }; + }, + [types.saveJobCompleted]: (state, { payload: newJob }) => { + return { + ...state, + newJob + }; + }, + [types.fetchJobCompleted]: (state, { payload: { jobs, currentJob } }) => ({ + ...state, + currentJob, + jobs + }), + [types.updatePromoCode]: (state, { payload }) => ({ + ...state, + promoCode: replace(payload) + }), + [types.applyPromo]: (state, { payload: promo }) => { + + const { + fullPrice: price, + buttonId, + discountAmount, + code: promoCode, + name: promoName + } = promo; + + return { + ...state, + price, + buttonId, + discountAmount, + promoCode, + promoApplied: true, + promoName + }; + }, + [types.clearPromo]: state => ({ + /* eslint-disable no-undefined */ + ...state, + price: undefined, + buttonId: undefined, + discountAmount: undefined, + promoCode: undefined, + promoApplied: false, + promoName: undefined + /* eslint-enable no-undefined */ + }) + }, + initialState +); diff --git a/common/app/routes/Jobs/redux/save-job-saga.js b/common/app/routes/Jobs/redux/save-job-saga.js new file mode 100644 index 0000000000..0faf17d823 --- /dev/null +++ b/common/app/routes/Jobs/redux/save-job-saga.js @@ -0,0 +1,26 @@ +import { Observable } from 'rx'; + +import { saveJobCompleted } from './actions'; +import { saveJob } from './types'; + +import { handleError } from '../../../redux/types'; + +export default ({ services }) => ({ dispatch }) => next => { + return function saveJobSaga(action) { + if (action.type !== saveJob) { + return next(action); + } + const { payload: job } = action; + + return services.createService$({ + service: 'jobs', + params: { job } + }) + .map(job => saveJobCompleted(job)) + .catch(error => Observable.just({ + type: handleError, + error + })) + .doOnNext(dispatch); + }; +}; diff --git a/common/app/routes/Jobs/redux/types.js b/common/app/routes/Jobs/redux/types.js index 3e3ccc2b6c..7417e734b6 100644 --- a/common/app/routes/Jobs/redux/types.js +++ b/common/app/routes/Jobs/redux/types.js @@ -3,16 +3,15 @@ const types = [ 'fetchJobsCompleted', 'findJob', - 'saveJob', - 'getJob', - 'getJobs', - 'openModal', - 'closeModal', - 'handleFormUpdate', + 'saveForm', - 'clear' + 'clearForm', + 'loadSavedForm', + 'loadSavedFormCompleted' ]; -export default types - .reduce((types, type) => ({ ...types, [type]: `jobs.${type}` }), {}); +export default types.reduce((types, type) => { + types[type] = `jobs.${type}`; + return types; +}, {}); diff --git a/common/app/sagas.js b/common/app/sagas.js index fc40bc384c..cfd486242b 100644 --- a/common/app/sagas.js +++ b/common/app/sagas.js @@ -1,6 +1,9 @@ import { sagas as appSagas } from './redux'; import { sagas as hikesSagas} from './routes/Hikes/redux'; +import { sagas as jobsSagas } from './routes/Jobs/redux'; + export default [ ...appSagas, - ...hikesSagas + ...hikesSagas, + ...jobsSagas ];