Skip to content
Snippets Groups Projects
Unverified Commit 217f2d18 authored by Todd Dembrey's avatar Todd Dembrey Committed by GitHub
Browse files

Merge pull request #931 from OpenTechFund/feature/844-api-refactor

Refactor how the api works to reduce boilerplate
parents 1bd4c377 d88b3329
No related branches found
No related tags found
No related merge requests found
Showing
with 172 additions and 211 deletions
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,
}
};
}
import { apiFetch } from '@api/utils';
export function fetchRound(id) {
return apiFetch(`/apply/api/rounds/${id}/`, 'GET');
return {
path:`/apply/api/rounds/${id}/`,
};
}
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}/`,
};
}
......@@ -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;
......
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,
})
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,
});
......
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'
}))
)
}
......@@ -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),
......
......@@ -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') {
......
......@@ -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'),
}
}
......
......@@ -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",
......
......@@ -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",
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment