Skip to content

Commit

Permalink
initial version of folder list working, displaying folder on page load
Browse files Browse the repository at this point in the history
  • Loading branch information
BlueAccords committed May 28, 2018
1 parent 330ec13 commit c27ede4
Show file tree
Hide file tree
Showing 19 changed files with 476 additions and 12 deletions.
5 changes: 5 additions & 0 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"redux": "^4.0.0",
"redux-saga": "^0.16.0",
"redux-saga-thunk": "^0.7.1",
"reselect": "^3.0.1",
"yup": "^0.24.1"
}
}
25 changes: 25 additions & 0 deletions client/src/state/allFolders/_helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* helper function to transform object of key/value pairs to be sent as part of
* api call URL
* @param {Object} params object of query parameters
*/
export const concatParams = function(params) {
const paramsArr = Object.keys(params).reduce((acc, val) => {
// check if array, and concat item in array if value is true
if(Array.isArray(params[val])) {
const valArr = params[val];
valArr.forEach(option => {
if(option.checked) acc.push(val.concat('=', option.label));
});

return acc;
} else if(params[val] !== undefined && params[val] !== null) {
acc.push(val.concat('=', params[val]));
return acc;
} else {
return acc;
}
}, [])

return paramsArr.join('&');
}
50 changes: 50 additions & 0 deletions client/src/state/allFolders/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as types from './types';

export const foldersFetchRequest = function(payload) {
return {
type: types.FOLDERS_FETCH_REQUEST,
payload
}
}

export const foldersFetchSuccess = function(payload) {
return {
type: types.FOLDERS_FETCH_SUCCESS,
payload: payload
}
}

export const foldersFetchFailure = function(error) {
return {
type: types.FOLDERS_FETCH_FAILURE,
payload: error
}
}

export const foldersFetchExit = function() {
return {
type: types.FOLDERS_FETCH_EXIT,
}
}

export const foldersSetCurrentPage = function(page) {
return {
type: types.FOLDERS_SET_CURRENT_PAGE,
payload: page
}
}

export const foldersClearPageCache = function() {
return {
type: types.FOLDERS_CLEAR_PAGE_CACHE
}
}

// this action does the same thing as fetchRequest, but adds a debounce of 2 seconds
// to account for the user typing
export const foldersFilterRequest = function(payload) {
return {
type: types.FOLDERS_FILTER_REQUEST,
payload
}
}
46 changes: 46 additions & 0 deletions client/src/state/allFolders/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import axios from 'axios';
axios.defaults.withCredentials = true;
import * as helpers from './_helpers';

const BASE_URL = 'http://localhost:3000/api/'
const LIMIT = 25;

const foldersFetch = async (params) => {
try {
const { page, sortKey, sortDirection, searchFilter, optionsFilter } = params;

const queryString = helpers.concatParams({
page: page,
limit: LIMIT,
sortBy: sortKey,
order: sortDirection,
q: searchFilter
// folder_type: optionsFilter, can be set leter via checkbox options
});

console.log(queryString);
const url = BASE_URL.concat('folder', '?', queryString);
const response = await axios.get(url);
// const totalCount = response.headers['x-total-count'];
const totalCount = response.data.data.total;
const lastPage = response.data.data.lastPage;
const results = response.data.data.results;

// FIXME: the problem is if you make an api call for a page > last page in the api,
// the api will just return an empty data set but it will still be a 200 response code.
return {
data: results,
lastPage: lastPage,
currentPage: page
}
} catch(err) {
// TODO: add better error handling for "network error": i.e. when server is not online at all
// https://github.com/axios/axios#handling-errors
console.log(err);
throw err.response || err.message;
}
}

export default {
foldersFetch
}
15 changes: 15 additions & 0 deletions client/src/state/allFolders/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import reducer from './reducers';
import * as types from './types';
import * as actions from './actions';
import * as selectors from './selectors';
import { sagas } from './sagas';

export {
types,
actions,
sagas,
selectors
}

export default reducer;

104 changes: 104 additions & 0 deletions client/src/state/allFolders/reducers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import * as types from './types';
import { combineReducers } from 'redux';

const isLoading = (state = false, action) => {
switch (action.type) {
case types.FOLDERS_FETCH_REQUEST:
return true;
case types.FOLDERS_FETCH_SUCCESS:
return false;
case types.FOLDERS_FETCH_FAILURE:
return false;
case types.FOLDERS_FETCH_EXIT: // used to prematurely exit fetch if folders are already cached
return false;
default:
return state;
}
}

const error = (state = null, action) => {
switch(action.type) {
case types.FOLDERS_FETCH_REQUEST:
return null;
case types.FOLDERS_FETCH_FAILURE:
return action.payload
default:
return state;
}
}

// object containing all the items, with folder keys as the object properties
const byId = (state = {}, action) => {
console.log(action.payload);
const foldersList = {};
switch(action.type) {
case types.FOLDERS_FETCH_SUCCESS:
for(let i = 0; i < action.payload.data.length; i++) {
const folderItem = action.payload.data[i];
foldersList[folderItem.id] = folderItem;
}
return {
...state,
...foldersList
}
default:
return state;
}
}

// array of only the item ids
const allIds = (state = [], action) => {
switch(action.type) {
case types.FOLDERS_FETCH_SUCCESS:
return [
...state,
...action.payload.data.map((item) => {
return item.id;
})
]
default:
return state;
}
}

const currentPage = (state = 1, action) => {
switch(action.type) {
case types.FOLDERS_SET_CURRENT_PAGE:
return action.payload;
default:
return state;
}
}

const lastPage = (state = 0, action) => {
switch(action.type) {
case types.FOLDERS_FETCH_SUCCESS:
return action.payload.lastPage
default:
return state;
}
}

const pages = (state = {}, action) => {
switch(action.type) {
case types.FOLDERS_FETCH_SUCCESS:
return {
...state,
[action.payload.currentPage]: action.payload.data.map((item) => item.id)
}
case types.FOLDERS_CLEAR_PAGE_CACHE:
return {}
default:
return state;
}
}

export default combineReducers({
error,
isLoading,
byId,
allIds,
currentPage,
lastPage,
pages
});
75 changes: 75 additions & 0 deletions client/src/state/allFolders/sagas.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { delay } from 'redux-saga';
import { put, takeLatest, takeEvery, all, call, fork, select} from 'redux-saga/effects'
// import { push } from 'react-router-redux';

import * as actions from './actions';
import * as types from './types';
import * as selectors from './selectors'
import api from './api';

const FILTER_DEBOUNCE_DELAY = 1000;


export function* watchFolderFilterRequest() {
yield takeLatest(types.FOLDERS_FILTER_REQUEST, handleFilterRequest);
}

export function* handleFilterRequest(action) {
yield delay(FILTER_DEBOUNCE_DELAY);

// execute folder fetch after debounce delay
yield put(actions.foldersFetchRequest({
...action.payload,
page: 1 // manually set page to 1 as filtered results can have less pages than current query params
}));
}

export function* watchFolderFetchRequest() {
yield takeLatest(types.FOLDERS_FETCH_REQUEST, foldersFetch);
}

export function* foldersFetch(action) {
const { page, sortKey, sortDirection, clearCache=false, searchFilter=null, optionsFilter } = action.payload;

try {
if(clearCache) {
yield put(actions.foldersClearPageCache()); // clear page cache to set new results
}
const currentPage = yield select(selectors.getCurrentPage);
const pageParam = page ? page : currentPage; // get current page from payload or store if not provided
const isPageCached = yield select(selectors.getIsFolderCached, pageParam);


// check is page is already loaded before making api call
if(!isPageCached || clearCache) {
// TODO: check is page is cached on sort/filter
const data = yield call(api.foldersFetch, {
page: pageParam,
sortKey,
sortDirection,
searchFilter,
optionsFilter
});
yield put(actions.foldersSetCurrentPage(data.currentPage)); // set current page
yield put(actions.foldersFetchSuccess(data));
} else {
yield put(actions.foldersSetCurrentPage(pageParam)); // set current page
yield put(actions.foldersFetchExit());
}

} catch(err) {
// TODO: better error handling
console.log('err');
console.log(err);
yield put(actions.foldersFetchFailure(err));
}
}

// export only watcher sagas in one variable
export const sagas = [
watchFolderFetchRequest,
watchFolderFilterRequest
];



37 changes: 37 additions & 0 deletions client/src/state/allFolders/selectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SELECTORS
import { createSelector } from 'reselect';


// INPUT SELECTORS
export const getCurrentPage = (state) => state.allFolders.currentPage;
export const getIsFolderCached = (state, page) => state.allFolders.pages[page] !== undefined;
const getFoldersById = (state) => state.allFolders.byId;
const getFoldersAllIds = (state) => state.allFolders.allIds;
const getFolderIdsByPage = state => {
const page = state.allFolders.currentPage;
const pageIds = state.allFolders.pages[page];

// check if page ids are already cached or not
if (pageIds === undefined) {
return [];
} else {
return pageIds
}
}

// SELECTORS
// get all folders by mapping the array of only ids to the object containing
// all folders by their key
export const getAllFolders = createSelector(
[getFoldersById, getFoldersAllIds],
(foldersById, foldersAllIds) => {
return foldersAllIds.map((allIdsKey) => foldersById[allIdsKey]);
}
)

export const getAllFoldersOfCurrentPage = createSelector(
[getFoldersById, getFolderIdsByPage],
(aById, aIdsByPage) => {
return aIdsByPage.map((pageIdKey) => aById[pageIdKey]);
}
)
8 changes: 8 additions & 0 deletions client/src/state/allFolders/types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const FOLDERS_FETCH_REQUEST = 'FOLDERS_FETCH_REQUEST';
export const FOLDERS_FETCH_SUCCESS = 'FOLDERS_FETCH_SUCCESS';
export const FOLDERS_FETCH_FAILURE = 'FOLDERS_FETCH_FAILURE';
export const FOLDERS_FETCH_EXIT = 'FOLDERS_FETCH_EXIT';
export const FOLDERS_SET_CURRENT_PAGE = 'FOLDERS_SET_CURRENT_PAGE';
export const FOLDERS_FILTER_REQUEST = 'FOLDERS_FILTER_REQUEST';

export const FOLDERS_CLEAR_PAGE_CACHE = 'FOLDERS_CLEAR_PAGE_CACHE';
Loading

0 comments on commit c27ede4

Please sign in to comment.