Skip to content

Commit

Permalink
Improve reducers and actions
Browse files Browse the repository at this point in the history
  • Loading branch information
tofumatt committed Nov 10, 2016
1 parent a76ff28 commit db84db9
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 71 deletions.
7 changes: 4 additions & 3 deletions config/default.js
Expand Up @@ -81,6 +81,7 @@ module.exports = {
'trackingEnabled',
'trackingId',
'trackingSendInitPageView',
'validClientApplications',
],

// Content Security Policy.
Expand Down Expand Up @@ -158,11 +159,11 @@ module.exports = {

enablePostCssLoader: true,

// The list of valid client application names. These are derived from UA strings when
// not supplied in the URL.
// The list of valid client application names.
// These are derived from UA strings when not supplied in the URL.
validClientApplications: [
'firefox',
'android',
'firefox',
],

// The default app used in the URL.
Expand Down
43 changes: 20 additions & 23 deletions src/amo/components/Categories.js
Expand Up @@ -7,54 +7,51 @@ import translate from 'core/i18n/translate';
import './Categories.scss';


export function filterAndSortCategories(categories, addonType, clientApp) {
return categories.filter((category) => {
return category.type === addonType && category.application === clientApp;
}).sort((a, b) => {
return a.name > b.name;
});
}

export class CategoriesBase extends React.Component {
static propTypes = {
addonType: PropTypes.string.isRequired,
categories: PropTypes.arrayOf(PropTypes.object),
clientApp: PropTypes.string.isRequired,
error: PropTypes.bool,
loading: PropTypes.bool.isRequired,
i18n: PropTypes.object.isRequired,
}

render() {
const {
addonType, categories, clientApp, error, loading, i18n,
} = this.props;
const { addonType, categories, error, loading, i18n } = this.props;

if (loading && !categories.length) {
return <div>{i18n.gettext('Loading...')}</div>;
if (loading) {
return (
<div className="Categories">
<p>{i18n.gettext('Loading...')}</p>
</div>
);
}

if (error) {
return <div>{i18n.gettext('Failed to load categories.')}</div>;
return (
<div className="Categories">
<p>{i18n.gettext('Failed to load categories.')}</p>
</div>
);
}

const categoriesToShow = filterAndSortCategories(
categories, addonType, clientApp);

if (!loading && !categoriesToShow.length) {
return <div>{i18n.gettext('No categories found.')}</div>;
if (!Object.values(categories).length) {
return (
<div className="Categories">
<p>{i18n.gettext('No categories found.')}</p>
</div>
);
}

return (
<div className="Categories">
<ul className="Categories-list"
ref={(ref) => { this.categories = ref; }}>
{categoriesToShow.map((category) => {
const queryParams = { category: category.slug, type: addonType };
{Object.values(categories).map((category) => {
return (
<li className="Categories-list-item">
<Link className="Categories-link"
to={{ pathname: '/search/', query: queryParams }}>
to={`/${addonType}s/${category.slug}/`}>
{category.name}
</Link>
</li>
Expand Down
12 changes: 9 additions & 3 deletions src/amo/containers/CategoriesPage.js
Expand Up @@ -7,10 +7,16 @@ import { loadCategoriesIfNeeded } from 'core/utils';


export function mapStateToProps(state, ownProps) {
const addonType = ownProps.params.addonType.replace(/s$/, '');
const clientApp = state.api.clientApp;
const categories = state.categories.categories[clientApp][addonType] ?
state.categories.categories[clientApp][addonType] : {};

return {
addonType: ownProps.params.addonType.replace(/s$/, ''),
clientApp: state.api.clientApp,
...state.categories,
addonType,
categories,
error: state.categories.error,
loading: state.categories.loading,
};
}

Expand Down
25 changes: 25 additions & 0 deletions src/amo/containers/CategoryAddons.js
@@ -0,0 +1,25 @@
import { connect } from 'react-redux';
import { asyncConnect } from 'redux-connect';
import { compose } from 'redux';

import SearchPage from 'amo/components/SearchPage';
import {
loadSearchResultsIfNeeded,
mapStateToProps,
} from 'core/containers/SearchPage';
import { loadCategoriesIfNeeded } from 'core/utils';


export default compose(
asyncConnect([
{
deferred: true,
promise: loadSearchResultsIfNeeded,
},
{
deferred: true,
promise: loadCategoriesIfNeeded,
},
]),
connect(mapStateToProps)
)(SearchPage);
4 changes: 1 addition & 3 deletions src/core/actions/categories.js
Expand Up @@ -16,9 +16,7 @@ export function categoriesLoad({ result }) {
type: CATEGORIES_LOAD,
payload: {
loading: false,
results: Object.keys(result).map((key) => {
return result[key];
}),
result,
},
};
}
Expand Down
46 changes: 43 additions & 3 deletions src/core/reducers/categories.js
@@ -1,12 +1,21 @@
import config from 'config';
import {
CATEGORIES_GET,
CATEGORIES_LOAD,
CATEGORIES_FAILED,
} from 'core/constants';


export function emptyCategoryList() {
let categories = {};
config.get('validClientApplications').forEach((appName) => {
categories[appName] = {};
});
return categories;
}

const initialState = {
categories: [],
categories: emptyCategoryList(),
error: false,
loading: false,
};
Expand All @@ -16,13 +25,44 @@ export default function categories(state = initialState, action) {

switch (action.type) {
case CATEGORIES_GET:
return { ...state, ...payload, loading: true, categories: [] };
return { ...state, ...payload, loading: true };
case CATEGORIES_LOAD:
let categories = emptyCategoryList();
payload.result.forEach((result) => {
// If the API returns data for an application we don't support,
// we'll ignore it for now.
if (!categories[result.application]) {
return;
}

if (!categories[result.application][result.type]) {
categories[result.application][result.type] = [];
}

categories[result.application][result.type].push({
...result,
// count: 0,
// page: null,
// results: [],
});
});

config.get('validClientApplications').forEach((appName) => {
Object.keys(categories[appName]).forEach((addonType) => {
categories[appName][addonType] = categories[appName][addonType]
.sort((a, b) => a.name > b.name)
.reduce((object, value, index) => {
object[value.slug] = value;
return object;
}, {});
});
});

return {
...state,
...payload,
loading: false,
categories: payload.results,
categories,
};
case CATEGORIES_FAILED:
return { ...initialState, ...payload, error: true };
Expand Down
21 changes: 1 addition & 20 deletions tests/client/amo/components/TestCategeories.js
Expand Up @@ -7,7 +7,7 @@ import {
import { Provider } from 'react-redux';

import createStore from 'amo/store';
import Categories, { filterAndSortCategories } from 'amo/components/Categories';
import Categories from 'amo/components/Categories';
import { getFakeI18nInst } from 'tests/client/helpers';


Expand Down Expand Up @@ -99,22 +99,3 @@ describe('Categories', () => {
assert.equal(root.textContent, 'Failed to load categories.');
});
});

describe('filterAndSortCategories', () => {
it('filters out categories by addonType and clientApp', () => {
assert.lengthOf(
filterAndSortCategories(categories, 'extension', 'firefox'), 1);
assert.lengthOf(
filterAndSortCategories(categories, 'extension', 'android'), 2);
assert.lengthOf(
filterAndSortCategories(categories, 'theme', 'android'), 1);
});

it('sorts categories by name and weight', () => {
const sortedCategories = filterAndSortCategories(
categories, 'extension', 'android');

assert.equal(sortedCategories[0].slug, 'Games');
assert.equal(sortedCategories[1].slug, 'travel');
});
});
12 changes: 8 additions & 4 deletions tests/client/amo/containers/TestCategoriesPage.js
Expand Up @@ -5,15 +5,19 @@ describe('<CategoriesPage />', () => {
it('maps state to props', () => {
const props = mapStateToProps({
api: { clientApp: 'android', lang: 'pt' },
categories: { categories: [], loading: true },
categories: {
categories: { android: {}, firefox: {} },
error: false,
loading: true,
},
}, {
params: { addonType: 'theme' },
params: { addonType: 'themes' },
});

assert.deepEqual(props, {
addonType: 'theme',
categories: [],
clientApp: 'android',
categories: {},
error: false,
loading: true,
});
});
Expand Down
4 changes: 2 additions & 2 deletions tests/client/core/actions/test_categories.js
Expand Up @@ -17,7 +17,7 @@ describe('CATEGORIES_GET', () => {

describe('CATEGORIES_LOAD', () => {
const params = {
result: { 0: 'foo', 1: 'bar' },
result: ['foo', 'bar'],
loading: false,
};
const action = actions.categoriesLoad(params);
Expand All @@ -28,7 +28,7 @@ describe('CATEGORIES_LOAD', () => {

it('sets the payload', () => {
assert.deepEqual(action.payload.loading, false);
assert.deepEqual(action.payload.results, ['foo', 'bar']);
assert.deepEqual(action.payload.result, ['foo', 'bar']);
});
});

Expand Down

0 comments on commit db84db9

Please sign in to comment.