Skip to content

Commit

Permalink
Add Export button to Bidder Portfolio (#142)
Browse files Browse the repository at this point in the history
  • Loading branch information
mjoyce91 authored and burgwyn committed Apr 15, 2019
1 parent 4738935 commit ba6b385
Show file tree
Hide file tree
Showing 14 changed files with 392 additions and 20 deletions.
2 changes: 2 additions & 0 deletions src/Components/BidderPortfolio/BidControls/BidControls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import TotalResults from './TotalResults';
import SelectForm from '../../SelectForm';
import { BID_PORTFOLIO_SORTS } from '../../../Constants/Sort';
import ResultsViewBy from '../../ResultsViewBy/ResultsViewBy';
import ExportLink from '../ExportLink';

class BidControls extends Component {
constructor(props) {
Expand Down Expand Up @@ -36,6 +37,7 @@ class BidControls extends Component {
onSelectOption={this.onSortChange}
/>
<ResultsViewBy initial={viewType} onClick={changeViewType} />
<ExportLink />
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ exports[`BidControlsComponent matches snapshot when isLoading is false 1`] = `
initial="card"
onClick={[Function]}
/>
<Connect(ExportLink) />
</div>
</div>
</div>
Expand Down Expand Up @@ -103,6 +104,7 @@ exports[`BidControlsComponent matches snapshot when isLoading is true 1`] = `
initial="card"
onClick={[Function]}
/>
<Connect(ExportLink) />
</div>
</div>
</div>
Expand Down
105 changes: 105 additions & 0 deletions src/Components/BidderPortfolio/ExportLink/ExportLink.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { CSVLink } from 'react-csv';
import { get } from 'lodash';
import { bidderPortfolioFetchDataFromLastQuery } from '../../../actions/bidderPortfolio';
import { EMPTY_FUNCTION } from '../../../Constants/PropTypes';

// Mapping columns to data fields
const HEADERS = [
{ label: 'Last Name', key: 'user.last_name' },
{ label: 'First Name', key: 'user.first_name' },
{ label: 'Email', key: 'user.email' },
{ label: 'Username', key: 'user.username' },
{ label: 'Grade', key: 'grade' },
{ label: 'Primary Nationality', key: 'primary_nationality' },
{ label: 'Secondary Nationality', key: 'secondary_nationality' },
{ label: 'Current Assignment', key: 'current_assignment' },
{ label: 'Language', key: 'language_qualifications[0].representation' },
];

// Processes results before sending to the download component to allow for custom formatting.
const processData = data => (
data.map(entry => ({
...entry,
// any other processing we may want to do here
}))
);

export class ExportLink extends Component {
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
this.state = {
data: '',
isLoading: false,
};
}

componentWillReceiveProps(nextProps) {
if (!nextProps.isLoading) {
this.setState({ isLoading: false });
}
if (this.props.isLoading && !nextProps.isLoading && !nextProps.hasErrored) {
const data = processData(nextProps.data.results);
this.setState({ data, isLoading: false }, () => {
if (get(this.csvLink, 'link.click')) {
this.csvLink.link.click();
}
});
}
}

onClick() {
const { isLoading } = this.state;
const { fetchData } = this.props;
if (!isLoading) {
this.setState({
isLoading: true,
}, () => {
fetchData();
});
}
}

render() {
const { data, isLoading } = this.state;
return (
<div className="export-button-container">
<button className="usa-button-secondary" onClick={this.onClick}>
{isLoading && <span className="ds-c-spinner spinner-blue" />}<span>Export</span>
</button>
<CSVLink ref={(x) => { this.csvLink = x; }} target="_blank" filename={this.props.filename} data={data} headers={HEADERS} />
</div>
);
}
}

ExportLink.propTypes = {
filename: PropTypes.string,
hasErrored: PropTypes.bool,
isLoading: PropTypes.bool,
data: PropTypes.shape({ results: PropTypes.arrayOf(PropTypes.shape({})) }),
fetchData: PropTypes.func,
};

ExportLink.defaultProps = {
filename: 'TalentMap_bidder_portfolio_export.csv',
hasErrored: false,
isLoading: false,
data: {},
fetchData: EMPTY_FUNCTION,
};

const mapStateToProps = state => ({
hasErrored: state.lastBidderPortfolioHasErrored,
isLoading: state.lastBidderPortfolioIsLoading,
data: state.lastBidderPortfolio,
});

export const mapDispatchToProps = dispatch => ({
fetchData: () => dispatch(bidderPortfolioFetchDataFromLastQuery()),
});

export default connect(mapStateToProps, mapDispatchToProps)(ExportLink);
40 changes: 40 additions & 0 deletions src/Components/BidderPortfolio/ExportLink/ExportLink.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { shallow } from 'enzyme';
import React from 'react';
import toJSON from 'enzyme-to-json';
import { ExportLink, mapDispatchToProps } from './ExportLink';
import { testDispatchFunctions } from '../../../testUtilities/testUtilities';

describe('SearchResultsExportLink', () => {
it('is defined', () => {
const wrapper = shallow(<ExportLink />);
expect(wrapper).toBeDefined();
});

it('calls onClick on button click', () => {
const wrapper = shallow(<ExportLink />);
expect(wrapper.instance().state.isLoading).toBe(false);
wrapper.find('button').simulate('click');
expect(wrapper.instance().state.isLoading).toBe(true);
});

it('sets state when data is done loading', () => {
const wrapper = shallow(<ExportLink isLoading />);
const instance = wrapper.instance();
const data = { results: [] };
wrapper.setProps({ isLoading: false, hasErrored: false, data });
expect(instance.state.isLoading).toBe(false);
expect(instance.state.data).toEqual(data.results);
});

it('matches snapshot', () => {
const wrapper = shallow(<ExportLink />);
expect(toJSON(wrapper)).toMatchSnapshot();
});
});

describe('mapDispatchToProps', () => {
const config = {
fetchData: [],
};
testDispatchFunctions(mapDispatchToProps, config);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`SearchResultsExportLink matches snapshot 1`] = `
<div
className="export-button-container"
>
<button
className="usa-button-secondary"
onClick={[Function]}
>
<span>
Export
</span>
</button>
<CSVLink
asyncOnClick={false}
data=""
filename="TalentMap_bidder_portfolio_export.csv"
headers={
Array [
Object {
"key": "user.last_name",
"label": "Last Name",
},
Object {
"key": "user.first_name",
"label": "First Name",
},
Object {
"key": "user.email",
"label": "Email",
},
Object {
"key": "user.username",
"label": "Username",
},
Object {
"key": "grade",
"label": "Grade",
},
Object {
"key": "primary_nationality",
"label": "Primary Nationality",
},
Object {
"key": "secondary_nationality",
"label": "Secondary Nationality",
},
Object {
"key": "current_assignment",
"label": "Current Assignment",
},
Object {
"key": "language_qualifications[0].representation",
"label": "Language",
},
]
}
separator=","
target="_blank"
uFEFF={true}
/>
</div>
`;
1 change: 1 addition & 0 deletions src/Components/BidderPortfolio/ExportLink/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './ExportLink';
6 changes: 1 addition & 5 deletions src/Components/BidderPortfolio/TopNav/TopNav.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import React from 'react';
import { BIDDER_PORTFOLIO_COUNTS } from '../../../Constants/PropTypes';
import Navigation from './Navigation';
import Actions from './Actions';

const TopNav = ({ bidderPortfolioCounts }) => (
<div className="usa-grid-full portfolio-top-nav-container">
<div className="usa-width-five-sixths">
<div className="usa-width-one-whole">
<Navigation counts={bidderPortfolioCounts} />
</div>
<div className="usa-width-one-sixth">
<Actions />
</div>
</div>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ exports[`TopNavComponent matches snapshot 1`] = `
className="usa-grid-full portfolio-top-nav-container"
>
<div
className="usa-width-five-sixths"
className="usa-width-one-whole"
>
<Navigation
counts={
Expand All @@ -19,10 +19,5 @@ exports[`TopNavComponent matches snapshot 1`] = `
}
/>
</div>
<div
className="usa-width-one-sixth"
>
<Actions />
</div>
</div>
`;
49 changes: 48 additions & 1 deletion src/actions/bidderPortfolio.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,25 @@ export function bidderPortfolioFetchDataSuccess(results) {
};
}

export function lastBidderPortfolioHasErrored(bool) {
return {
type: 'LAST_BIDDER_PORTFOLIO_HAS_ERRORED',
hasErrored: bool,
};
}
export function lastBidderPortfolioIsLoading(bool) {
return {
type: 'LAST_BIDDER_PORTFOLIO_IS_LOADING',
isLoading: bool,
};
}
export function lastBidderPortfolioFetchDataSuccess(results) {
return {
type: 'LAST_BIDDER_PORTFOLIO_FETCH_DATA_SUCCESS',
results,
};
}

export function bidderPortfolioCountsHasErrored(bool) {
return {
type: 'BIDDER_PORTFOLIO_COUNTS_HAS_ERRORED',
Expand All @@ -38,6 +57,14 @@ export function bidderPortfolioCountsFetchDataSuccess(counts) {
};
}

export function bidderPortfolioLastQuery(query, count) {
return {
type: 'SET_BIDDER_PORTFOLIO_LAST_QUERY',
query,
count,
};
}

export function bidderPortfolioCountsFetchData() {
return (dispatch) => {
dispatch(bidderPortfolioCountsIsLoading(true));
Expand All @@ -57,10 +84,12 @@ export function bidderPortfolioCountsFetchData() {

export function bidderPortfolioFetchData(query = '') {
return (dispatch) => {
const q = `/client/?${query}`;
dispatch(bidderPortfolioIsLoading(true));
dispatch(bidderPortfolioHasErrored(false));
api().get(`/client/?${query}`)
api().get(q)
.then(({ data }) => {
dispatch(bidderPortfolioLastQuery(query, data.count));
dispatch(bidderPortfolioHasErrored(false));
dispatch(bidderPortfolioIsLoading(false));
dispatch(bidderPortfolioFetchDataSuccess(data));
Expand All @@ -71,3 +100,21 @@ export function bidderPortfolioFetchData(query = '') {
});
};
}

export function bidderPortfolioFetchDataFromLastQuery() {
return (dispatch, getState) => {
dispatch(lastBidderPortfolioIsLoading(true));
dispatch(lastBidderPortfolioHasErrored(false));
const q = getState().bidderPortfolioLastQuery;
api().get(q)
.then(({ data }) => {
dispatch(lastBidderPortfolioFetchDataSuccess(data));
dispatch(lastBidderPortfolioHasErrored(false));
dispatch(lastBidderPortfolioIsLoading(false));
})
.catch(() => {
dispatch(lastBidderPortfolioHasErrored(true));
dispatch(lastBidderPortfolioIsLoading(false));
});
};
}
30 changes: 30 additions & 0 deletions src/actions/bidderPortfolio.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,36 @@ describe('bidderPortfolio async actions', () => {
f();
});

it('can fetch data from the last query', (done) => {
const store = mockStore({ results: [] });

const f = () => {
setTimeout(() => {
store.dispatch(actions.bidderPortfolioFetchDataFromLastQuery());
store.dispatch(actions.lastBidderPortfolioIsLoading());
done();
}, 0);
};
f();
});

it('can handle failures when fetching data from the last query', (done) => {
const store = mockStore({ results: [] });

mockAdapter.onGet('http://localhost:8000/api/v1/client/?').reply(404,
null,
);

const f = () => {
setTimeout(() => {
store.dispatch(actions.bidderPortfolioFetchData('q=failure'));
store.dispatch(actions.bidderPortfolioIsLoading());
done();
}, 0);
};
f();
});

it("can handle failures when fetching a CDO's client/bidder list", (done) => {
const store = mockStore({ results: [] });

Expand Down

0 comments on commit ba6b385

Please sign in to comment.