diff --git a/opentech/static_src/src/app/src/api/notes.js b/opentech/static_src/src/app/src/api/notes.js index 7b51bfd2899ab70f80b839bc2b866cbce5a1b0eb..ec431cf28874a8b7baed6976de7a1988a751c5ab 100644 --- a/opentech/static_src/src/app/src/api/notes.js +++ b/opentech/static_src/src/app/src/api/notes.js @@ -1,15 +1,20 @@ -import { apiFetch } from '@api/utils'; - export function fetchNotesForSubmission(submissionID, visibility = 'internal') { - return apiFetch(`/apply/api/submissions/${submissionID}/comments/`, 'GET', { - visibility, - page_size: 1000, - }); + return { + path: `/apply/api/submissions/${submissionID}/comments/`, + params: { + visibility, + page_size: 1000, + } + }; } export function createNoteForSubmission(submissionID, note) { - return apiFetch(`/apply/api/submissions/${submissionID}/comments/`, 'POST', {}, { - body: JSON.stringify(note), - }); + return { + path: `/apply/api/submissions/${submissionID}/comments/`, + method: 'POST', + options: { + body: note, + } + }; } diff --git a/opentech/static_src/src/app/src/api/rounds.js b/opentech/static_src/src/app/src/api/rounds.js index cf8261575443b3539c176b6468ac9886b5f24d98..afd6f8bd22b9de9ae72911ff015fb8f9371931ac 100644 --- a/opentech/static_src/src/app/src/api/rounds.js +++ b/opentech/static_src/src/app/src/api/rounds.js @@ -1,5 +1,5 @@ -import { apiFetch } from '@api/utils'; - export function fetchRound(id) { - return apiFetch(`/apply/api/rounds/${id}/`, 'GET'); + return { + path:`/apply/api/rounds/${id}/`, + }; } diff --git a/opentech/static_src/src/app/src/api/submissions.js b/opentech/static_src/src/app/src/api/submissions.js index 8d7f95f776558bdb14a68dc874901214de1dcceb..a1505ebf50ce314aded651753c7abfc4f2285f61 100644 --- a/opentech/static_src/src/app/src/api/submissions.js +++ b/opentech/static_src/src/app/src/api/submissions.js @@ -1,13 +1,16 @@ -import { apiFetch } from '@api/utils'; - -export async function fetchSubmissionsByRound(id) { - return apiFetch('/apply/api/submissions/', 'GET', { - 'round': id, - 'page_size': 1000, - }); +export function fetchSubmissionsByRound(id) { + return { + path:'/apply/api/submissions/', + params: { + round: id, + page_size: 1000, + } + }; } -export async function fetchSubmission(id) { - return apiFetch(`/apply/api/submissions/${id}/`, 'GET'); +export function fetchSubmission(id) { + return { + path: `/apply/api/submissions/${id}/`, + }; } diff --git a/opentech/static_src/src/app/src/api/utils.js b/opentech/static_src/src/app/src/api/utils.js index 8471e751820ad9bfb9524ba4e0d8762c448c4bb1..ed1c123f417b39a540f2f157cd0bcbdbbe4505d1 100644 --- a/opentech/static_src/src/app/src/api/utils.js +++ b/opentech/static_src/src/app/src/api/utils.js @@ -4,7 +4,7 @@ const getBaseUrl = () => { return process.env.API_BASE_URL; }; -export async function apiFetch(path, method = 'GET', params, options = {}) { +export function apiFetch({path, method = 'GET', params = {}, options = {}}) { const url = new URL(getBaseUrl()); url.pathname = path; diff --git a/opentech/static_src/src/app/src/redux/actions/notes.js b/opentech/static_src/src/app/src/redux/actions/notes.js index 49b3bacde145424d05dfbedaacac0b5aef7f1380..b0200db84a47cf27a91b4162eaa4fe1bb79c8384 100644 --- a/opentech/static_src/src/app/src/redux/actions/notes.js +++ b/opentech/static_src/src/app/src/redux/actions/notes.js @@ -1,4 +1,5 @@ -import { updateSubmission, appendNoteIDForSubmission } from '@actions/submissions'; +import { CALL_API } from '@middleware/api' + import api from '@api'; export const FAIL_FETCHING_NOTES = 'FAIL_FETCHING_NOTES'; @@ -9,86 +10,27 @@ export const UPDATE_NOTE = 'UPDATE_NOTE'; export const START_CREATING_NOTE_FOR_SUBMISSION = 'START_CREATING_NOTE_FOR_SUBMISSION'; export const FAIL_CREATING_NOTE_FOR_SUBMISSION = 'FAIL_CREATING_NOTE_FOR_SUBMISSION'; -const startFetchingNotes = () => ({ - type: START_FETCHING_NOTES, -}); - -const failFetchingNotes = message => ({ - type: FAIL_FETCHING_NOTES, - message, -}); - -export const fetchNotesForSubmission = submissionID => { - return async function(dispatch) { - dispatch(startFetchingNotes()); - try { - const response = await api.fetchNotesForSubmission(submissionID); - const json = await response.json(); - if (!response.ok) { - return dispatch(failFetchingNotes(json.detail)); - } - return dispatch(updateNotesForSubmission(submissionID, json)); - } catch(e) { - return dispatch(failFetchingNotes(e.message)); - } - }; -}; - -const updateNotesForSubmission = (submissionID, data) => { - return function(dispatch) { - dispatch(updateNotes(data)); - dispatch(updateSubmission(submissionID, { - comments: data.results.map(v => v.id), - })); - }; -}; +export const fetchNotesForSubmission = submissionID => (dispatch, getState) => { + return dispatch(fetchNotes(submissionID)) +} -export const updateNotes = data => ({ - type: UPDATE_NOTES, - data, -}); - -const startCreatingNoteForSubmission = submissionID => ({ - type: START_CREATING_NOTE_FOR_SUBMISSION, - submissionID -}); - -const failCreatingNoteForSubmission = (submissionID, error) => ({ - type: FAIL_CREATING_NOTE_FOR_SUBMISSION, +const fetchNotes = (submissionID) => ({ + [CALL_API]: { + types: [ START_FETCHING_NOTES, UPDATE_NOTES, FAIL_FETCHING_NOTES], + endpoint: api.fetchNotesForSubmission(submissionID), + }, submissionID, - error -}); +}) -const updateNote = (data, submissionID) => ({ - type: UPDATE_NOTE, - submissionID, - data, -}); -const createdNoteForSubmission = (submissionID, data) => { - return function(dispatch) { - dispatch(updateNote(data, submissionID)); - dispatch(appendNoteIDForSubmission(submissionID, data.id)); - return true; - }; -}; +export const createNoteForSubmission = (submissionID, note) => (dispatch, getState) => { + return dispatch(createNote(submissionID, note)) +} -export const createNoteForSubmission = (submissionID, note) => { - return async function(dispatch) { - dispatch(startCreatingNoteForSubmission(submissionID)); - try { - const response = await api.createNoteForSubmission(submissionID, note); - const json = await response.json(); - if (!response.ok) { - return dispatch(failCreatingNoteForSubmission( - submissionID, - JSON.stringify(json) - )); - } - return dispatch(createdNoteForSubmission(submissionID, json)); - } catch (e) { - console.error(e); - return dispatch(failCreatingNoteForSubmission(submissionID, e.message)); - } - } -}; +const createNote = (submissionID, note) => ({ + [CALL_API]: { + types: [ START_CREATING_NOTE_FOR_SUBMISSION, UPDATE_NOTE, FAIL_CREATING_NOTE_FOR_SUBMISSION], + endpoint: api.createNoteForSubmission(submissionID, note), + }, + submissionID, +}) diff --git a/opentech/static_src/src/app/src/redux/actions/submissions.js b/opentech/static_src/src/app/src/redux/actions/submissions.js index 62091b71d15248937338875529347abbb178812f..11ff549a44b1f7066e1baa303c59aa8287d46871 100644 --- a/opentech/static_src/src/app/src/redux/actions/submissions.js +++ b/opentech/static_src/src/app/src/redux/actions/submissions.js @@ -1,3 +1,5 @@ +import { CALL_API } from '@middleware/api' + import api from '@api'; import { getCurrentSubmission, @@ -63,80 +65,29 @@ export const loadCurrentRoundSubmissions = () => (dispatch, getState) => { } -export const fetchRound = roundID => { - return async function(dispatch) { - dispatch(startLoadingRound(roundID)); - try { - const response = await api.fetchRound(roundID); - const json = await response.json(); - if (response.ok) { - dispatch(updateRound(roundID, json)); - } else { - dispatch(failLoadingRound(json.detail)); - } - } catch (e) { - dispatch(failLoadingRound(e.message)); - } - }; -}; - - -const updateRound = (roundID, data) => ({ - type: UPDATE_ROUND, - roundID, - data, -}); - - -const startLoadingRound = (roundID) => ({ - type: START_LOADING_ROUND, - roundID, -}); - - -const failLoadingRound = (message) => ({ - type: FAIL_LOADING_ROUND, - message, -}); - - - -export const fetchSubmissionsByRound = roundID => { - return async function(dispatch) { - dispatch(startLoadingSubmissionsByRound(roundID)); - try { - const response = await api.fetchSubmissionsByRound(roundID); - const json = await response.json(); - if (response.ok) { - dispatch(updateSubmissionsByRound(roundID, json)); - } else { - dispatch(failLoadingSubmissionsByRound(json.detail)); - } - } catch (e) { - dispatch(failLoadingSubmissionsByRound(e.message)); - } - }; -}; - - -const updateSubmissionsByRound = (roundID, data) => ({ - type: UPDATE_SUBMISSIONS_BY_ROUND, +const fetchRound = (roundID) => ({ + [CALL_API]: { + types: [ START_LOADING_ROUND, UPDATE_ROUND, FAIL_LOADING_ROUND], + endpoint: api.fetchRound(roundID), + }, roundID, - data, -}); +}) - -const startLoadingSubmissionsByRound = (roundID) => ({ - type: START_LOADING_SUBMISSIONS_BY_ROUND, +const fetchSubmissionsByRound = (roundID) => ({ + [CALL_API]: { + types: [ START_LOADING_SUBMISSIONS_BY_ROUND, UPDATE_SUBMISSIONS_BY_ROUND, FAIL_LOADING_SUBMISSIONS_BY_ROUND], + endpoint: api.fetchSubmissionsByRound(roundID), + }, roundID, -}); - - -const failLoadingSubmissionsByRound = (message) => ({ - type: FAIL_LOADING_SUBMISSIONS_BY_ROUND, - message, -}); +}) +const fetchSubmission = (submissionID) => ({ + [CALL_API]: { + types: [ START_LOADING_SUBMISSION, UPDATE_SUBMISSION, FAIL_LOADING_SUBMISSION], + endpoint: api.fetchSubmission(submissionID), + }, + submissionID, +}) export const loadCurrentSubmission = (requiredFields=[]) => (dispatch, getState) => { const submissionID = getCurrentSubmissionID(getState()) @@ -153,42 +104,6 @@ export const loadCurrentSubmission = (requiredFields=[]) => (dispatch, getState) } -export const fetchSubmission = submissionID => { - return async function(dispatch) { - - dispatch(startLoadingSubmission(submissionID)); - try { - const response = await api.fetchSubmission(submissionID); - const json = await response.json(); - if (response.ok) { - dispatch(updateSubmission(submissionID, json)); - } else { - dispatch(failLoadingSubmission(json.detail)); - } - } catch (e) { - dispatch(failLoadingSubmission(e.message)); - } - }; -}; - - -const startLoadingSubmission = submissionID => ({ - type: START_LOADING_SUBMISSION, - submissionID, -}); - -const failLoadingSubmission = submissionID => ({ - type: FAIL_LOADING_SUBMISSION, - submissionID, -}); - - -export const updateSubmission = (submissionID, data) => ({ - type: UPDATE_SUBMISSION, - submissionID, - data, -}); - export const clearCurrentSubmission = () => ({ type: CLEAR_CURRENT_SUBMISSION, }); diff --git a/opentech/static_src/src/app/src/redux/middleware/api.js b/opentech/static_src/src/app/src/redux/middleware/api.js new file mode 100644 index 0000000000000000000000000000000000000000..d808652d5eb396f47f3a0e15177e5ac6b857d93e --- /dev/null +++ b/opentech/static_src/src/app/src/redux/middleware/api.js @@ -0,0 +1,80 @@ +import { camelizeKeys, decamelizeKeys } from 'humps' + +import { apiFetch } from '@api/utils' + +const callApi = (endpoint) => { + // If body is an object, decamelize the keys. + const { options } = endpoint; + if (options !== undefined && typeof options.body === 'object') { + endpoint = { + ...endpoint, + options: { + ...options, + body: JSON.stringify(decamelizeKeys(options.body)) + } + } + } + + return apiFetch(endpoint) + .then(response => + response.json().then(json => { + if (!response.ok) { + return Promise.reject({message: json.error}) + } + return camelizeKeys(json) + }) + ) +} + + +export const CALL_API = 'Call API' + + +// A Redux middleware that interprets actions with CALL_API info specified. +// Performs the call and promises when such actions are dispatched. +export default store => next => action => { + const callAPI = action[CALL_API] + + if (callAPI === undefined) { + return next(action) + } + + let { endpoint } = callAPI + const { types } = callAPI + + if (typeof endpoint === 'function') { + endpoint = endpoint(store.getState()) + } + + if (typeof endpoint !== 'object' && typeof endpoint.path !== 'string') { + throw new Error('Specify a string endpoint URL.') + } + + if (!Array.isArray(types) || types.length !== 3) { + throw new Error('Expected an array of three action types.') + + } + if (!types.every(type => typeof type === 'string')) { + throw new Error('Expected action types to be strings.') + } + + const actionWith = data => { + const finalAction = {...action, ...data} + delete finalAction[CALL_API] + return finalAction + } + + const [ requestType, successType, failureType ] = types + next(actionWith({ type: requestType })) + + return callApi(endpoint).then( + response => next(actionWith({ + data: response, + type: successType + })), + error => next(actionWith({ + type: failureType, + error: error.message || 'Something bad happened' + })) + ) +} diff --git a/opentech/static_src/src/app/src/redux/reducers/submissions.js b/opentech/static_src/src/app/src/redux/reducers/submissions.js index 0c5253fd60552f4e909819a19728c3a2ca36e7f1..6e66e225a39375232849ec2610b9a4a33b9dd6a3 100644 --- a/opentech/static_src/src/app/src/redux/reducers/submissions.js +++ b/opentech/static_src/src/app/src/redux/reducers/submissions.js @@ -7,9 +7,10 @@ import { UPDATE_SUBMISSIONS_BY_ROUND, UPDATE_SUBMISSION, SET_CURRENT_SUBMISSION, - ADD_NOTE_FOR_SUBMISSION, } from '@actions/submissions'; +import { UPDATE_NOTES, UPDATE_NOTE } from '@actions/notes' + function submission(state, action) { switch(action.type) { @@ -32,11 +33,16 @@ function submission(state, action) { isFetching: false, isErrored: false, }; - case ADD_NOTE_FOR_SUBMISSION: + case UPDATE_NOTES: + return { + ...state, + comments: action.data.results.map(note => note.id), + }; + case UPDATE_NOTE: return { ...state, comments: [ - action.noteID, + action.data.id, ...(state.comments || []), ] }; @@ -50,8 +56,9 @@ function submissionsByID(state = {}, action) { switch(action.type) { case START_LOADING_SUBMISSION: case FAIL_LOADING_SUBMISSION: - case ADD_NOTE_FOR_SUBMISSION: case UPDATE_SUBMISSION: + case UPDATE_NOTE: + case UPDATE_NOTES: return { ...state, [action.submissionID]: submission(state[action.submissionID], action), diff --git a/opentech/static_src/src/app/src/redux/store.js b/opentech/static_src/src/app/src/redux/store.js index ed0b719d6c0ff1968140fc66689ce15d53ddaa07..a7002461574d3319ee31b524868b725189d11b00 100644 --- a/opentech/static_src/src/app/src/redux/store.js +++ b/opentech/static_src/src/app/src/redux/store.js @@ -4,9 +4,11 @@ import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly' import logger from 'redux-logger' import rootReducer from '@reducers'; +import api from '@middleware/api' const MIDDLEWARE = [ ReduxThunk, + api, ]; if (process.env.NODE_ENV === 'development') { diff --git a/opentech/static_src/src/app/webpack.base.config.js b/opentech/static_src/src/app/webpack.base.config.js index b30d026d86c698c658126352f7bc9fd8c41fcca8..3c0f73b6bd702ce544c39510ac7f7d116e15730e 100644 --- a/opentech/static_src/src/app/webpack.base.config.js +++ b/opentech/static_src/src/app/webpack.base.config.js @@ -71,6 +71,7 @@ module.exports = { '@reducers': path.resolve(__dirname, 'src/redux/reducers'), '@selectors': path.resolve(__dirname, 'src/redux/selectors'), '@actions': path.resolve(__dirname, 'src/redux/actions'), + '@middleware': path.resolve(__dirname, 'src/redux/middleware'), '@api': path.resolve(__dirname, 'src/api'), } } diff --git a/package-lock.json b/package-lock.json index acafab1431a4125fa65640316ce03a3ecc34a885..6553c6995b6f055d9adc352016f2535403329d69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5824,6 +5824,11 @@ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "dev": true }, + "humps": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/humps/-/humps-2.0.1.tgz", + "integrity": "sha1-3QLqYIG9BWjcXQcxhEY5V7qe+ao=" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/package.json b/package.json index 5757f29e394fe322874f8c8f567b4f1bbcb6871f..88b522444675e5436f6ad83576404ee6853ff28e 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "gulp-size": "^3.0.0", "gulp-touch-cmd": "0.0.1", "gulp-uglify": "^3.0.1", + "humps": "^2.0.1", "js-cookie": "^2.2.0", "markdown": "^0.5.0", "moment": "^2.24.0",