diff --git a/package.json b/package.json index 65f6ae54b..f5e0dc66a 100644 --- a/package.json +++ b/package.json @@ -71,10 +71,12 @@ "react-redux": "^5.0.2", "react-syntax-highlighter": "^5.6.2", "redux": "^3.6.0", + "redux-actions": "^2.2.1", "redux-logger": "^2.7.4", "redux-persist": "^4.3.1", "redux-persist-transform-encrypt": "^1.0.2", - "redux-thunk": "^2.2.0" + "redux-thunk": "^2.2.0", + "reselect": "^3.0.1" }, "devDependencies": { "@commitlint/cli": "^3.1.0", diff --git a/src/auth/auth.selectors.js b/src/auth/auth.selectors.js new file mode 100644 index 000000000..d11542d69 --- /dev/null +++ b/src/auth/auth.selectors.js @@ -0,0 +1,8 @@ +import { createSelector } from 'reselect'; + +export const getAuthFromStore = state => state.auth; + +export const getAuthLanguage = createSelector( + getAuthFromStore, + auth => auth.getAuthLanguage +); diff --git a/src/auth/index.js b/src/auth/index.js index 9ef35c4bd..f3be03aea 100644 --- a/src/auth/index.js +++ b/src/auth/index.js @@ -1,5 +1,6 @@ export * from './auth.action'; export * from './auth.reducer'; export * from './auth.type'; +export * from './auth.selectors'; export * from './screens'; diff --git a/src/organization/index.js b/src/organization/index.js index 56d5364cd..ec9c14b2a 100644 --- a/src/organization/index.js +++ b/src/organization/index.js @@ -1,5 +1,6 @@ export * from './organization.action'; export * from './organization.reducer'; -export * from './organization.type'; +export * from './organization.constants'; +export * from './organization.selectors'; export * from './screens'; diff --git a/src/organization/organization.action.js b/src/organization/organization.action.js index 6f305c40c..51189fb25 100644 --- a/src/organization/organization.action.js +++ b/src/organization/organization.action.js @@ -1,68 +1,80 @@ -import { fetchOrg, fetchOrgMembers, fetchUrl } from 'api'; -import { GET_ORG, GET_ORG_REPOS, GET_ORG_MEMBERS } from './organization.type'; +import { createAction } from 'redux-actions'; -export const getOrg = orgName => { - return (dispatch, getState) => { - const accessToken = getState().auth.accessToken; +import { + fetchOrg, + fetchOrgMembers, + fetchUrl, +} from 'api'; +import { + GET_ORG, + GET_ORG_LOADING, + GET_ORG_ERROR, + GET_ORG_REPOS, + GET_ORG_REPOS_LOADING, + GET_ORG_REPOS_ERROR, + GET_ORG_MEMBERS, + GET_ORG_MEMBERS_LOADING, + GET_ORG_MEMBERS_ERROR, +} from './organization.constants'; - dispatch({ type: GET_ORG.PENDING }); +export const getOrg = createAction(GET_ORG); +export const getOrgLoading = createAction(GET_ORG_LOADING); +export const getOrgError = createAction(GET_ORG_ERROR); +export const getOrgRepos = createAction(GET_ORG_REPOS); +export const getOrgReposLoading = createAction(GET_ORG_REPOS_LOADING); +export const getOrgReposError = createAction(GET_ORG_REPOS_ERROR); +export const getOrgMembers = createAction(GET_ORG_MEMBERS); +export const getOrgMembersLoading = createAction(GET_ORG_MEMBERS_LOADING); +export const getOrgMembersError = createAction(GET_ORG_MEMBERS_ERROR); - return fetchOrg(orgName, accessToken) - .then(data => { - dispatch({ - type: GET_ORG.SUCCESS, - payload: data, - }); - }) - .catch(error => { - dispatch({ - type: GET_ORG.ERROR, - payload: error, - }); - }); - }; +export const fetchOrganizations = orgName => (dispatch, getState) => { + // use a selector here + const accessToken = getState().auth.accessToken; + + dispatch(getOrgLoading(true)); + dispatch(getOrgError('')); + + return fetchOrg(orgName, accessToken) + .then(data => { + dispatch(getOrgLoading(false)); + dispatch(getOrg(data)); + }) + .catch(error => { + dispatch(getOrgLoading(false)); + dispatch(getOrgError(error)); + }); }; -export const getOrgRepos = url => { - return (dispatch, getState) => { - const accessToken = getState().auth.accessToken; +export const fetchOrganizationRepos = url => (dispatch, getState) => { + const accessToken = getState().auth.accessToken; - dispatch({ type: GET_ORG_REPOS.PENDING }); + dispatch(getOrgReposLoading(true)); + dispatch(getOrgReposError('')); - fetchUrl(url, accessToken) - .then(data => { - dispatch({ - type: GET_ORG_REPOS.SUCCESS, - payload: data, - }); - }) - .catch(error => { - dispatch({ - type: GET_ORG_REPOS.ERROR, - payload: error, - }); - }); - }; + fetchUrl(url, accessToken) + .then(data => { + dispatch(getOrgReposLoading(false)); + dispatch(getOrgRepos(data)); + }) + .catch(error => { + dispatch(getOrgReposLoading(false)); + dispatch(getOrgReposError(error)); + }); }; -export const getOrgMembers = orgName => { - return (dispatch, getState) => { - const accessToken = getState().auth.accessToken; +export const fetchOrganizationMembers = orgName => (dispatch, getState) => { + const accessToken = getState().auth.accessToken; - dispatch({ type: GET_ORG_MEMBERS.PENDING }); + dispatch(getOrgMembersLoading(true)); + dispatch(getOrgMembersError('')); - return fetchOrgMembers(orgName, accessToken) - .then(data => { - dispatch({ - type: GET_ORG_MEMBERS.SUCCESS, - payload: data, - }); - }) - .catch(error => { - dispatch({ - type: GET_ORG_MEMBERS.ERROR, - payload: error, - }); - }); - }; + return fetchOrgMembers(orgName, accessToken) + .then(data => { + dispatch(getOrgMembersLoading(false)); + dispatch(getOrgMembers(data)); + }) + .catch(error => { + dispatch(getOrgMembersLoading(false)); + dispatch(getOrgMembersError(error)); + }); }; diff --git a/src/organization/organization.constants.js b/src/organization/organization.constants.js new file mode 100644 index 000000000..76e3d0e2c --- /dev/null +++ b/src/organization/organization.constants.js @@ -0,0 +1,9 @@ +export const GET_ORG = 'GET_ORG'; +export const GET_ORG_LOADING = 'GET_ORG_LOADING'; +export const GET_ORG_ERROR = 'GET_ORG_ERROR'; +export const GET_ORG_REPOS = 'GET_ORG_REPOS'; +export const GET_ORG_REPOS_LOADING = 'GET_ORG_REPOS_LOADING'; +export const GET_ORG_REPOS_ERROR = 'GET_ORG_REPOS_ERROR'; +export const GET_ORG_MEMBERS = 'GET_ORG_MEMBERS'; +export const GET_ORG_MEMBERS_LOADING = 'GET_ORG_MEMBERS_LOADING'; +export const GET_ORG_MEMBERS_ERROR = 'GET_ORG_MEMBERS_ERROR'; diff --git a/src/organization/organization.reducer.js b/src/organization/organization.reducer.js index 37df01601..e719ee919 100644 --- a/src/organization/organization.reducer.js +++ b/src/organization/organization.reducer.js @@ -1,4 +1,16 @@ -import { GET_ORG, GET_ORG_REPOS, GET_ORG_MEMBERS } from './organization.type'; +import { handleActions } from 'redux-actions'; + +import { + GET_ORG, + GET_ORG_LOADING, + GET_ORG_ERROR, + GET_ORG_REPOS, + GET_ORG_REPOS_LOADING, + GET_ORG_REPOS_ERROR, + GET_ORG_MEMBERS, + GET_ORG_MEMBERS_LOADING, + GET_ORG_MEMBERS_ERROR, +} from './organization.constants'; const initialState = { organization: {}, @@ -7,63 +19,65 @@ const initialState = { isPendingOrg: false, isPendingRepos: false, isPendingMembers: false, - error: '', + organizationError: '', + organizationRepositoriesError: '', + organizationMembersError: '', }; -export const organizationReducer = (state = initialState, action = {}) => { - switch (action.type) { - case GET_ORG.PENDING: - return { - ...state, - isPendingOrg: true, - }; - case GET_ORG.SUCCESS: - return { - ...state, - organization: action.payload, - isPendingOrg: false, - }; - case GET_ORG.ERROR: - return { - ...state, - error: action.payload, - isPendingOrg: false, - }; - case GET_ORG_REPOS.PENDING: - return { - ...state, - isPendingRepos: true, - }; - case GET_ORG_REPOS.SUCCESS: - return { - ...state, - repositories: action.payload, - isPendingRepos: false, - }; - case GET_ORG_REPOS.ERROR: - return { - ...state, - error: action.payload, - isPendingRepos: false, - }; - case GET_ORG_MEMBERS.PENDING: - return { - ...state, - isPendingMembers: true, - }; - case GET_ORG_MEMBERS.SUCCESS: - return { - ...state, - members: action.payload, - isPendingMembers: false, - }; - case GET_ORG_MEMBERS.ERROR: - return { - ...state, - error: action.payload, - isPendingMembers: false, - }; - default: - return state; - } -}; +export const organizationReducer = handleActions({ + [GET_ORG]: (state, { payload }) => { + return { + ...state, + organization: payload, + }; + }, + [GET_ORG_LOADING]: (state, { payload }) => { + return { + ...state, + isPendingOrg: payload, + }; + }, + [GET_ORG_ERROR]: (state, { payload }) => { + return { + ...state, + organizationError: payload, + }; + }, + [GET_ORG_REPOS]: (state, { payload }) => { + return { + ...state, + repositories: payload, + }; + }, + [GET_ORG_REPOS_LOADING]: (state, { payload }) => { + return { + ...state, + isPendingRepos: payload, + }; + }, + [GET_ORG_REPOS_ERROR]: (state, { payload }) => { + return { + ...state, + organizationRepositoriesError: payload, + }; + }, + [GET_ORG_MEMBERS]: (state, { payload }) => { + return { + ...state, + members: payload, + }; + }, + [GET_ORG_MEMBERS_LOADING]: (state, { payload }) => { + return { + ...state, + isPendingMembers: payload, + }; + }, + [GET_ORG_MEMBERS_ERROR]: (state, { payload }) => { + return { + ...state, + organizationMembersError: payload, + }; + }, +}, initialState); + diff --git a/src/organization/organization.selectors.js b/src/organization/organization.selectors.js new file mode 100644 index 000000000..a8f170a29 --- /dev/null +++ b/src/organization/organization.selectors.js @@ -0,0 +1,48 @@ +import { createSelector } from 'reselect'; + +const getOrganizationFromStore = state => state.organization; + +export const getOrganization = createSelector( + getOrganizationFromStore, + organization => organization.organization || {} +); + +export const getOrganizationRepositories = createSelector( + getOrganizationFromStore, + organization => organization.repositories || [] +); + +export const getOrganizationMembers = createSelector( + getOrganizationFromStore, + organization => organization.members || [] +); + +export const getOrganizationIsPendingOrg = createSelector( + getOrganizationFromStore, + organization => organization.isPendingOrg || false +); + +export const getOrganizationIsPendingRepos = createSelector( + getOrganizationFromStore, + organization => organization.isPendingRepos || false +); + +export const getOrganizationIsPendingMembers = createSelector( + getOrganizationFromStore, + organization => organization.isPendingMembers || false +); + +export const getOrganizationError = createSelector( + getOrganizationFromStore, + organization => organization.organizationError || '' +); + +export const getOrganizationRepositoriesError = createSelector( + getOrganizationFromStore, + organization => organization.organizationRepositoriesError || '' +); + +export const getOrganizationMembersError = createSelector( + getOrganizationFromStore, + organization => organization.organizationMembersError || '' +); diff --git a/src/organization/organization.type.js b/src/organization/organization.type.js deleted file mode 100644 index 2e953d2aa..000000000 --- a/src/organization/organization.type.js +++ /dev/null @@ -1,5 +0,0 @@ -import { createActionSet } from 'utils'; - -export const GET_ORG = createActionSet('GET_ORG'); -export const GET_ORG_REPOS = createActionSet('GET_ORG_REPOS'); -export const GET_ORG_MEMBERS = createActionSet('GET_ORG_MEMBERS'); diff --git a/src/organization/screens/organization-profile.screen.js b/src/organization/screens/organization-profile.screen.js index 8ab15a4d2..56ce2726e 100644 --- a/src/organization/screens/organization-profile.screen.js +++ b/src/organization/screens/organization-profile.screen.js @@ -1,8 +1,12 @@ import React, { Component } from 'react'; +import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { StyleSheet, RefreshControl } from 'react-native'; import { ListItem } from 'react-native-elements'; - +import { createStructuredSelector } from 'reselect'; +import { + getAuthLanguage, +} from 'auth'; import { ViewContainer, UserProfile, @@ -12,25 +16,40 @@ import { ParallaxScroll, EntityInfo, } from 'components'; -import { emojifyText, translate } from 'utils'; +import { + emojifyText, + translate, +} from 'utils'; import { colors, fonts } from 'config'; -import { getOrg, getOrgRepos, getOrgMembers } from '../index'; - -const mapStateToProps = state => ({ - organization: state.organization.organization, - repositories: state.organization.repositories, - members: state.organization.members, - isPendingOrg: state.organization.isPendingOrg, - isPendingRepos: state.organization.isPendingRepos, - isPendingMembers: state.organization.isPendingMembers, - language: state.auth.language, +import { + // actions + fetchOrganizations, + fetchOrganizationMembers, + // Selectors + getOrganization, + getOrganizationRepositories, + getOrganizationMembers, + getOrganizationIsPendingOrg, + getOrganizationIsPendingRepos, + getOrganizationIsPendingMembers, +} from '../index'; + +const selectors = createStructuredSelector({ + organization: getOrganization, + repositories: getOrganizationRepositories, + members: getOrganizationMembers, + isPendingOrg: getOrganizationIsPendingOrg, + isPendingRepos: getOrganizationIsPendingRepos, + isPendingMembers: getOrganizationIsPendingMembers, + language: getAuthLanguage, }); -const mapDispatchToProps = dispatch => ({ - getOrgByDispatch: orgName => dispatch(getOrg(orgName)), - getOrgReposByDispatch: url => dispatch(getOrgRepos(url)), - getOrgMembersByDispatch: orgName => dispatch(getOrgMembers(orgName)), -}); +const actionCreators = { + fetchOrganizations, + fetchOrganizationMembers, +}; + +const actions = dispatch => bindActionCreators(actionCreators, dispatch); const styles = StyleSheet.create({ listTitle: { @@ -45,9 +64,9 @@ const styles = StyleSheet.create({ class OrganizationProfile extends Component { props: { - getOrgByDispatch: Function, + fetchOrganizations: Function, // getOrgReposByDispatch: Function, - getOrgMembersByDispatch: Function, + fetchOrganizationMembers: Function, organization: Object, // repositories: Array, members: Array, @@ -72,8 +91,8 @@ class OrganizationProfile extends Component { componentDidMount() { const organization = this.props.navigation.state.params.organization; - this.props.getOrgByDispatch(organization.login); - this.props.getOrgMembersByDispatch(organization.login); + this.props.fetchOrganizations(organization.login); + this.props.fetchOrganizationMembers(organization.login); } getOrgData = () => { @@ -81,8 +100,8 @@ class OrganizationProfile extends Component { this.setState({ refreshing: true }); Promise.all( - this.props.getOrgByDispatch(organization.login), - this.props.getOrgMembersByDispatch(organization.login) + this.props.fetchOrganizations(organization.login), + this.props.fetchOrganizationMembers(organization.login) ).then(() => { this.setState({ refreshing: false }); }); @@ -157,6 +176,6 @@ class OrganizationProfile extends Component { } export const OrganizationProfileScreen = connect( - mapStateToProps, - mapDispatchToProps + selectors, + actions )(OrganizationProfile); diff --git a/yarn.lock b/yarn.lock index cf9ab3fb4..c688cdfd8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3221,7 +3221,7 @@ interpret@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90" -invariant@^2.0.0, invariant@^2.2.0: +invariant@^2.0.0, invariant@^2.2.0, invariant@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" dependencies: @@ -4129,7 +4129,7 @@ lodash@^3.5.0: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@^4.0.0, lodash@^4.11.2, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.16.6, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1, lodash@^4.6.1: +lodash@^4.0.0, lodash@^4.11.2, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.16.6, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1, lodash@^4.6.1: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -5524,6 +5524,19 @@ reduce-component@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/reduce-component/-/reduce-component-1.0.1.tgz#e0c93542c574521bea13df0f9488ed82ab77c5da" +reduce-reducers@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-0.1.2.tgz#fa1b4718bc5292a71ddd1e5d839c9bea9770f14b" + +redux-actions@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/redux-actions/-/redux-actions-2.2.1.tgz#d64186b25649a13c05478547d7cd7537b892410d" + dependencies: + invariant "^2.2.1" + lodash "^4.13.1" + lodash-es "^4.17.4" + reduce-reducers "^0.1.0" + redux-logger@^2.7.4: version "2.10.2" resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-2.10.2.tgz#3c5a5f0a6f32577c1deadf6655f257f82c6c3937" @@ -5672,6 +5685,10 @@ require-uncached@^1.0.2: caller-path "^0.1.0" resolve-from "^1.0.0" +reselect@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147" + resolve-from@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226"