From bbd2a063b11b4c1facb59f59ce0f1bbf0c424f26 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Thu, 9 Nov 2017 17:32:45 -0500 Subject: [PATCH 01/42] Initial ZIP filter implementation --- .../search/filters/location/location.scss | 68 ++++++++++ .../filters/location/LocationPicker.jsx | 119 ++++++++++-------- .../search/filters/location/ZIPField.jsx | 91 ++++++++++++++ .../location/LocationPickerContainer.jsx | 95 +++++++++++++- src/js/helpers/mapHelper.js | 24 ++++ 5 files changed, 339 insertions(+), 58 deletions(-) create mode 100644 src/js/components/search/filters/location/ZIPField.jsx diff --git a/src/_scss/pages/search/filters/location/location.scss b/src/_scss/pages/search/filters/location/location.scss index afc188ff01..112e67e0e6 100644 --- a/src/_scss/pages/search/filters/location/location.scss +++ b/src/_scss/pages/search/filters/location/location.scss @@ -8,6 +8,13 @@ padding: 0 $global-pad rem(20); + .location-picker-divider { + border: none; + border-bottom: 1px solid $color-gray-lighter; + margin-left: rem(5); + margin-right: rem(5); + } + .location-filter-form { padding: 0 rem(5); @@ -41,6 +48,67 @@ cursor: not-allowed; } } + + .zip-field { + .zip-content { + @include display(flex); + @include justify-content(center); + @include align-items(center); + + .zip-input { + @include flex(1 1 auto); + margin-right: rem(4); + font-size: $small-font-size; + color: $color-base; + line-height: rem(36); + height: rem(40); + + @include placeholder { + color: $color-gray-light; + } + } + + .zip-submit { + @include flex(0 0 auto); + + @include display(flex); + @include justify-content(center); + @include align-items(center); + @include button-unstyled; + + background-color: $color-gray-lighter; + border: 1px solid $color-gray-light; + cursor: pointer; + + height: rem(40); + width: rem(50); + + .icon { + @include flex(0 0 auto); + width: rem(20); + height: rem(20); + + svg { + fill: $color-gray; + width: rem(20); + height: rem(20); + } + } + + &:disabled { + background-color: $color-gray-lightest; + border: 1px solid $color-gray-lighter; + cursor: not-allowed; + + .icon { + svg { + fill: $color-gray-light; + } + } + } + } + } + } } .shown { diff --git a/src/js/components/search/filters/location/LocationPicker.jsx b/src/js/components/search/filters/location/LocationPicker.jsx index 4792d7d67c..48d0e2dddb 100644 --- a/src/js/components/search/filters/location/LocationPicker.jsx +++ b/src/js/components/search/filters/location/LocationPicker.jsx @@ -6,6 +6,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import EntityDropdown from './EntityDropdown'; +import ZIPField from './ZIPField'; const propTypes = { selectedLocations: PropTypes.object, @@ -13,6 +14,7 @@ const propTypes = { state: PropTypes.object, county: PropTypes.object, district: PropTypes.object, + zip: PropTypes.object, availableCountries: PropTypes.array, availableStates: PropTypes.array, availableCounties: PropTypes.array, @@ -25,7 +27,8 @@ const propTypes = { clearCounties: PropTypes.func, clearDistricts: PropTypes.func, createLocationObject: PropTypes.func, - addLocation: PropTypes.func + addLocation: PropTypes.func, + validateZip: PropTypes.func }; export default class LocationPicker extends React.Component { @@ -127,60 +130,66 @@ export default class LocationPicker extends React.Component { } return ( -
-
- -
-
- -
-
- -
-
- -
- -
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+ +
); } } diff --git a/src/js/components/search/filters/location/ZIPField.jsx b/src/js/components/search/filters/location/ZIPField.jsx new file mode 100644 index 0000000000..8d75124cb9 --- /dev/null +++ b/src/js/components/search/filters/location/ZIPField.jsx @@ -0,0 +1,91 @@ +/** + * ZIPField.jsx + * Created by Kevin Li 11/9/17 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Search } from 'components/sharedComponents/icons/Icons'; +import Warning from 'components/sharedComponents/autocomplete/Warning'; + +const propTypes = { + zip: PropTypes.object, + validateZip: PropTypes.func +}; + +export default class ZIPField extends React.Component { + constructor(props) { + super(props); + + this.state = { + zip: '', + enabled: false + }; + + this.changedText = this.changedText.bind(this); + this.submitForm = this.submitForm.bind(this); + this.pressedButton = this.pressedButton.bind(this); + } + + changedText(e) { + this.setState({ + zip: e.target.value, + enabled: e.target.value.length === 5 + }); + } + + submitForm(e) { + e.preventDefault(); + } + + pressedButton() { + this.props.validateZip(this.state.zip); + } + + render() { + let error = null; + if (this.props.zip.invalid !== '') { + error = (); + } + + return ( +
+
+ +
+ + +
+ {error} +
+
+ ); + } +} + +ZIPField.propTypes = propTypes; diff --git a/src/js/containers/search/filters/location/LocationPickerContainer.jsx b/src/js/containers/search/filters/location/LocationPickerContainer.jsx index 196a142bce..352d102464 100644 --- a/src/js/containers/search/filters/location/LocationPickerContainer.jsx +++ b/src/js/containers/search/filters/location/LocationPickerContainer.jsx @@ -8,7 +8,7 @@ import PropTypes from 'prop-types'; import { isCancel } from 'axios'; import { concat } from 'lodash'; -import { fetchLocationList } from 'helpers/mapHelper'; +import { fetchLocationList, performZIPGeocode } from 'helpers/mapHelper'; import LocationPicker from 'components/search/filters/location/LocationPicker'; @@ -37,6 +37,10 @@ const defaultSelections = { code: '', district: '', name: '' + }, + zip: { + valid: '', + invalid: '' } }; @@ -52,12 +56,14 @@ export default class LocationPickerContainer extends React.Component { country: Object.assign({}, defaultSelections.country), state: Object.assign({}, defaultSelections.state), county: Object.assign({}, defaultSelections.county), - district: Object.assign({}, defaultSelections.district) + district: Object.assign({}, defaultSelections.district), + zip: Object.assign({}, defaultSelections.zip) }; this.listRequest = null; // keep the CD request seperate bc they may be parallel with county this.districtRequest = null; + this.zipRequest = null; this.loadStates = this.loadStates.bind(this); this.loadCounties = this.loadCounties.bind(this); @@ -71,6 +77,7 @@ export default class LocationPickerContainer extends React.Component { this.createLocationObject = this.createLocationObject.bind(this); this.addLocation = this.addLocation.bind(this); + this.validateZip = this.validateZip.bind(this); } componentDidMount() { @@ -297,6 +304,87 @@ export default class LocationPickerContainer extends React.Component { } } + addZip() { + if (this.state.zip.valid === '') { + // no zip + return; + } + + // make a ZIP location object + const location = { + identifier: `USA_${this.state.zip.valid}`, + display: { + title: this.state.zip.valid, + entity: 'ZIP Code', + standalone: this.state.zip.valid + }, + filter: { + country: 'USA', + zip: this.state.zip.valid + } + }; + + this.props.addLocation(location); + } + + validateZip(zip) { + if (this.zipRequest) { + this.zipRequest.cancel(); + } + + // zip must be 5 characters and all numeric + const zipRegex = /^[0-9]{5}$/; + if (zip.length !== 5 || !zipRegex.test(zip)) { + this.invalidZip(zip); + return; + } + + this.validZip(zip); + + // this.zipRequest = performZIPGeocode(zip); + + // this.zipRequest.promise + // .then((res) => { + // this.parseZip(res.data, zip); + // }) + // .catch((err) => { + // if (!isCancel(err)) { + // console.log(err); + // this.zipRequest = null; + // } + // }); + } + + parseZip(data, zip) { + if (data.features && data.features.length > 0) { + // this is a valid zip code + this.validZip(zip); + } + else { + this.invalidZip(zip); + } + } + + invalidZip(zip) { + this.setState({ + zip: { + valid: '', + invalid: zip + } + }); + } + + validZip(zip) { + this.setState({ + zip: { + valid: zip, + invalid: '' + } + }, () => { + this.addZip(zip); + }); + } + render() { return ( + addLocation={this.addLocation} + validateZip={this.validateZip} /> ); } } diff --git a/src/js/helpers/mapHelper.js b/src/js/helpers/mapHelper.js index 8348b01871..0f903acc51 100644 --- a/src/js/helpers/mapHelper.js +++ b/src/js/helpers/mapHelper.js @@ -6,6 +6,9 @@ import Axios, { CancelToken } from 'axios'; import { min, max } from 'lodash'; import { scaleLinear } from 'd3-scale'; + +import kGlobalConstants from 'GlobalConstants'; + import * as MoneyFormatter from './moneyFormatter'; /* eslint-disable quote-props */ @@ -216,3 +219,24 @@ export const fetchLocationList = (fileName) => { } }; }; + +export const performZIPGeocode = (zip) => { + const source = CancelToken.source(); + return { + promise: Axios.request({ + baseURL: 'https://api.mapbox.com/', + url: `geocoding/v5/mapbox.places/${zip}.json`, + params: { + access_token: kGlobalConstants.MAPBOX_TOKEN, + country: 'us', + types: 'postcode', + autocomplete: 'false' + }, + method: 'get', + cancelToken: source.token + }), + cancel() { + source.cancel(); + } + }; +}; From be1ec7d83ea7252c35e6563daa1161593f550e03 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Wed, 15 Nov 2017 11:50:56 -0500 Subject: [PATCH 02/42] Added tests to zip code --- .../location/LocationPickerContainer.jsx | 28 ++-- .../location/LocationPickerContainer-test.jsx | 152 ++++++++++++++++-- .../search/filters/location/mockMapHelper.js | 56 ++++++- 3 files changed, 207 insertions(+), 29 deletions(-) diff --git a/src/js/containers/search/filters/location/LocationPickerContainer.jsx b/src/js/containers/search/filters/location/LocationPickerContainer.jsx index 352d102464..d19291b9f8 100644 --- a/src/js/containers/search/filters/location/LocationPickerContainer.jsx +++ b/src/js/containers/search/filters/location/LocationPickerContainer.jsx @@ -339,20 +339,20 @@ export default class LocationPickerContainer extends React.Component { return; } - this.validZip(zip); - - // this.zipRequest = performZIPGeocode(zip); - - // this.zipRequest.promise - // .then((res) => { - // this.parseZip(res.data, zip); - // }) - // .catch((err) => { - // if (!isCancel(err)) { - // console.log(err); - // this.zipRequest = null; - // } - // }); + // this.validZip(zip); + + this.zipRequest = performZIPGeocode(zip); + + this.zipRequest.promise + .then((res) => { + this.parseZip(res.data, zip); + }) + .catch((err) => { + if (!isCancel(err)) { + console.log(err); + this.zipRequest = null; + } + }); } parseZip(data, zip) { diff --git a/tests/containers/search/filters/location/LocationPickerContainer-test.jsx b/tests/containers/search/filters/location/LocationPickerContainer-test.jsx index 87ae8c5993..2580aa97ba 100644 --- a/tests/containers/search/filters/location/LocationPickerContainer-test.jsx +++ b/tests/containers/search/filters/location/LocationPickerContainer-test.jsx @@ -9,7 +9,7 @@ import { shallow } from 'enzyme'; import LocationPickerContainer from 'containers/search/filters/location/LocationPickerContainer'; import { mockPickerRedux } from './mockLocations'; -import { _mockCountries, _mockStates, _mockCounties, _mockDistricts } from './mockMapHelper'; +import { mockCountries, mockStates, mockCounties, mockDistricts, mockValidZip, mockInvalidZip } from './mockMapHelper'; global.Promise = require.requireActual('promise'); @@ -22,7 +22,7 @@ describe('LocationPickerContainer', () => { describe('parseCountries', () => { it('should be the API response prepended with USA, all foreign counries, and a divider', () => { const container = shallow(); - container.instance().parseCountries(_mockCountries); + container.instance().parseCountries(mockCountries); expect(container.state().availableCountries).toEqual([ { @@ -44,7 +44,7 @@ describe('LocationPickerContainer', () => { describe('parseStates', () => { it('should be the API response prepended with an All states option', () => { const container = shallow(); - container.instance().parseStates(_mockStates); + container.instance().parseStates(mockStates); expect(container.state().availableStates).toEqual([ { @@ -62,7 +62,7 @@ describe('LocationPickerContainer', () => { describe('parseCounties', () => { it('should be the API response prepended with an All counties option', () => { const container = shallow(); - container.instance().parseCounties(_mockCounties); + container.instance().parseCounties(mockCounties); expect(container.state().availableCounties).toEqual([ { @@ -82,7 +82,7 @@ describe('LocationPickerContainer', () => { describe('parseDistricts', () => { it('should be the API response prepended with an All congressional districts option', () => { const container = shallow(); - container.instance().parseDistricts(_mockDistricts); + container.instance().parseDistricts(mockDistricts); expect(container.state().availableDistricts).toEqual([ { @@ -102,8 +102,8 @@ describe('LocationPickerContainer', () => { it('should clear the available states and reset the selected state to a blank value', () => { const container = shallow(); container.setState({ - availableStates: _mockStates.states, - state: _mockStates.states[0] + availableStates: mockStates.states, + state: mockStates.states[0] }); container.instance().clearStates(); @@ -120,8 +120,8 @@ describe('LocationPickerContainer', () => { it('should clear the available counties and reset the selected county to a blank value', () => { const container = shallow(); container.setState({ - availableCounties: _mockCounties.counties, - county: _mockCounties.counties[0] + availableCounties: mockCounties.counties, + county: mockCounties.counties[0] }); container.instance().clearCounties(); @@ -139,8 +139,8 @@ describe('LocationPickerContainer', () => { it('should clear the available congressional districts and reset the selected congressional district to a blank value', () => { const container = shallow(); container.setState({ - availableDistricts: _mockDistricts.districts, - district: _mockDistricts.districts[0] + availableDistricts: mockDistricts.districts, + district: mockDistricts.districts[0] }); container.instance().clearDistricts(); @@ -460,4 +460,134 @@ describe('LocationPickerContainer', () => { }); }); }); + + describe('validateZip', () => { + it('should make a Mapbox API request to validate the input', async () => { + const container = shallow(); + container.instance().parseZip = jest.fn(); + + container.instance().validateZip('46556'); + expect(container.instance().zipRequest).not.toBeNull(); + await container.instance().zipRequest.promise; + + expect(container.instance().parseZip).toHaveBeenCalledTimes(1); + expect(container.instance().parseZip).toHaveBeenCalledWith(mockValidZip, '46556'); + }); + it('should not attempt to validate if the input is not five digits and all numeric', () => { + const container = shallow(); + container.instance().parseZip = jest.fn(); + container.instance().invalidZip = jest.fn(); + + container.instance().validateZip('W1A 1AA'); + expect(container.instance().zipRequest).toBeNull(); + expect(container.instance().parseZip).toHaveBeenCalledTimes(0); + expect(container.instance().invalidZip).toHaveBeenCalledTimes(1); + expect(container.instance().invalidZip).toHaveBeenCalledWith('W1A 1AA'); + }); + }); + + describe('parseZip', () => { + it('should consider a ZIP code to be valid if the Mapbox API responds with at least one Feature object', () => { + const container = shallow(); + container.instance().validZip = jest.fn(); + container.instance().invalidZip = jest.fn(); + + container.instance().parseZip(mockValidZip, '46556'); + expect(container.instance().validZip).toHaveBeenCalledTimes(1); + expect(container.instance().validZip).toHaveBeenCalledWith('46556'); + expect(container.instance().invalidZip).toHaveBeenCalledTimes(0); + }); + it('should consider a ZIP code to be invalid if Mapbox does not return any features', () => { + const container = shallow(); + container.instance().validZip = jest.fn(); + container.instance().invalidZip = jest.fn(); + + container.instance().parseZip(mockInvalidZip, '00000'); + expect(container.instance().invalidZip).toHaveBeenCalledTimes(1); + expect(container.instance().invalidZip).toHaveBeenCalledWith('00000'); + expect(container.instance().validZip).toHaveBeenCalledTimes(0); + }); + }); + + describe('invalidZip', () => { + it('should update the state with the invalid ZIP and clear any valid ZIP', () => { + const container = shallow(); + container.setState({ + zip: { + valid: '46556', + invalid: '' + } + }); + + expect(container.state().zip.valid).toEqual('46556'); + expect(container.state().zip.invalid).toEqual(''); + + container.instance().invalidZip('W1A 1AA'); + expect(container.state().zip.valid).toEqual(''); + expect(container.state().zip.invalid).toEqual('W1A 1AA'); + }); + }); + + describe('validZip', () => { + it('should update the state with the valid ZIP and clear any invalid ZIP', () => { + const container = shallow(); + container.setState({ + zip: { + valid: '', + invalid: 'W1A 1AA' + } + }); + + expect(container.state().zip.valid).toEqual(''); + expect(container.state().zip.invalid).toEqual('W1A 1AA'); + + container.instance().validZip('46556'); + expect(container.state().zip.valid).toEqual('46556'); + expect(container.state().zip.invalid).toEqual(''); + }); + }); + + describe('addZip', () => { + it('should create an object that contains only a valid ZIP code and US country code to be used as a location filter object', () => { + const mockAdd = jest.fn(); + const mockRedux = Object.assign({}, mockPickerRedux, { + addLocation: mockAdd + }); + const container = shallow(); + container.setState({ + zip: { + valid: '46556', + invalid: '' + } + }); + + const expectedLocation = { + identifier: 'USA_46556', + display: { + title: '46556', + entity: 'ZIP Code', + standalone: '46556' + }, + filter: { + country: 'USA', + zip: '46556' + } + }; + + container.instance().addZip(); + expect(mockAdd).toHaveBeenCalledTimes(1); + expect(mockAdd).toHaveBeenCalledWith(expectedLocation); + }); + + it('should return early if there is no valid ZIP code in the state', () => { + const mockAdd = jest.fn(); + const mockRedux = Object.assign({}, mockPickerRedux, { + addLocation: mockAdd + }); + const container = shallow(); + + container.instance().addZip(); + expect(mockAdd).toHaveBeenCalledTimes(0); + }); + }); }); diff --git a/tests/containers/search/filters/location/mockMapHelper.js b/tests/containers/search/filters/location/mockMapHelper.js index 317a8c04ed..b3ea38b6d7 100644 --- a/tests/containers/search/filters/location/mockMapHelper.js +++ b/tests/containers/search/filters/location/mockMapHelper.js @@ -1,4 +1,4 @@ -export const _mockCountries = { +export const mockCountries = { countries: [ { code: 'ABC', @@ -7,7 +7,7 @@ export const _mockCountries = { ] }; -export const _mockStates = { +export const mockStates = { states: [ { fips: '00', @@ -17,7 +17,7 @@ export const _mockStates = { ] }; -export const _mockCounties = { +export const mockCounties = { counties: [ { code: '0000X', @@ -28,7 +28,7 @@ export const _mockCounties = { ] }; -export const _mockDistricts = { +export const mockDistricts = { districts: [ { code: '00XX', @@ -38,6 +38,36 @@ export const _mockDistricts = { ] }; +export const mockValidZip = { + attribution: 'Fake response', + features: [ + { + bbox: [1, 1, 1, 1], + center: [1, 1], + context: [{ + data: 'fake' + }], + geometry: {}, + id: 'postcode.1', + place_name: '46556, Notre Dame, Indiana, United States', + place_type: ['postcode'], + properties: {}, + relevance: 1, + text: '46556', + type: 'Feature' + } + ], + query: ['46556'], + type: 'FeatureCollection' +}; + +export const mockInvalidZip = { + attribution: 'Fake response', + features: [], + query: ['00000'], + type: 'FeatureCollection' +}; + export const fetchLocationList = (file) => { let response = mockCountries; if (file === 'states') { @@ -61,3 +91,21 @@ export const fetchLocationList = (file) => { } }; }; + +export const performZIPGeocode = (zip) => { + let response = mockValidZip; + if (zip === '00000') { + response = mockInvalidZip; + } + + return { + promise: new Promise((resolve) => { + resolve({ + data: response + }); + }), + cancel() { + jest.fn(); + } + }; +}; From bcaf3b01ebb3fbcb32c6358a35669350ce6b1ff6 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Fri, 17 Nov 2017 12:09:52 -0500 Subject: [PATCH 03/42] Add subawards tab --- src/js/components/award/details/DetailsSection.jsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/js/components/award/details/DetailsSection.jsx b/src/js/components/award/details/DetailsSection.jsx index a055d33a3d..1888301604 100644 --- a/src/js/components/award/details/DetailsSection.jsx +++ b/src/js/components/award/details/DetailsSection.jsx @@ -33,6 +33,11 @@ const commonTabs = [ internal: 'transaction', enabled: true }, + { + label: 'Sub-Awards', + internal: 'subaward', + enabled: true + }, { label: 'Financial System Details', internal: 'financial', From e6a614ede8325209056aa7c17cd1a632f1e36557 Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Mon, 20 Nov 2017 14:24:28 -0500 Subject: [PATCH 04/42] Re-enabling Bulk Download links --- src/_scss/pages/bulkDownload/form/_dateRange.scss | 1 + src/js/components/search/header/NoDownloadHover.jsx | 3 ++- src/js/components/sharedComponents/Footer.jsx | 5 +++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/_scss/pages/bulkDownload/form/_dateRange.scss b/src/_scss/pages/bulkDownload/form/_dateRange.scss index bb32184953..9519dfe53d 100644 --- a/src/_scss/pages/bulkDownload/form/_dateRange.scss +++ b/src/_scss/pages/bulkDownload/form/_dateRange.scss @@ -24,6 +24,7 @@ } } .archive-link-box { + display: none; margin-top: rem(20); background-color: $color-gray-lightest; padding: rem(20) rem(15); diff --git a/src/js/components/search/header/NoDownloadHover.jsx b/src/js/components/search/header/NoDownloadHover.jsx index 781f34ac9a..718bebbf7e 100644 --- a/src/js/components/search/header/NoDownloadHover.jsx +++ b/src/js/components/search/header/NoDownloadHover.jsx @@ -15,7 +15,8 @@ const NoDownloadHover = () => (
- Download functionality is not available when there are more than 500,000 records. Please limit your results with additional filters. + Please visit the Bulk Download page to export + more than 500,000 records or limit your results with additional filters.
diff --git a/src/js/components/sharedComponents/Footer.jsx b/src/js/components/sharedComponents/Footer.jsx index e827a0b769..34f492d0fc 100644 --- a/src/js/components/sharedComponents/Footer.jsx +++ b/src/js/components/sharedComponents/Footer.jsx @@ -73,6 +73,11 @@ export default class Footer extends React.Component { Resources

To ensure that contract data is accurate, the Office of Management and - Budget issues the Federal Government Procurement Data Quality Summary - about data submitted by the agencies to the Federal Procurement Data System - (FPDS).

+ Budget issues the  +
+ Federal Government Procurement Data Quality Summary + +  about data submitted by the agencies to the Federal Procurement + Data System (FPDS).

Additionally, the Inspector General of each agency must issue reports to - Congress on the agency’s compliance with DATA Act requirements. - Go to oversight.gov to see these reports.

-

For more information about the data, see the FAQs and the - Data Dictionary.

+ Congress on the agency's compliance with DATA Act requirements. + Go to  + + oversight.gov + +  to see these reports.

+

For more information about the data, see the  + + FAQs + +  and the  + + Data Dictionary + + . The federal agencies' raw quarterly submission files, + including Qualification Statements about the data, are available  + + here + + .

Data Element Mapping

In updating our system, the names of some of our data elements - may have changed. For a mapping of the updated element’s new names and + may have changed. For a mapping of the updated element's new names and legacy names, please refer to the link below.

diff --git a/src/js/components/about/DataSources.jsx b/src/js/components/about/DataSources.jsx index bc489d3ebd..84cf4b53e4 100644 --- a/src/js/components/about/DataSources.jsx +++ b/src/js/components/about/DataSources.jsx @@ -18,24 +18,28 @@ export default class DataSources extends React.Component {

Connecting the dots across government.

-

As you see in Exhibit 1 below, a lot of information is collected into - USAspending.gov from a variety of government systems. Data is uploaded - directly from hundreds of federal agencies’ financial systems. Data is - also pulled or derived from other government systems. For example, - contract award data is pulled into USAspending.gov daily from FPDS, - the Federal Procurement Data System; Financial Assistance award data is - loaded in from FABS – the Federal Assistance Broker Submission. In the - end, more than 400 points of data are collected.

-

And that’s not all. Entities receiving awards directly from federal agencies - submit data on their sub-awards to FSRS, the FFATA Sub-award Reporting - System. And the businesses that are required to report their Highly - Compensated Executives data do so to SAM – the System for - Award Management.

-

Federal agencies submit contract, grant, loan, and direct payments award - data at least twice a month. That data is published on USAspending.gov - daily. Federal agencies upload data from their financial systems and - link it to the award data quarterly.

-

For more specific information on the data, see the FAQs and the Glossary.

+

As you see in Exhibit 1 below, many sources of information support + USAspending.gov, linking data from a variety of government systems + to improve transparency on federal spending for the public. Data is + uploaded directly from hundreds of federal agencies' financial systems. + Data is also pulled or derived from other government systems. + For example, contract award data is pulled into USAspending.gov daily + from the Federal Procurement Data System Next Generation (FPDS-NG); + Financial Assistance award data is loaded in from the Federal Assistance + Broker Submission system (FABS). In the end, more than 400 points of data + are collected.

+

And that's not all. Entities receiving awards directly from federal + agencies submit data on their sub-awards to the FFATA Sub-award Reporting + System (FSRS). And businesses that are required to report their Highly + Compensated Executives data do so to the System for Award Management + (SAM).

+

Federal agencies submit contract, grant, loan, direct payment, and other + award data at least twice a month to be published on USAspending.gov. + Federal agencies upload data from their financial systems and link it + to the award data quarterly. This quarterly data must be certified by + the agency's Senior Accountable Official before it is displayed on + USAspending.gov.

+

Exhibit 1

Data Sources
diff --git a/src/js/components/about/Mission.jsx b/src/js/components/about/Mission.jsx index 2588ffe769..79ae150b1b 100644 --- a/src/js/components/about/Mission.jsx +++ b/src/js/components/about/Mission.jsx @@ -19,10 +19,11 @@ export default class Mission extends React.Component {

USAspending.gov is the official source for spending data for the U.S. - Government. Its mission is to show the American public how the federal - government spends nearly $4 trillion a year. You can follow the money from - the Congressional appropriations to the federal agencies and down to local - communities and businesses.

+ Government. Its mission is to show the American public what the federal + government spends every year and how it spends the money. You can follow + the money from the Congressional appropriations to the federal agencies + and down to local communities and businesses. +

); From f52f24a9e5407d20d480f51ce7e85d1e0d3b9f6a Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Wed, 22 Nov 2017 12:23:26 -0500 Subject: [PATCH 23/42] Updating "Exhibit 1" text to be a subtitle --- src/js/components/about/DataSources.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/js/components/about/DataSources.jsx b/src/js/components/about/DataSources.jsx index 84cf4b53e4..33a9eb2ec7 100644 --- a/src/js/components/about/DataSources.jsx +++ b/src/js/components/about/DataSources.jsx @@ -39,7 +39,11 @@ export default class DataSources extends React.Component { to the award data quarterly. This quarterly data must be certified by the agency's Senior Accountable Official before it is displayed on USAspending.gov.

-

Exhibit 1

+ +
+

Exhibit 1

+
+
Data Sources
From 79d9a5e0b895b43ea4648e20ff5303b5db6c332e Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Wed, 22 Nov 2017 12:32:00 -0500 Subject: [PATCH 24/42] Removing Data Element Mapping section per feedback --- src/js/components/about/DataQuality.jsx | 29 ------------------------- 1 file changed, 29 deletions(-) diff --git a/src/js/components/about/DataQuality.jsx b/src/js/components/about/DataQuality.jsx index cc499b9730..63cef2ffe1 100644 --- a/src/js/components/about/DataQuality.jsx +++ b/src/js/components/about/DataQuality.jsx @@ -6,16 +6,6 @@ import React from 'react'; export default class DataQuality extends React.Component { - constructor(props) { - super(props); - - this.downloadDataElementMapping = this.downloadDataElementMapping.bind(this); - } - - downloadDataElementMapping() { - window.open('./data/Data Element Mapping.xlsx', '_self'); - } - render() { return (
.

-
-

Data Element Mapping

-
-
-

In updating our system, the names of some of our data elements - may have changed. For a mapping of the updated element's new names and - legacy names, please refer to the link below.

-
-
- -
-
-
); } From 9827cd90b3575d46ee9f967f5198195fdc4d00f2 Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Wed, 22 Nov 2017 13:34:50 -0500 Subject: [PATCH 25/42] Access correct location of CFDA data --- .../FinancialAssistanceDetails.jsx | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/js/components/award/financialAssistance/FinancialAssistanceDetails.jsx b/src/js/components/award/financialAssistance/FinancialAssistanceDetails.jsx index 34cf852639..8101a1168a 100644 --- a/src/js/components/award/financialAssistance/FinancialAssistanceDetails.jsx +++ b/src/js/components/award/financialAssistance/FinancialAssistanceDetails.jsx @@ -132,21 +132,21 @@ export default class FinancialAssistanceDetails extends React.Component { let programName = 'Not Available'; let programDescription = 'Not Available'; - if (latestTransaction.assistance_data.cfda) { - const cfda = latestTransaction.assistance_data.cfda; - if (cfda.program_number && cfda.program_title) { - programName = `${latestTransaction.assistance_data.cfda.program_number} - \ -${latestTransaction.assistance_data.cfda.program_title}`; + if (latestTransaction.assistance_data) { + const assistanceData = latestTransaction.assistance_data; + + if (assistanceData.cfda_number && assistanceData.cfda_title) { + programName = `${assistanceData.cfda_number} - ${assistanceData.cfda_title}`; } - else if (cfda.program_number) { - programName = cfda.program_number; + else if (assistanceData.cfda_number) { + programName = assistanceData.cfda_title; } - else if (cfda.program_title) { - programName = cfda.program_title; + else if (assistanceData.program_title) { + programName = assistanceData.cfda_title; } - if (cfda.objectives) { - programDescription = cfda.objectives; + if (assistanceData.cfda_objectives) { + programDescription = assistanceData.cfda_objectives; } } @@ -155,8 +155,6 @@ ${latestTransaction.assistance_data.cfda.program_title}`; cfdaOverflow = true; } - // Todo - Mike Bray - update typeDesc when available from Broker data - // Probably "assistance_type" this.setState({ description, programName, From 0beaf7abdeea15ffb1f177f0bc1fcb6254354199 Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Wed, 22 Nov 2017 14:59:58 -0500 Subject: [PATCH 26/42] Generating correct string for Period of Performance --- .../FinancialAssistanceDetails.jsx | 49 +++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/src/js/components/award/financialAssistance/FinancialAssistanceDetails.jsx b/src/js/components/award/financialAssistance/FinancialAssistanceDetails.jsx index 8101a1168a..6e429cca2d 100644 --- a/src/js/components/award/financialAssistance/FinancialAssistanceDetails.jsx +++ b/src/js/components/award/financialAssistance/FinancialAssistanceDetails.jsx @@ -96,25 +96,54 @@ export default class FinancialAssistanceDetails extends React.Component { prepareValues() { let yearRangeTotal = ""; + let monthRangeTotal = ""; let description = null; const award = this.props.selectedAward; const latestTransaction = award.latest_transaction; // Date Range - const startDate = moment(award.period_of_performance_start_date, 'M/D/YYYY'); - const endDate = moment(award.period_of_performance_current_end_date, 'M/D/YYYY'); - const yearRange = endDate.diff(startDate, 'year'); + const formattedStartDate = award.period_of_performance_start_date; + const formattedEndDate = award.period_of_performance_current_end_date; + + const startDate = moment(formattedStartDate, 'M/D/YYYY'); + const endDate = moment(formattedEndDate, 'M/D/YYYY'); + + const duration = moment.duration(endDate.diff(startDate)); + const years = duration.years(); + const months = duration.months(); + let popDate = "Not Available"; - if (!isNaN(yearRange) && yearRange !== 0) { - if (yearRange === 1) { - yearRangeTotal = `${yearRange} year)`; + if (!isNaN(years)) { + if (months > 0) { + if (months === 1) { + monthRangeTotal = `${months} month`; + } + else { + monthRangeTotal = `${months} months`; + } + } + + if (years > 0) { + if (years === 1) { + yearRangeTotal = `${years} year`; + } + else { + yearRangeTotal = `${years} years`; + } + } + + let timeRange = ''; + if (monthRangeTotal && yearRangeTotal) { + timeRange = `(${yearRangeTotal}, ${monthRangeTotal})`; + } + else if (monthRangeTotal) { + timeRange = `(${monthRangeTotal})`; } - else { - yearRangeTotal = `(${yearRange} years)`; + else if (yearRangeTotal) { + timeRange = `(${yearRangeTotal})`; } - popDate = `${award.period_of_performance_start_date} - - ${award.period_of_performance_current_end_date} ${yearRangeTotal}`; + popDate = `${formattedStartDate} - ${formattedEndDate} ${timeRange}`; } if (award.description) { From f09e1c18b7c5413b67e7c831999a43a18e9bd670 Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Wed, 22 Nov 2017 15:17:47 -0500 Subject: [PATCH 27/42] Updating ContractDetails to use same methodology --- .../award/contract/ContractDetails.jsx | 63 +++++++++++-------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/src/js/components/award/contract/ContractDetails.jsx b/src/js/components/award/contract/ContractDetails.jsx index f228d05de5..b4b518de33 100644 --- a/src/js/components/award/contract/ContractDetails.jsx +++ b/src/js/components/award/contract/ContractDetails.jsx @@ -95,40 +95,53 @@ export default class ContractDetails extends React.Component { prepareValues(award) { let yearRangeTotal = ""; + let monthRangeTotal = ""; let description = null; // Date Range - const startDate = moment(award.period_of_performance_start_date, 'M/D/YYYY'); - const endDate = moment(award.period_of_performance_current_end_date, 'M/D/YYYY'); - const yearRange = endDate.diff(startDate, 'year'); - const monthRange = (endDate.diff(startDate, 'month') - (yearRange * 12)); - if (yearRange !== 0 && !Number.isNaN(yearRange)) { - if (yearRange === 1) { - yearRangeTotal = `${yearRange} year`; - } - else { - yearRangeTotal = `${yearRange} years`; + const formattedStartDate = award.period_of_performance_start_date; + const formattedEndDate = award.period_of_performance_current_end_date; + + const startDate = moment(formattedStartDate, 'M/D/YYYY'); + const endDate = moment(formattedEndDate, 'M/D/YYYY'); + + const duration = moment.duration(endDate.diff(startDate)); + const years = duration.years(); + const months = duration.months(); + + let popDate = "Not Available"; + if (!isNaN(years)) { + if (months > 0) { + if (months === 1) { + monthRangeTotal = `${months} month`; + } + else { + monthRangeTotal = `${months} months`; + } } - if (monthRange > 0) { - yearRangeTotal += ' '; + + if (years > 0) { + if (years === 1) { + yearRangeTotal = `${years} year`; + } + else { + yearRangeTotal = `${years} years`; + } } - } - if (monthRange >= 1) { - if (yearRange < 1 && monthRange > 1) { - yearRangeTotal = `${monthRange} months`; + + let timeRange = ''; + if (monthRangeTotal && yearRangeTotal) { + timeRange = `(${yearRangeTotal}, ${monthRangeTotal})`; } - else if (monthRange === 1) { - yearRangeTotal += `${monthRange} month`; + else if (monthRangeTotal) { + timeRange = `(${monthRangeTotal})`; } - else { - yearRangeTotal += `${monthRange} months`; + else if (yearRangeTotal) { + timeRange = `(${yearRangeTotal})`; } + + popDate = `${formattedStartDate} - ${formattedEndDate} ${timeRange}`; } - if (yearRangeTotal) { - yearRangeTotal = `(${yearRangeTotal})`; - } - const popDate = `${award.period_of_performance_start_date} - - ${award.period_of_performance_current_end_date} ${yearRangeTotal}`; if (award.description) { description = award.description; From 6ed4e128fb301d551a4349b01d863b734e5cddd8 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Wed, 22 Nov 2017 17:13:38 -0500 Subject: [PATCH 28/42] Remove table column picker --- .../components/search/table/ResultsTableSection.jsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/js/components/search/table/ResultsTableSection.jsx b/src/js/components/search/table/ResultsTableSection.jsx index 18527f9c4e..0dbc386ea0 100644 --- a/src/js/components/search/table/ResultsTableSection.jsx +++ b/src/js/components/search/table/ResultsTableSection.jsx @@ -14,8 +14,6 @@ import ExtraModalContainer from 'containers/search/modals/tableDownload/ExtraMod import ResultsTable from './ResultsTable'; import ResultsTableTabs from './ResultsTableTabs'; import ResultsTableMessage from './ResultsTableMessage'; -import ResultsTablePicker from './ResultsTablePicker'; -import ResultsSelectColumns from './ResultsSelectColumns'; const propTypes = { inFlight: PropTypes.bool, @@ -90,16 +88,6 @@ export default class ResultsTableSection extends React.Component {

Spending by Award


-
- - -
Date: Mon, 27 Nov 2017 12:53:52 -0500 Subject: [PATCH 29/42] Updated the agency filter title --- src/js/components/bulkDownload/awards/filters/AgencyFilter.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/components/bulkDownload/awards/filters/AgencyFilter.jsx b/src/js/components/bulkDownload/awards/filters/AgencyFilter.jsx index aa37dc4780..6bdc456ddd 100644 --- a/src/js/components/bulkDownload/awards/filters/AgencyFilter.jsx +++ b/src/js/components/bulkDownload/awards/filters/AgencyFilter.jsx @@ -170,7 +170,7 @@ export default class AgencyFilter extends React.Component { return (
- {icon} Select an agency and sub-agency. + {icon} Select an awarding agency and sub-agency.
From 855b481438a958801e9b7b1e96d62282b4dafc69 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Mon, 27 Nov 2017 12:58:15 -0500 Subject: [PATCH 30/42] Remove rank visualization --- src/js/components/search/SearchResults.jsx | 3 --- src/js/components/search/header/SearchHeader.jsx | 10 +--------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/js/components/search/SearchResults.jsx b/src/js/components/search/SearchResults.jsx index 36eabdb9f6..e8481bf232 100644 --- a/src/js/components/search/SearchResults.jsx +++ b/src/js/components/search/SearchResults.jsx @@ -13,8 +13,6 @@ import TopFilterBarContainer from 'containers/search/topFilterBar/TopFilterBarCo import TimeVisualizationSectionContainer from 'containers/search/visualizations/time/TimeVisualizationSectionContainer'; -import RankVisualizationWrapperContainer from - 'containers/search/visualizations/rank/RankVisualizationWrapperContainer'; import GeoVisualizationSectionContainer from 'containers/search/visualizations/geo/GeoVisualizationSectionContainer'; @@ -104,7 +102,6 @@ export default class SearchResults extends React.Component { {lastUpdate}
-
diff --git a/src/js/components/search/header/SearchHeader.jsx b/src/js/components/search/header/SearchHeader.jsx index e2f7766694..34df0cfcd3 100644 --- a/src/js/components/search/header/SearchHeader.jsx +++ b/src/js/components/search/header/SearchHeader.jsx @@ -17,7 +17,7 @@ const propTypes = { downloadAvailable: PropTypes.bool }; -const sectionList = ['time', 'rank', 'geo', 'table']; +const sectionList = ['time', 'geo', 'table']; export default class SearchHeader extends React.Component { constructor(props) { @@ -177,14 +177,6 @@ export default class SearchHeader extends React.Component { accessibleLabel="Organize spending by time periods" icon={} /> -
  • - } /> -
  • Date: Mon, 27 Nov 2017 14:20:32 -0500 Subject: [PATCH 31/42] Require time period always be between FY 2008 and present --- package.json | 2 +- src/js/models/search/SearchAwardsOperation.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index ee96a7a58e..9d5064e52f 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "react-aria-modal": "^2.7.0", "react-copy-to-clipboard": "^5.0.1", "react-custom-scrollbars": "^4.1.2", - "react-day-picker": "^6.1.0", + "react-day-picker": "^7.0.2", "react-dnd": "^2.4.0", "react-dnd-html5-backend": "^2.4.1", "react-dom": "15.6.1", diff --git a/src/js/models/search/SearchAwardsOperation.js b/src/js/models/search/SearchAwardsOperation.js index b0d73e230c..1dd9eb9a77 100644 --- a/src/js/models/search/SearchAwardsOperation.js +++ b/src/js/models/search/SearchAwardsOperation.js @@ -110,6 +110,20 @@ class SearchAwardsOperation { } } + if ((this.timePeriodType === 'fy' && this.timePeriodFY.length === 0) || + (this.timePeriodType === 'dr' && this.timePeriodRange.length === 0)) { + // the user selected fiscal years but did not specify any years OR + // the user has selected the date range type but has not entered any dates yet + // this should default to a period of time from FY 2008 to present + const initialYear = 2008; + const currentYear = FiscalYearHelper.currentFiscalYear(); + + filters[rootKeys.timePeriod] = [{ + [timePeriodKeys.startDate]: FiscalYearHelper.convertFYToDateRange(initialYear)[0], + [timePeriodKeys.endDate]: FiscalYearHelper.convertFYToDateRange(currentYear)[1] + }]; + } + // Add award types if (this.awardType.length > 0) { filters[rootKeys.awardType] = this.awardType; From 663e0992327dde014e5621f581d79c02d36fbb30 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Mon, 27 Nov 2017 14:23:36 -0500 Subject: [PATCH 32/42] Remove award status label --- src/js/components/award/SummaryBar.jsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/js/components/award/SummaryBar.jsx b/src/js/components/award/SummaryBar.jsx index 9e8f10ce5a..14ef3e5952 100644 --- a/src/js/components/award/SummaryBar.jsx +++ b/src/js/components/award/SummaryBar.jsx @@ -91,9 +91,6 @@ export default class SummaryBar extends React.Component { label="Award ID" value={this.props.selectedAward.award_id} /> { parentAwardId } -
  • From e184d00c9b09bed7cedd0cf9be838546538a6667 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Mon, 27 Nov 2017 14:40:18 -0500 Subject: [PATCH 33/42] Disabling days --- src/_scss/all.scss | 1 - .../sharedComponents/DatePicker.jsx | 22 ++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/_scss/all.scss b/src/_scss/all.scss index d6f56b3839..1f9ac4f260 100644 --- a/src/_scss/all.scss +++ b/src/_scss/all.scss @@ -2,7 +2,6 @@ @import 'lib/bourbon/bourbon'; @import 'lib/neat/neat'; @import 'lib/normalize'; -// @import 'lib/mapbox/mapbox'; // Core -------------- // @import 'core/defaults'; diff --git a/src/js/components/sharedComponents/DatePicker.jsx b/src/js/components/sharedComponents/DatePicker.jsx index de2a395e0f..cbff07b515 100644 --- a/src/js/components/sharedComponents/DatePicker.jsx +++ b/src/js/components/sharedComponents/DatePicker.jsx @@ -12,7 +12,8 @@ import * as Icons from './icons/Icons'; const defaultProps = { type: 'startDate', tabIndex: 1, - allowClearing: false + allowClearing: false, + disabledDays: [] }; const propTypes = { @@ -24,7 +25,8 @@ const propTypes = { opposite: PropTypes.object, tabIndex: PropTypes.number, title: PropTypes.string, - allowClearing: PropTypes.bool + allowClearing: PropTypes.bool, + disabledDays: PropTypes.array }; export default class DatePicker extends React.Component { @@ -214,18 +216,18 @@ export default class DatePicker extends React.Component { // handle the cutoff dates (preventing end dates from coming before // start dates or vice versa) - let cutoffFunc = null; + const disabledDays = this.props.disabledDays; if (this.props.type === 'startDate' && this.props.opposite) { // the cutoff date represents the latest possible date - cutoffFunc = (day) => ( - moment(day).isAfter(this.props.opposite) - ); + disabledDays.push({ + after: this.props.opposite.toDate() + }); } else if (this.props.type === 'endDate' && this.props.opposite) { // cutoff date represents the earliest possible date - cutoffFunc = (day) => ( - moment(day).isBefore(this.props.opposite) - ); + disabledDays.push({ + before: this.props.opposite.toDate() + }); } return ( @@ -256,7 +258,7 @@ export default class DatePicker extends React.Component { this.datepicker = daypicker; }} initialMonth={pickedDay} - disabledDays={cutoffFunc} + disabledDays={disabledDays} selectedDays={(day) => DateUtils.isSameDay(pickedDay, day)} onDayClick={this.handleDatePick} onFocus={this.handleDateFocus} From a9d66593e566cfec5455d130a8aa9c2a1e45d96b Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Mon, 27 Nov 2017 14:41:44 -0500 Subject: [PATCH 34/42] Remove ellipsis button --- src/js/components/award/SummaryBar.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/js/components/award/SummaryBar.jsx b/src/js/components/award/SummaryBar.jsx index 14ef3e5952..c6d7c5974b 100644 --- a/src/js/components/award/SummaryBar.jsx +++ b/src/js/components/award/SummaryBar.jsx @@ -11,7 +11,6 @@ import * as SummaryPageHelper from 'helpers/summaryPageHelper'; import { awardTypeGroups } from 'dataMapping/search/awardType'; import InfoSnippet from './InfoSnippet'; -import MoreHeaderOptions from './MoreHeaderOptions'; const propTypes = { selectedAward: PropTypes.object @@ -92,7 +91,6 @@ export default class SummaryBar extends React.Component { value={this.props.selectedAward.award_id} /> { parentAwardId } -
    From 82ec397ed1c172546e03927a048ee2eca403fa06 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Mon, 27 Nov 2017 15:02:37 -0500 Subject: [PATCH 35/42] Disable date picker dates before FY 2008, fill open-ended date ranges to be limited within 2008 to present --- .../components/datePicker/_datePicker.scss | 4 ++++ .../search/filters/timePeriod/DateRange.jsx | 11 ++++++++++ src/js/models/search/SearchAwardsOperation.js | 21 ++++++++++++++++--- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/_scss/components/datePicker/_datePicker.scss b/src/_scss/components/datePicker/_datePicker.scss index ee056a521e..82b091b008 100644 --- a/src/_scss/components/datePicker/_datePicker.scss +++ b/src/_scss/components/datePicker/_datePicker.scss @@ -101,6 +101,10 @@ font-size: 1.3rem; } } + .DayPicker-Day--disabled { + opacity: 0.4; + pointer-events: none; + } } } diff --git a/src/js/components/search/filters/timePeriod/DateRange.jsx b/src/js/components/search/filters/timePeriod/DateRange.jsx index 1ba149c201..d00c6ad176 100644 --- a/src/js/components/search/filters/timePeriod/DateRange.jsx +++ b/src/js/components/search/filters/timePeriod/DateRange.jsx @@ -5,8 +5,10 @@ import React from 'react'; import PropTypes from 'prop-types'; +import moment from 'moment'; import DatePicker from 'components/sharedComponents/DatePicker'; import { AngleRight } from 'components/sharedComponents/icons/Icons'; +import * as FiscalYearHelper from 'helpers/fiscalYearHelper'; const defaultProps = { startDate: '01/01/2016', @@ -55,6 +57,9 @@ export default class DateRange extends React.Component { } render() { + const earliestDateString = + FiscalYearHelper.convertFYToDateRange(FiscalYearHelper.earliestFiscalYear)[0]; + const earliestDate = moment(earliestDateString, 'YYYY-MM-DD').toDate(); return (
    { this.startPicker = component; }} @@ -82,6 +90,9 @@ export default class DateRange extends React.Component { opposite={this.props.startDate} showError={this.props.showError} hideError={this.props.hideError} + disabledDays={[{ + before: earliestDate + }]} ref={(component) => { this.endPicker = component; }} diff --git a/src/js/models/search/SearchAwardsOperation.js b/src/js/models/search/SearchAwardsOperation.js index 1dd9eb9a77..dfaff787f9 100644 --- a/src/js/models/search/SearchAwardsOperation.js +++ b/src/js/models/search/SearchAwardsOperation.js @@ -101,10 +101,25 @@ class SearchAwardsOperation { }); } else if (this.timePeriodType === 'dr' && this.timePeriodRange.length > 0) { + let start = this.timePeriodRange[0]; + let end = this.timePeriodRange[1]; + + // if no start or end date is provided, use the 2008-present date range to fill out + // the missing dates + const initialYear = FiscalYearHelper.earliestFiscalYear; + const currentYear = FiscalYearHelper.currentFiscalYear(); + + if (!start) { + start = FiscalYearHelper.convertFYToDateRange(initialYear)[0]; + } + if (!end) { + end = FiscalYearHelper.convertFYToDateRange(currentYear)[1]; + } + filters[rootKeys.timePeriod] = [ { - [timePeriodKeys.startDate]: this.timePeriodRange[0], - [timePeriodKeys.endDate]: this.timePeriodRange[1] + [timePeriodKeys.startDate]: start, + [timePeriodKeys.endDate]: end } ]; } @@ -115,7 +130,7 @@ class SearchAwardsOperation { // the user selected fiscal years but did not specify any years OR // the user has selected the date range type but has not entered any dates yet // this should default to a period of time from FY 2008 to present - const initialYear = 2008; + const initialYear = FiscalYearHelper.earliestFiscalYear; const currentYear = FiscalYearHelper.currentFiscalYear(); filters[rootKeys.timePeriod] = [{ From 7f14a2c9e228bc2e8c67ffca4de71d339591c580 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Tue, 28 Nov 2017 11:27:01 -0500 Subject: [PATCH 36/42] Delete v1 download, allow only descending sort on award search page --- .../search/results/table/_tableStyle.scss | 10 ++ .../search/modals/tableDownload/CylonEye.jsx | 19 --- .../modals/tableDownload/DownloadLocation.jsx | 73 ---------- .../modals/tableDownload/ExtraModal.jsx | 77 ---------- .../modals/tableDownload/ExtraModalTabs.jsx | 23 --- .../search/table/ResultsTableSection.jsx | 26 +--- .../tableDownload/ExtraModalContainer.jsx | 130 ----------------- .../search/table/ResultsTableContainer.jsx | 39 ++--- .../table/ResultsTableHeaderCellContainer.jsx | 12 +- .../ExtraModalContainer-test.jsx | 136 ------------------ .../modals/tableDownload/mockDownload.js | 23 --- .../table/ResultsTableContainer-test.jsx | 2 +- 12 files changed, 32 insertions(+), 538 deletions(-) delete mode 100644 src/js/components/search/modals/tableDownload/CylonEye.jsx delete mode 100644 src/js/components/search/modals/tableDownload/DownloadLocation.jsx delete mode 100644 src/js/components/search/modals/tableDownload/ExtraModal.jsx delete mode 100644 src/js/components/search/modals/tableDownload/ExtraModalTabs.jsx delete mode 100644 src/js/containers/search/modals/tableDownload/ExtraModalContainer.jsx delete mode 100644 tests/containers/search/modals/tableDownload/ExtraModalContainer-test.jsx delete mode 100644 tests/containers/search/modals/tableDownload/mockDownload.js diff --git a/src/_scss/pages/search/results/table/_tableStyle.scss b/src/_scss/pages/search/results/table/_tableStyle.scss index 10bba89391..8534bca350 100644 --- a/src/_scss/pages/search/results/table/_tableStyle.scss +++ b/src/_scss/pages/search/results/table/_tableStyle.scss @@ -19,6 +19,16 @@ &.last-column { border-right: none; } + + .cell-content { + .header-sort .header-icons { + .sort-icon{ + &:first-child { + display: none; + } + } + } + } } // gray out even rows .row-even { diff --git a/src/js/components/search/modals/tableDownload/CylonEye.jsx b/src/js/components/search/modals/tableDownload/CylonEye.jsx deleted file mode 100644 index 14944d3ff2..0000000000 --- a/src/js/components/search/modals/tableDownload/CylonEye.jsx +++ /dev/null @@ -1,19 +0,0 @@ -/** - * CylonEye.jsx - * Created by Kevin Li 5/9/17 - */ - -import React from 'react'; - -export default class CylonEye extends React.Component { - render() { - return ( -
    -
    -
    -
    -
    -
    - ); - } -} diff --git a/src/js/components/search/modals/tableDownload/DownloadLocation.jsx b/src/js/components/search/modals/tableDownload/DownloadLocation.jsx deleted file mode 100644 index bfe2ff9390..0000000000 --- a/src/js/components/search/modals/tableDownload/DownloadLocation.jsx +++ /dev/null @@ -1,73 +0,0 @@ -/** - * DownloadLocation.jsx - * Created by Kevin Li 5/9/17 - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import Clipboard from 'clipboard'; - -const propTypes = { - location: PropTypes.string -}; - -export default class DownloadLocation extends React.Component { - constructor(props) { - super(props); - - this.state = { - button: 'Copy URL' - }; - - this.clipboard = null; - this.copiedLink = this.copiedLink.bind(this); - } - - componentDidMount() { - this.mounted = true; - this.clipboard = new Clipboard('.download-location-copy'); - this.clipboard.on('success', this.copiedLink); - } - - componentWillUnmount() { - this.mounted = false; - this.clipboard.destroy(); - } - - copiedLink() { - this.setState({ - button: 'Copied!' - }, () => { - // restore the button text after 2 seconds - window.setTimeout(() => { - if (this.mounted) { - // but only change the state if the component is still mounted - this.setState({ - button: 'Copy URL' - }); - } - }, 2000); - }); - } - - render() { - return ( -
    - - -
    - ); - } -} - -DownloadLocation.propTypes = propTypes; diff --git a/src/js/components/search/modals/tableDownload/ExtraModal.jsx b/src/js/components/search/modals/tableDownload/ExtraModal.jsx deleted file mode 100644 index 3a9c1826ef..0000000000 --- a/src/js/components/search/modals/tableDownload/ExtraModal.jsx +++ /dev/null @@ -1,77 +0,0 @@ -/** - * DownloadModal.jsx - * Created by Kevin Li 5/2/17 - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import Modal from 'react-aria-modal'; - -import { Close } from 'components/sharedComponents/icons/Icons'; - -import ExtraModalTabs from './ExtraModalTabs'; -import CylonEye from './CylonEye'; -import DownloadLocation from './DownloadLocation'; - -const propTypes = { - mounted: PropTypes.bool, - hideModal: PropTypes.func, - title: PropTypes.string, - message: PropTypes.string, - location: PropTypes.string, - animate: PropTypes.bool -}; - -export default class ExtraModal extends React.Component { - render() { - let animation = null; - if (this.props.animate) { - animation = ; - } - - let location = null; - if (this.props.location && this.props.location !== '') { - location = ; - } - - return ( - -
    -
    - - - -
    - -
    -

    - {this.props.title} -

    - - {animation} - -
    - {this.props.message} -
    - - {location} -
    -
    -
    - ); - } -} - -ExtraModal.propTypes = propTypes; diff --git a/src/js/components/search/modals/tableDownload/ExtraModalTabs.jsx b/src/js/components/search/modals/tableDownload/ExtraModalTabs.jsx deleted file mode 100644 index 6366aec56e..0000000000 --- a/src/js/components/search/modals/tableDownload/ExtraModalTabs.jsx +++ /dev/null @@ -1,23 +0,0 @@ -/** - * ExtraModalTabs.jsx - * Created by Kevin Li 5/2/17 - */ - -import React from 'react'; - -export default class ExtraModalTabs extends React.Component { - render() { - return ( -
    -
      -
    • - -
    • -
    -
    - ); - } -} diff --git a/src/js/components/search/table/ResultsTableSection.jsx b/src/js/components/search/table/ResultsTableSection.jsx index 0dbc386ea0..b86b3dd239 100644 --- a/src/js/components/search/table/ResultsTableSection.jsx +++ b/src/js/components/search/table/ResultsTableSection.jsx @@ -9,8 +9,6 @@ import PropTypes from 'prop-types'; import ResultsTableHeaderCellContainer from 'containers/search/table/ResultsTableHeaderCellContainer'; -import ExtraModalContainer from 'containers/search/modals/tableDownload/ExtraModalContainer'; - import ResultsTable from './ResultsTable'; import ResultsTableTabs from './ResultsTableTabs'; import ResultsTableMessage from './ResultsTableMessage'; @@ -24,8 +22,7 @@ const propTypes = { columns: PropTypes.object, counts: PropTypes.object, toggleColumnVisibility: PropTypes.func, - reorderColumns: PropTypes.func, - downloadParams: PropTypes.object + reorderColumns: PropTypes.func }; export default class ResultsTableSection extends React.Component { @@ -33,13 +30,10 @@ export default class ResultsTableSection extends React.Component { super(props); this.state = { - tableWidth: 0, - showModal: false + tableWidth: 0 }; this.setTableWidth = this.setTableWidth.bind(this); - this.showModal = this.showModal.bind(this); - this.hideModal = this.hideModal.bind(this); } componentDidMount() { // set the initial table width @@ -58,18 +52,6 @@ export default class ResultsTableSection extends React.Component { this.setState({ tableWidth }); } - showModal() { - this.setState({ - showModal: true - }); - } - - hideModal() { - this.setState({ - showModal: false - }); - } - render() { let loadingWrapper = 'loaded-table'; let message = null; @@ -107,10 +89,6 @@ export default class ResultsTableSection extends React.Component { headerCellClass={ResultsTableHeaderCellContainer} />
    {message} -
    ); } diff --git a/src/js/containers/search/modals/tableDownload/ExtraModalContainer.jsx b/src/js/containers/search/modals/tableDownload/ExtraModalContainer.jsx deleted file mode 100644 index 51f5ef99d7..0000000000 --- a/src/js/containers/search/modals/tableDownload/ExtraModalContainer.jsx +++ /dev/null @@ -1,130 +0,0 @@ -/** - * ExtraModalContainer.jsx - * Created by Kevin Li 5/5/17 - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { isCancel } from 'axios'; - -import ExtraModal from 'components/search/modals/tableDownload/ExtraModal'; - -import * as DownloadHelper from 'helpers/downloadHelper'; - -const propTypes = { - downloadParams: PropTypes.object, - mounted: PropTypes.bool -}; - -export class ExtraModalContainer extends React.Component { - constructor(props) { - super(props); - - this.state = { - title: 'A link to the file is being generated.', - message: 'Requesting file...', - activeParams: '', - location: '', - animate: true - }; - - this.request = null; - this.statusChecker = null; - } - - componentDidUpdate(prevProps) { - if (prevProps.mounted !== this.props.mounted && this.props.mounted) { - this.modalOpened(); - } - } - - - modalOpened() { - if (this.props.downloadParams !== {} && this.props.downloadParams !== this.state.activeParams) { - this.setState({ - activeParams: this.props.downloadParams - }, () => { - this.requestDownload(); - }); - } - } - - requestDownload() { - if (this.request) { - this.request.cancel(); - } - - this.setState({ - animate: true, - location: '', - title: 'A link to the file is being generated.' - }); - - this.request = DownloadHelper.requestAwardTable( - this.props.downloadParams - ); - - this.request.promise - .then((res) => { - this.parseResponse(res.data); - }) - .catch((err) => { - if (!isCancel(err)) { - if (err.response && err.response.data) { - let message = `Error: ${err.response.statusText} (${err.response.status})`; - if (err.response.data.status) { - message = err.response.data.status; - } - - this.setState({ - message, - title: 'An error occurred while generating the file.', - location: '', - animate: false - }); - } - - console.log(err); - } - }); - } - - parseResponse(data) { - let title = 'A link to the file is being generated.'; - let animate = true; - - let done = false; - - if (data.location && data.location !== '') { - title = 'A link to the file has been generated successfully.'; - animate = false; - done = true; - } - - this.setState({ - title, - animate, - message: data.status, - location: data.location - }, () => { - if (!done && this.props.mounted) { - // keep checking every 30 seconds - this.statusChecker = window.setTimeout(() => { - this.requestDownload(); - }, 30 * 1000); - } - }); - } - - render() { - return ( - - ); - } -} - -ExtraModalContainer.propTypes = propTypes; - -export default ExtraModalContainer; diff --git a/src/js/containers/search/table/ResultsTableContainer.jsx b/src/js/containers/search/table/ResultsTableContainer.jsx index edc72baf1d..dfedb61b4a 100644 --- a/src/js/containers/search/table/ResultsTableContainer.jsx +++ b/src/js/containers/search/table/ResultsTableContainer.jsx @@ -11,12 +11,10 @@ import Immutable from 'immutable'; import { isCancel } from 'axios'; import { difference, intersection } from 'lodash'; -import LegacySearchOperation from 'models/search/LegacySearchOperation'; import SearchAwardsOperation from 'models/search/SearchAwardsOperation'; import * as SearchHelper from 'helpers/searchHelper'; import { awardTypeGroups } from 'dataMapping/search/awardType'; -import tableSearchFields from 'dataMapping/search/tableSearchFields'; import { availableColumns, defaultColumns, defaultSort } from 'dataMapping/search/awardTableColumns'; @@ -83,7 +81,6 @@ export class ResultsTableContainer extends React.Component { this.state = { searchParams: new SearchAwardsOperation(), page: 0, - downloadParams: {}, counts: {} }; @@ -237,11 +234,14 @@ export class ResultsTableContainer extends React.Component { createColumn(title) { // create an object that integrates with the expected column data structure used by // the table component - const dataType = awardTableColumnTypes[title]; - let direction = 'asc'; - if (dataType === 'number' || dataType === 'currency') { - direction = 'desc'; - } + // const dataType = awardTableColumnTypes[title]; + // let direction = 'asc'; + // if (dataType === 'number' || dataType === 'currency') { + // direction = 'desc'; + // } + + // BODGE: Temporarily only allow descending columns + const direction = 'desc'; const column = { columnName: title, @@ -320,26 +320,6 @@ export class ResultsTableContainer extends React.Component { }; // Set the params needed for download API call - // TODO: Kevin Li - download is currently on v1 endpoints, so we will actually - // need to use our legacy filter converters to generate the download params - const legacySearchParams = new LegacySearchOperation(); - legacySearchParams.fromState(this.props.filters); - legacySearchParams.resultAwardType = awardTypeGroups[tableType]; - // we will no longer hold a list of table fields locally, but for the time - // being, let's just dump out all the old v1 fields (regardless of what is being - // displayed) - const legacySearchFields = []; - Object.keys(tableSearchFields[tableType]._mapping).forEach((key) => { - const legacyField = tableSearchFields[tableType]._mapping[key]; - legacySearchFields.push(legacyField); - }); - this.setState({ - downloadParams: { - filters: legacySearchParams.toParams(), - fields: legacySearchFields - } - }); - this.searchRequest = SearchHelper.performSpendingByAwardSearch(params); this.searchRequest.promise .then((res) => { @@ -464,8 +444,7 @@ export class ResultsTableContainer extends React.Component { tableTypes={tableTypes} currentType={this.props.meta.tableType} switchTab={this.switchTab} - loadNextPage={this.loadNextPage} - downloadParams={this.state.downloadParams} /> + loadNextPage={this.loadNextPage} /> ); } } diff --git a/src/js/containers/search/table/ResultsTableHeaderCellContainer.jsx b/src/js/containers/search/table/ResultsTableHeaderCellContainer.jsx index 196481f1a0..8a57798c71 100644 --- a/src/js/containers/search/table/ResultsTableHeaderCellContainer.jsx +++ b/src/js/containers/search/table/ResultsTableHeaderCellContainer.jsx @@ -30,10 +30,18 @@ class ResultsTableHeaderCellContainer extends React.Component { this.setSearchOrder = this.setSearchOrder.bind(this); } - setSearchOrder(field, direction) { + // setSearchOrder(field, direction) { + // this.props.setSearchOrder({ + // field, + // direction + // }); + // } + + // BODGE: Temporarily only allow descending sort + setSearchOrder(field) { this.props.setSearchOrder({ field, - direction + direction: 'desc' }); } diff --git a/tests/containers/search/modals/tableDownload/ExtraModalContainer-test.jsx b/tests/containers/search/modals/tableDownload/ExtraModalContainer-test.jsx deleted file mode 100644 index d3e7bfd8d8..0000000000 --- a/tests/containers/search/modals/tableDownload/ExtraModalContainer-test.jsx +++ /dev/null @@ -1,136 +0,0 @@ -/** - * ExtraModalContainer-test.jsx - * Created by Kevin Li 5/11/17 - */ -import React from 'react'; -import { mount, shallow } from 'enzyme'; -import sinon from 'sinon'; - -import * as DownloadHelper from 'helpers/downloadHelper'; -import { ExtraModalContainer } from 'containers/search/modals/tableDownload/ExtraModalContainer'; - -import { mockRequest, mockReady, mockParams } from './mockDownload'; - -// force Jest to use native Node promises -// see: https://facebook.github.io/jest/docs/troubleshooting.html#unresolved-promises -global.Promise = require.requireActual('promise'); - -// spy on specific functions inside the component -const downloadSpy = sinon.spy(ExtraModalContainer.prototype, 'requestDownload'); - -// mock the child component by replacing it with a function that returns a null element -jest.mock('components/search/modals/tableDownload/ExtraModal', () => - jest.fn(() => null)); - -const mockDownloadHelper = (functionName, event, expectedResponse) => { - jest.useFakeTimers(); - - // override the specified function - DownloadHelper[functionName] = jest.fn(() => { - // Axios normally returns a promise, replicate this, but return the expected result - const networkCall = new Promise((resolve, reject) => { - process.nextTick(() => { - if (event === 'resolve') { - resolve({ - data: expectedResponse - }); - } - else { - reject({ - data: expectedResponse - }); - } - }); - }); - - return { - promise: networkCall, - cancel: jest.fn() - }; - }); -}; - -describe('ExtraModalContainer', () => { - it('should request a download when the modal is opened', () => { - mockDownloadHelper('requestAwardTable', 'resolve', mockRequest); - - const container = mount(); - - container.setProps({ - mounted: true - }); - - expect(downloadSpy.callCount).toEqual(1); - downloadSpy.reset(); - }); - - it('should not request a download if the request hash hasn\'t changed', () => { - mockDownloadHelper('requestAwardTable', 'resolve', mockRequest); - - const container = mount(); - - container.setState({ - activeParams: mockParams - }); - - container.setProps({ - mounted: true - }); - - expect(downloadSpy.callCount).toEqual(0); - downloadSpy.reset(); - }); - - describe('parseResponse', () => { - it('should check for status updates if the download request has not finished', () => { - mockDownloadHelper('requestAwardTable', 'resolve', mockRequest); - - const container = shallow(); - - container.instance().parseResponse(mockRequest); - expect(container.instance().statusChecker).not.toBeNull(); - }); - - it('should not check for status updates if the download request has finished', () => { - mockDownloadHelper('requestAwardTable', 'resolve', mockRequest); - - const container = shallow(); - - container.instance().parseResponse(mockReady); - expect(container.instance().statusChecker).toBeNull(); - }); - - it('should display the current status message from the API', () => { - mockDownloadHelper('requestAwardTable', 'resolve', mockRequest); - - const container = shallow(); - - container.instance().parseResponse(mockRequest); - - expect(container.state().message).toEqual("message"); - expect(container.state().animate).toBeTruthy(); - }); - - it('should stop animating if the download is ready', () => { - mockDownloadHelper('requestAwardTable', 'resolve', mockRequest); - - const container = shallow(); - - container.instance().parseResponse(mockReady); - - expect(container.state().message).toEqual("done"); - expect(container.state().location).toEqual("url"); - expect(container.state().animate).toBeFalsy(); - }); - }); -}); diff --git a/tests/containers/search/modals/tableDownload/mockDownload.js b/tests/containers/search/modals/tableDownload/mockDownload.js deleted file mode 100644 index 1b7c8516a2..0000000000 --- a/tests/containers/search/modals/tableDownload/mockDownload.js +++ /dev/null @@ -1,23 +0,0 @@ -export const mockRequest = { - location: "", - status: "message", - request_checksum: "abcd", - request_path: "path" -}; - -export const mockReady = { - location: "url", - status: "done", - request_checksum: "abcd", - request_path: "path" -}; - -export const mockParams = { - filters: [{ - field: "type", - operation: "in", - value: ["A", "B", "C", "D"] - }], - order: ["-abc"], - fields: ["abc"] -}; diff --git a/tests/containers/search/table/ResultsTableContainer-test.jsx b/tests/containers/search/table/ResultsTableContainer-test.jsx index c02ab0623d..2e29900411 100644 --- a/tests/containers/search/table/ResultsTableContainer-test.jsx +++ b/tests/containers/search/table/ResultsTableContainer-test.jsx @@ -490,7 +490,7 @@ describe('ResultsTableContainer', () => { columnName: 'Award ID', displayName: 'Award ID', width: 220, - defaultDirection: 'asc' + defaultDirection: 'desc' }); }); }); From 9d01b2173450a704a492fadcc7514da22ac18273 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Tue, 28 Nov 2017 13:44:16 -0500 Subject: [PATCH 37/42] Use the All Fiscal Years tag when all fiscal years are selected --- .../topFilterBar/filterGroups/TimePeriodFYFilterGroup.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/components/search/topFilterBar/filterGroups/TimePeriodFYFilterGroup.jsx b/src/js/components/search/topFilterBar/filterGroups/TimePeriodFYFilterGroup.jsx index c1648b9492..21bf2e5899 100644 --- a/src/js/components/search/topFilterBar/filterGroups/TimePeriodFYFilterGroup.jsx +++ b/src/js/components/search/topFilterBar/filterGroups/TimePeriodFYFilterGroup.jsx @@ -53,7 +53,7 @@ export default class TimePeriodFYFilterGroup extends React.Component { // determine how many fiscal years there are available to select // add an extra year at the end to include the current year in the count - const allFY = (FiscalYearHelper.defaultFiscalYear() - FiscalYearHelper.earliestFiscalYear) + const allFY = (FiscalYearHelper.currentFiscalYear() - FiscalYearHelper.earliestFiscalYear) + 1; // check if all fiscal years were selected From f67836d282fbdd7bb982734787466f17b0abf01a Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Tue, 28 Nov 2017 13:48:58 -0500 Subject: [PATCH 38/42] Added recipient search text filter and eliminated the legal entities query --- .../filters/recipient/RecipientName.jsx | 20 +---- .../filters/recipient/SelectedRecipients.jsx | 16 ++-- .../filterGroups/RecipientFilterGroup.jsx | 4 +- .../recipient/RecipientNameDUNSContainer.jsx | 81 ++----------------- .../recipient/RecipientSearchContainer.jsx | 2 +- .../dataMapping/search/awardsOperationKeys.js | 2 +- src/js/models/search/SearchAwardsOperation.js | 9 +-- .../filters/recipientFilterFunctions.js | 7 +- .../reducers/search/searchFiltersReducer.js | 4 +- 9 files changed, 29 insertions(+), 116 deletions(-) diff --git a/src/js/components/search/filters/recipient/RecipientName.jsx b/src/js/components/search/filters/recipient/RecipientName.jsx index 47b6e71553..344a690aee 100644 --- a/src/js/components/search/filters/recipient/RecipientName.jsx +++ b/src/js/components/search/filters/recipient/RecipientName.jsx @@ -12,7 +12,6 @@ const propTypes = { searchRecipient: PropTypes.func, changedInput: PropTypes.func, value: PropTypes.string, - disableButton: PropTypes.any, showWarning: PropTypes.bool, selectedRecipients: PropTypes.object }; @@ -39,18 +38,12 @@ export default class RecipientName extends React.Component { description: 'Please enter more than two characters.' }; } - else if (this.props.selectedRecipients.has(this.props.value)) { + else { errorProps = { header: 'Duplicate Recipient', description: 'You have already selected that recipient.' }; } - else { - errorProps = { - header: 'Unknown Recipient', - description: 'We were unable to find that recipient.' - }; - } return ; } @@ -59,11 +52,6 @@ export default class RecipientName extends React.Component { } render() { - let disableButton = false; - if (this.props.disableButton) { - disableButton = true; - } - return (
    @@ -74,13 +62,11 @@ export default class RecipientName extends React.Component { className="recipient-input" placeholder="Recipient Name or DUNS" value={this.props.value} - onChange={this.props.changedInput} - disabled={disableButton} /> + onChange={this.props.changedInput} /> + value="Submit" />
    {this.generateWarning()} diff --git a/src/js/components/search/filters/recipient/SelectedRecipients.jsx b/src/js/components/search/filters/recipient/SelectedRecipients.jsx index e8b3ee7956..641befad4d 100644 --- a/src/js/components/search/filters/recipient/SelectedRecipients.jsx +++ b/src/js/components/search/filters/recipient/SelectedRecipients.jsx @@ -15,14 +15,14 @@ const propTypes = { export default class SelectedRecipients extends React.Component { render() { const shownRecipients = []; - this.props.selectedRecipients.entrySeq().forEach((entry) => { - const key = entry[0]; - const recipient = entry[1]; - const value = (); + this.props.selectedRecipients.forEach((recipient) => { + const value = ( + + ); shownRecipients.push(value); }); diff --git a/src/js/components/search/topFilterBar/filterGroups/RecipientFilterGroup.jsx b/src/js/components/search/topFilterBar/filterGroups/RecipientFilterGroup.jsx index e31060f21a..2a1ec1e83e 100644 --- a/src/js/components/search/topFilterBar/filterGroups/RecipientFilterGroup.jsx +++ b/src/js/components/search/topFilterBar/filterGroups/RecipientFilterGroup.jsx @@ -43,8 +43,8 @@ export default class RecipientFilterGroup extends React.Component { recipients.forEach((value) => { const tag = { - value: `${value.search_text}`, - title: `RECIPIENT | ${value.search_text}`, + value: `${value}`, + title: `RECIPIENT | ${value}`, isSpecial: false, removeFilter: this.removeFilter }; diff --git a/src/js/containers/search/filters/recipient/RecipientNameDUNSContainer.jsx b/src/js/containers/search/filters/recipient/RecipientNameDUNSContainer.jsx index e1a766f9ca..69be0ec9a1 100644 --- a/src/js/containers/search/filters/recipient/RecipientNameDUNSContainer.jsx +++ b/src/js/containers/search/filters/recipient/RecipientNameDUNSContainer.jsx @@ -7,9 +7,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import { isCancel } from 'axios'; -import * as SearchHelper from 'helpers/searchHelper'; import * as recipientActions from 'redux/actions/search/recipientActions'; import RecipientName from 'components/search/filters/recipient/RecipientName'; @@ -25,88 +23,24 @@ export class RecipientNameDUNSContainer extends React.Component { this.state = { recipientSearchString: '', - showWarning: false, - inFlight: false + showWarning: false }; this.handleTextInput = this.handleTextInput.bind(this); - this.queryRecipients = this.queryRecipients.bind(this); + this.searchRecipient = this.searchRecipient.bind(this); } - queryRecipients() { - this.setState({ - showWarning: false, - inFlight: true - }); - + searchRecipient() { const searchString = this.state.recipientSearchString; // Only search if input is 3 or more characters and is not already // in the list of results if (searchString.length >= 3 && !this.props.selectedRecipients.has(searchString)) { - if (this.recipientSearchRequest) { - // A request is currently in-flight, cancel it - this.recipientSearchRequest.cancel(); - } - - const recipientSearchParams = { - search_text: searchString - }; - - this.recipientSearchRequest = SearchHelper.fetchRecipients(recipientSearchParams); - - this.recipientSearchRequest.promise - .then((res) => { - const autocompleteData = res.data.results; - - this.setState({ - inFlight: false - }); - - // Show error if there are no recipient IDs - if (autocompleteData.recipient_id_list.length === 0) { - this.setState({ - showWarning: true - }); - } - else { - // Add search results to Redux - this.props.toggleRecipient(autocompleteData); - - // Reset input and hide any existing warnings - this.setState({ - showWarning: false, - recipientSearchString: '' - }); - } - }) - .catch((err) => { - if (!isCancel(err)) { - this.setState({ - showWarning: true, - inFlight: false - }); - } - }); - } - else if (searchString.length < 3) { - this.setState({ - showWarning: true, - inFlight: false - }); + this.props.toggleRecipient(searchString); } - else if (this.props.selectedRecipients.has(searchString)) { - this.setState({ - showWarning: true, - inFlight: false - }); - } - else if (this.recipientSearchRequest) { - // A request is currently in-flight, cancel it - this.recipientSearchRequest.cancel(); - + else { this.setState({ - inFlight: false + showWarning: true }); } } @@ -122,10 +56,9 @@ export class RecipientNameDUNSContainer extends React.Component { render() { return ( ); diff --git a/src/js/containers/search/filters/recipient/RecipientSearchContainer.jsx b/src/js/containers/search/filters/recipient/RecipientSearchContainer.jsx index 3113601a0c..07ef984f6c 100644 --- a/src/js/containers/search/filters/recipient/RecipientSearchContainer.jsx +++ b/src/js/containers/search/filters/recipient/RecipientSearchContainer.jsx @@ -63,7 +63,7 @@ export class RecipientSearchContainer extends React.Component { this.props.updateSelectedRecipients(recipient); // Analytics - RecipientSearchContainer.logRecipientFilterEvent(recipient.search_text); + RecipientSearchContainer.logRecipientFilterEvent(recipient); } toggleDomesticForeign(selection) { diff --git a/src/js/dataMapping/search/awardsOperationKeys.js b/src/js/dataMapping/search/awardsOperationKeys.js index a4a082bb1a..014393b827 100644 --- a/src/js/dataMapping/search/awardsOperationKeys.js +++ b/src/js/dataMapping/search/awardsOperationKeys.js @@ -8,7 +8,7 @@ export const rootKeys = { timePeriod: 'time_period', awardType: 'award_type_codes', agencies: 'agencies', - recipients: 'legal_entities', + recipients: 'recipient_search_text', recipientLocationScope: 'recipient_scope', recipientLocation: 'recipient_locations', recipientType: 'recipient_type_names', diff --git a/src/js/models/search/SearchAwardsOperation.js b/src/js/models/search/SearchAwardsOperation.js index dfaff787f9..9a08c261c8 100644 --- a/src/js/models/search/SearchAwardsOperation.js +++ b/src/js/models/search/SearchAwardsOperation.js @@ -6,7 +6,6 @@ import { rootKeys, timePeriodKeys, agencyKeys, awardAmountKeys } from 'dataMapping/search/awardsOperationKeys'; import * as FiscalYearHelper from 'helpers/fiscalYearHelper'; -import { concat } from 'lodash'; class SearchAwardsOperation { constructor() { @@ -175,13 +174,7 @@ class SearchAwardsOperation { // Add Recipients, Recipient Scope, Recipient Locations, and Recipient Types if (this.selectedRecipients.length > 0) { - let recipients = []; - - this.selectedRecipients.forEach((recipient) => { - recipients = concat(recipients, recipient.recipient_id_list); - }); - - filters[rootKeys.recipients] = recipients; + filters[rootKeys.recipients] = this.selectedRecipients; } if (this.recipientDomesticForeign !== '' && this.recipientDomesticForeign !== 'all') { diff --git a/src/js/redux/reducers/search/filters/recipientFilterFunctions.js b/src/js/redux/reducers/search/filters/recipientFilterFunctions.js index d60c12fc0b..a8bd1ff388 100644 --- a/src/js/redux/reducers/search/filters/recipientFilterFunctions.js +++ b/src/js/redux/reducers/search/filters/recipientFilterFunctions.js @@ -5,16 +5,17 @@ import { sortBy } from 'lodash'; import { Set } from 'immutable'; -export const updateSelectedRecipients = (state, value) => { +export const updateSelectedRecipients = (state, searchText) => { let updatedSet = state; - const recipientIdentifier = `${value.search_text}`; // force it to a string + const recipientIdentifier = `${searchText}`; // force it to a string if (updatedSet.has(recipientIdentifier)) { updatedSet = updatedSet.delete(recipientIdentifier); } else { - updatedSet = updatedSet.set(recipientIdentifier, value); + // Replace with the new search text because we are limiting the search to one recipient filter + updatedSet = new Set([searchText]); } return updatedSet; diff --git a/src/js/redux/reducers/search/searchFiltersReducer.js b/src/js/redux/reducers/search/searchFiltersReducer.js index 904fab0215..5646f78154 100644 --- a/src/js/redux/reducers/search/searchFiltersReducer.js +++ b/src/js/redux/reducers/search/searchFiltersReducer.js @@ -25,7 +25,7 @@ export const requiredTypes = { selectedLocations: OrderedMap, selectedFundingAgencies: OrderedMap, selectedAwardingAgencies: OrderedMap, - selectedRecipients: OrderedMap, + selectedRecipients: Set, recipientType: Set, selectedRecipientLocations: OrderedMap, awardType: Set, @@ -49,7 +49,7 @@ export const initialState = { locationDomesticForeign: 'all', selectedFundingAgencies: new OrderedMap(), selectedAwardingAgencies: new OrderedMap(), - selectedRecipients: new OrderedMap(), + selectedRecipients: new Set(), recipientDomesticForeign: 'all', recipientType: new Set(), selectedRecipientLocations: new OrderedMap(), From f510ad27a81a060d60bfb03913ead5e5153ee3ce Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Tue, 28 Nov 2017 15:07:00 -0500 Subject: [PATCH 39/42] Updated tests --- .../RecipientNameDUNSContainer-test.jsx | 116 +++++------------- .../RecipientSearchContainer-test.jsx | 26 +--- tests/containers/search/mockSearchHashes.js | 2 +- .../redux/reducers/search/mock/mockFilters.js | 8 +- .../search/searchFiltersReducer-test.js | 8 +- 5 files changed, 40 insertions(+), 120 deletions(-) diff --git a/tests/containers/search/filters/recipientFilter/RecipientNameDUNSContainer-test.jsx b/tests/containers/search/filters/recipientFilter/RecipientNameDUNSContainer-test.jsx index ba99818bc7..fe17b676dd 100644 --- a/tests/containers/search/filters/recipientFilter/RecipientNameDUNSContainer-test.jsx +++ b/tests/containers/search/filters/recipientFilter/RecipientNameDUNSContainer-test.jsx @@ -6,7 +6,7 @@ import React from 'react'; import { mount } from 'enzyme'; import sinon from 'sinon'; -import { OrderedMap } from 'immutable'; +import { Set } from 'immutable'; import { RecipientNameDUNSContainer } from 'containers/search/filters/recipient/RecipientNameDUNSContainer'; @@ -17,19 +17,18 @@ jest.mock('helpers/searchHelper', () => require('../searchHelper')); jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; describe('RecipientNameDUNSContainer', () => { - describe('Handling text input', () => { - it('should not search when two or fewer characters have been input', () => { + describe('Adding a Recipient to Redux', () => { + it('should not add a filter to Redux when two or fewer characters have been input', () => { const mockReduxAction = jest.fn(); - // Set up the Container and call the function to type a single letter + // Set up the Container and call the function to type fewer than three characters const recipientNameDUNSContainer = setup({ - toggleRecipient: mockReduxAction, selectedRecipients: new OrderedMap() + toggleRecipient: mockReduxAction, selectedRecipients: new Set() }); - const queryRecipientsSpy = sinon.spy(recipientNameDUNSContainer.instance(), - 'queryRecipients'); - const handleTextInputSpy = sinon.spy(recipientNameDUNSContainer.instance(), - 'handleTextInput'); + // set up spy + const searchRecipientSpy = sinon.spy(recipientNameDUNSContainer.instance(), + 'searchRecipient'); recipientNameDUNSContainer.instance().handleTextInput({ target: { @@ -40,34 +39,29 @@ describe('RecipientNameDUNSContainer', () => { // Run fake timer for input delay jest.runAllTicks(); - recipientNameDUNSContainer.instance().queryRecipients(); + recipientNameDUNSContainer.instance().searchRecipient(); // everything should be updated now - expect(handleTextInputSpy.callCount).toEqual(1); - expect(queryRecipientsSpy.callCount).toEqual(1); + expect(searchRecipientSpy.callCount).toEqual(1); expect(mockReduxAction).toHaveBeenCalledTimes(0); - expect(recipientNameDUNSContainer.instance().recipientSearchRequest).toBeFalsy(); - // reset the mocks and spies - handleTextInputSpy.reset(); - queryRecipientsSpy.reset(); + // reset the spy + searchRecipientSpy.reset(); }); - it('should search when three or more characters have been input ' + - 'into the Recipient Name/DUNS field', async () => { - // setup mock redux actions for handling search results + it('should add a filter to Redux when three or more characters have been input ' + + 'into the Recipient Name/DUNS field', () => { + // setup mock redux action const mockReduxAction = jest.fn(); - // Set up the Container and call the function to type a single letter + // Set up the Container and call the function to type more than 2 characters const recipientNameDUNSContainer = setup({ - toggleRecipient: mockReduxAction, selectedRecipients: new OrderedMap() + toggleRecipient: mockReduxAction, selectedRecipients: new Set() }); - // set up spies - const handleTextInputSpy = sinon.spy(recipientNameDUNSContainer.instance(), - 'handleTextInput'); - const queryRecipientsSpy = sinon.spy(recipientNameDUNSContainer.instance(), - 'queryRecipients'); + // set up spy + const searchRecipientSpy = sinon.spy(recipientNameDUNSContainer.instance(), + 'searchRecipient'); const searchQuery = { target: { @@ -80,54 +74,15 @@ describe('RecipientNameDUNSContainer', () => { // Run fake timer for input delay jest.runAllTicks(); - recipientNameDUNSContainer.instance().queryRecipients(); - await recipientNameDUNSContainer.instance().recipientSearchRequest.promise; + recipientNameDUNSContainer.instance().searchRecipient(); // everything should be updated now - expect(handleTextInputSpy.callCount).toEqual(1); - expect(queryRecipientsSpy.callCount).toEqual(1); + expect(searchRecipientSpy.callCount).toEqual(1); expect(mockReduxAction).toHaveBeenCalledTimes(1); - // Reset spies - handleTextInputSpy.reset(); - queryRecipientsSpy.reset(); - }); - }); - - describe('Adding a Recipient to Redux', () => { - it('should automatically add a Recipient to Redux after parsing a valid response', async () => { - // setup mock redux actions for handling search results - const mockReduxAction = jest.fn(); - - // Set up the Container and call the function to type a single letter - const recipientNameDUNSContainer = setup({ - toggleRecipient: mockReduxAction, - selectedRecipients: new OrderedMap() - }); - - // Set up spies - const queryRecipientsSpy = sinon.spy(recipientNameDUNSContainer.instance(), - 'queryRecipients'); - - recipientNameDUNSContainer.instance().handleTextInput({ - target: { - value: 'Booz Allen' - } - }); - - // Run fake timer for input delay - jest.runAllTicks(); - - recipientNameDUNSContainer.instance().queryRecipients(); - await recipientNameDUNSContainer.instance().recipientSearchRequest.promise; - - expect(queryRecipientsSpy.callCount).toEqual(1); - expect(mockReduxAction).toHaveBeenCalledTimes(1); - - // Reset spies - queryRecipientsSpy.reset(); + // Reset spy + searchRecipientSpy.reset(); }); - it('should not search for a Recipient that already exists in Redux', () => { // setup mock redux actions for handling search results const mockReduxAction = jest.fn(); @@ -135,20 +90,12 @@ describe('RecipientNameDUNSContainer', () => { // Set up the Container and call the function to type a single letter const recipientNameDUNSContainer = setup({ toggleRecipient: mockReduxAction, - selectedRecipients: new OrderedMap({ - "Booz Allen": { - search_text: "Booz Allen", - recipient_id_list: [ - 2232, - 2260 - ] - } - }) + selectedRecipients: new Set(["Booz Allen"]) }); - // Set up spy - const queryRecipientsSpy = sinon.spy(recipientNameDUNSContainer.instance(), - 'queryRecipients'); + // set up spy + const searchRecipientSpy = sinon.spy(recipientNameDUNSContainer.instance(), + 'searchRecipient'); recipientNameDUNSContainer.instance().handleTextInput({ target: { @@ -159,14 +106,13 @@ describe('RecipientNameDUNSContainer', () => { // Run fake timer for input delay jest.runAllTicks(); - recipientNameDUNSContainer.instance().queryRecipients(); + recipientNameDUNSContainer.instance().searchRecipient(); - expect(queryRecipientsSpy.callCount).toEqual(1); + expect(searchRecipientSpy.callCount).toEqual(1); expect(mockReduxAction).toHaveBeenCalledTimes(0); - expect(recipientNameDUNSContainer.instance().recipientSearchRequest).toBeFalsy(); // Reset spy - queryRecipientsSpy.reset(); + searchRecipientSpy.reset(); }); }); }); diff --git a/tests/containers/search/filters/recipientFilter/RecipientSearchContainer-test.jsx b/tests/containers/search/filters/recipientFilter/RecipientSearchContainer-test.jsx index ca2cecfd58..1f5771e084 100644 --- a/tests/containers/search/filters/recipientFilter/RecipientSearchContainer-test.jsx +++ b/tests/containers/search/filters/recipientFilter/RecipientSearchContainer-test.jsx @@ -11,19 +11,13 @@ import { RecipientSearchContainer } from 'containers/search/filters/recipient/RecipientSearchContainer'; const initialFilters = { - selectedRecipients: {}, + selectedRecipients: [], recipientDomesticForeign: 'all', selectedRecipientLocations: {}, recipientType: {} }; -const recipient = { - search_text: "Booz Allen", - recipient_id_list: [ - 2232, - 2260 - ] -}; +const recipient = "Booz Allen"; const location = { place_type: "CITY", @@ -36,13 +30,7 @@ describe('RecipientSearchContainer', () => { describe('Handling adding and removing recipients', () => { it('should add a Recipient that has been selected to Redux', () => { const mockReduxAction = jest.fn((args) => { - expect(args).toEqual({ - search_text: "Booz Allen", - recipient_id_list: [ - 2232, - 2260 - ] - }); + expect(args).toEqual("Booz Allen"); }); // Set up container with mocked action @@ -67,13 +55,7 @@ describe('RecipientSearchContainer', () => { it('should remove a Recipient that has been deselected from Redux', () => { const mockReduxAction = jest.fn((args) => { - expect(args).toEqual({ - search_text: "Booz Allen", - recipient_id_list: [ - 2232, - 2260 - ] - }); + expect(args).toEqual("Booz Allen"); }); // Set up container with mocked action diff --git a/tests/containers/search/mockSearchHashes.js b/tests/containers/search/mockSearchHashes.js index 9f8862ac83..dc41555ab2 100644 --- a/tests/containers/search/mockSearchHashes.js +++ b/tests/containers/search/mockSearchHashes.js @@ -11,7 +11,7 @@ export const mockFilters = { filters: { locationDomesticForeign: "all", selectedAwardIDs: {}, - selectedRecipients: {}, + selectedRecipients: [], selectedFundingAgencies: {}, selectedLocations: {}, recipientType: [], diff --git a/tests/redux/reducers/search/mock/mockFilters.js b/tests/redux/reducers/search/mock/mockFilters.js index 06a4aeb2e5..0465b72ce0 100644 --- a/tests/redux/reducers/search/mock/mockFilters.js +++ b/tests/redux/reducers/search/mock/mockFilters.js @@ -1,10 +1,4 @@ -export const mockRecipient = { - search_text: "Booz Allen", - recipient_id_list: [ - 2232, - 2260 - ] -}; +export const mockRecipient = "Booz Allen"; export const mockAgency = { id: 1788, diff --git a/tests/redux/reducers/search/searchFiltersReducer-test.js b/tests/redux/reducers/search/searchFiltersReducer-test.js index 9e5b1ec680..4e78273350 100644 --- a/tests/redux/reducers/search/searchFiltersReducer-test.js +++ b/tests/redux/reducers/search/searchFiltersReducer-test.js @@ -318,23 +318,21 @@ describe('searchFiltersReducer', () => { const recipient = 'Booz Allen'; - const expectedRecipient = mockRecipient; - it('should add the Recipient if it does not currently exist in the filter', () => { const updatedState = searchFiltersReducer(undefined, action); expect(updatedState.selectedRecipients).toEqual( - new OrderedMap([[recipient, expectedRecipient]]) + new Set([recipient]) ); }); it('should remove the Recipient if already exists in the filter', () => { const startingState = Object.assign({}, initialState, { - selectedRecipients: new OrderedMap([[recipient, expectedRecipient]]) + selectedRecipients: new Set([recipient]) }); const updatedState = searchFiltersReducer(startingState, action); - expect(updatedState.selectedRecipients).toEqual(new OrderedMap()); + expect(updatedState.selectedRecipients).toEqual(new Set()); }); }); From 4f4ade3277d5a97ca7299b7c1c60b9a8332309f5 Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Tue, 28 Nov 2017 15:56:13 -0500 Subject: [PATCH 40/42] Reset input when the filter is applied --- .../search/filters/recipient/RecipientNameDUNSContainer.jsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/js/containers/search/filters/recipient/RecipientNameDUNSContainer.jsx b/src/js/containers/search/filters/recipient/RecipientNameDUNSContainer.jsx index 69be0ec9a1..22221cc20b 100644 --- a/src/js/containers/search/filters/recipient/RecipientNameDUNSContainer.jsx +++ b/src/js/containers/search/filters/recipient/RecipientNameDUNSContainer.jsx @@ -37,6 +37,11 @@ export class RecipientNameDUNSContainer extends React.Component { // in the list of results if (searchString.length >= 3 && !this.props.selectedRecipients.has(searchString)) { this.props.toggleRecipient(searchString); + + // Reset input + this.setState({ + recipientSearchString: '' + }); } else { this.setState({ From c43fecee3e0e9fc7ee69c55981f02b3a10b96f76 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Tue, 28 Nov 2017 16:33:22 -0500 Subject: [PATCH 41/42] Text change --- src/js/components/about/DataSources.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/components/about/DataSources.jsx b/src/js/components/about/DataSources.jsx index 33a9eb2ec7..90cbe3ea1d 100644 --- a/src/js/components/about/DataSources.jsx +++ b/src/js/components/about/DataSources.jsx @@ -21,7 +21,7 @@ export default class DataSources extends React.Component {

    As you see in Exhibit 1 below, many sources of information support USAspending.gov, linking data from a variety of government systems to improve transparency on federal spending for the public. Data is - uploaded directly from hundreds of federal agencies' financial systems. + uploaded directly from more than a hundred federal agencies' financial systems. Data is also pulled or derived from other government systems. For example, contract award data is pulled into USAspending.gov daily from the Federal Procurement Data System Next Generation (FPDS-NG); From 72a3e65d2a1697f43fd0234ad8fafa17630239b0 Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Tue, 28 Nov 2017 17:39:40 -0500 Subject: [PATCH 42/42] Removed recipient autocomplete code; disable when a recipient filter is already selected --- .../filters/recipient/RecipientName.jsx | 6 + src/js/helpers/searchHelper.js | 17 -- src/js/redux/reducers/index.js | 2 - .../redux/reducers/search/recipientReducer.js | 30 -- .../reducers/search/recipientReducer-test.js | 268 ------------------ 5 files changed, 6 insertions(+), 317 deletions(-) delete mode 100644 src/js/redux/reducers/search/recipientReducer.js delete mode 100644 tests/redux/reducers/search/recipientReducer-test.js diff --git a/src/js/components/search/filters/recipient/RecipientName.jsx b/src/js/components/search/filters/recipient/RecipientName.jsx index 344a690aee..e26431d622 100644 --- a/src/js/components/search/filters/recipient/RecipientName.jsx +++ b/src/js/components/search/filters/recipient/RecipientName.jsx @@ -52,6 +52,10 @@ export default class RecipientName extends React.Component { } render() { + let disableButton = false; + if (this.props.selectedRecipients.size !== 0) { + disableButton = true; + } return (

    @@ -62,10 +66,12 @@ export default class RecipientName extends React.Component { className="recipient-input" placeholder="Recipient Name or DUNS" value={this.props.value} + disabled={disableButton} onChange={this.props.changedInput} />
    diff --git a/src/js/helpers/searchHelper.js b/src/js/helpers/searchHelper.js index 4056ac175b..c2d8cba27e 100644 --- a/src/js/helpers/searchHelper.js +++ b/src/js/helpers/searchHelper.js @@ -314,23 +314,6 @@ export const performSpendingByAwardSearch = (params) => { }; }; -// Fetch Recipients -export const fetchRecipients = (req) => { - const source = CancelToken.source(); - return { - promise: Axios.request({ - url: 'v2/autocomplete/recipient/', - baseURL: kGlobalConstants.API, - method: 'post', - data: req, - cancelToken: source.token - }), - cancel() { - source.cancel(); - } - }; -}; - // Fetch Award IDs export const fetchAwardIDs = (params) => { const source = CancelToken.source(); diff --git a/src/js/redux/reducers/index.js b/src/js/redux/reducers/index.js index e1dffe2d0e..94a76d5051 100644 --- a/src/js/redux/reducers/index.js +++ b/src/js/redux/reducers/index.js @@ -13,7 +13,6 @@ import autocompleteReducer from './search/autocompleteReducer'; import columnVisibilityReducer from './search/columnVisibilityReducer'; import recordReducer from './records/recordReducer'; import autocompleteAgencyReducer from './search/agencyReducer'; -import recipientReducer from './search/recipientReducer'; import awardIDReducer from './search/awardIDReducer'; import awardReducer from './award/awardReducer'; import accountReducer from './account/accountReducer'; @@ -39,7 +38,6 @@ const appReducer = combineReducers({ columnVisibility: columnVisibilityReducer, autocompleteAwardIDs: awardIDReducer, autocompleteAgencies: autocompleteAgencyReducer, - autocompleteRecipients: recipientReducer, records: recordReducer, download: downloadReducer, award: awardReducer, diff --git a/src/js/redux/reducers/search/recipientReducer.js b/src/js/redux/reducers/search/recipientReducer.js deleted file mode 100644 index df20a85fae..0000000000 --- a/src/js/redux/reducers/search/recipientReducer.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * recipientReducer.js - * Created by michaelbray on 2/17/17. - */ - -import { concat } from 'lodash'; - -const initialState = { - recipients: [], - recipientLocations: [] -}; - -const recipientReducer = (state = initialState, action) => { - switch (action.type) { - case 'SET_AUTOCOMPLETE_RECIPIENTS': { - return Object.assign({}, state, { - recipients: concat([], action.recipients) - }); - } - case 'SET_AUTOCOMPLETE_RECIPIENT_LOCATIONS': { - return Object.assign({}, state, { - recipientLocations: concat([], action.locations) - }); - } - default: - return state; - } -}; - -export default recipientReducer; diff --git a/tests/redux/reducers/search/recipientReducer-test.js b/tests/redux/reducers/search/recipientReducer-test.js deleted file mode 100644 index 610b699713..0000000000 --- a/tests/redux/reducers/search/recipientReducer-test.js +++ /dev/null @@ -1,268 +0,0 @@ -/** - * recipientReducer-test.js - * Created by michaelbray on 2/17/17. - */ - -import recipientReducer from 'redux/reducers/search/recipientReducer'; - -const initialState = { - recipients: [], - recipientLocations: [] -}; - -describe('recipientReducer', () => { - it('should return the initial state by default', () => { - expect( - recipientReducer(undefined, {}) - ).toEqual(initialState); - }); - - describe('SET_AUTOCOMPLETE_RECIPIENTS', () => { - it('should return a new instance of the input recipients object', () => { - const action = { - type: 'SET_AUTOCOMPLETE_RECIPIENTS', - recipients: [ - { - legal_entity_id: 973, - data_source: null, - parent_recipient_unique_id: "964725688", - recipient_name: "BOOZ ALLEN HAMILTON INC.", - vendor_doing_as_business_name: null, - vendor_phone_number: "7033770195", - vendor_fax_number: "7039023200", - business_types: "UN", - business_types_description: "Unknown Business Type", - recipient_unique_id: "006928857", - limited_liability_corporation: "f", - sole_proprietorship: "f", - partnership_or_limited_liability_partnership: "f", - subchapter_scorporation: "f", - foundation: "f", - for_profit_organization: "t", - nonprofit_organization: "f", - corporate_entity_tax_exempt: "f", - corporate_entity_not_tax_exempt: "t", - other_not_for_profit_organization: "f", - sam_exception: null, - city_local_government: "f", - county_local_government: "f", - inter_municipal_local_government: "f", - local_government_owned: "f", - municipality_local_government: "f", - school_district_local_government: "f", - township_local_government: "f", - us_state_government: null, - us_federal_government: "f", - federal_agency: "f", - federally_funded_research_and_development_corp: "f", - us_tribal_government: "f", - foreign_government: "f", - community_developed_corporation_owned_firm: "f", - labor_surplus_area_firm: "f", - small_agricultural_cooperative: "f", - international_organization: "f", - us_government_entity: null, - emerging_small_business: null, - c8a_program_participant: "f", - sba_certified_8a_joint_venture: null, - dot_certified_disadvantage: "f", - self_certified_small_disadvantaged_business: null, - historically_underutilized_business_zone: "f", - small_disadvantaged_business: null, - the_ability_one_program: null, - historically_black_college: "f", - c1862_land_grant_college: "f", - c1890_land_grant_college: "f", - c1994_land_grant_college: "f", - minority_institution: "f", - private_university_or_college: "f", - school_of_forestry: "f", - state_controlled_institution_of_higher_learning: "f", - tribal_college: "f", - veterinary_college: "f", - educational_institution: "f", - alaskan_native_servicing_institution: "f", - community_development_corporation: "f", - native_hawaiian_servicing_institution: "f", - domestic_shelter: "f", - manufacturer_of_goods: "f", - hospital_flag: "f", - veterinary_hospital: "f", - hispanic_servicing_institution: "f", - woman_owned_business: "f", - minority_owned_business: "f", - women_owned_small_business: null, - economically_disadvantaged_women_owned_small_business: null, - joint_venture_women_owned_small_business: null, - joint_venture_economic_disadvantaged_women_owned_small_bus: null, - veteran_owned_business: "f", - service_disabled_veteran_owned_business: null, - contracts: null, - grants: null, - receives_contracts_and_grants: null, - airport_authority: "f", - council_of_governments: "f", - housing_authorities_public_tribal: "f", - interstate_entity: "f", - planning_commission: "f", - port_authority: "f", - transit_authority: "f", - foreign_owned_and_located: "f", - american_indian_owned_business: "f", - alaskan_native_owned_corporation_or_firm: "f", - indian_tribe_federally_recognized: "f", - native_hawaiian_owned_business: "f", - tribally_owned_business: "f", - asian_pacific_american_owned_business: "f", - black_american_owned_business: "f", - hispanic_american_owned_business: "f", - native_american_owned_business: "f", - subcontinent_asian_asian_indian_american_owned_business: "f", - other_minority_owned_business: "f", - us_local_government: "f", - undefinitized_action: null, - domestic_or_foreign_entity: null, - division_name: null, - division_number: null, - last_modified_date: null, - certified_date: null, - reporting_period_start: null, - reporting_period_end: null, - create_date: "2017-02-15T20:06:43.024083Z", - update_date: "2017-02-15T20:06:43.024108Z", - city_township_government: null, - special_district_government: null, - small_business: null, - individual: null, - location: { - location_id: 12619, - data_source: null, - country_name: "UNITED STATES", - state_code: "VA", - state_name: null, - state_description: null, - city_name: "McLean", - city_code: "48376", - county_name: "Fairfax", - county_code: "59", - address_line1: "8283 GREENSBORO DR", - address_line2: "", - address_line3: "", - foreign_location_description: null, - zip4: null, - zip_4a: null, - congressional_code: "11", - performance_code: null, - zip_last4: "3830", - zip5: "22102", - foreign_postal_code: null, - foreign_province: null, - foreign_city_name: null, - reporting_period_start: null, - reporting_period_end: null, - last_modified_date: null, - certified_date: null, - create_date: "2017-02-15T20:32:31.411437Z", - update_date: "2017-02-15T20:32:31.411470Z", - place_of_performance_flag: false, - recipient_flag: false, - location_country_code: "USA" - } - } - ] - }; - - const updatedState = recipientReducer(undefined, action); - - // the value should be equal - expect(updatedState.recipients).toEqual(action.recipients); - // but it should be its own instance - expect(updatedState.recipients).not.toBe(action.recipients); - }); - }); - - describe('SET_AUTOCOMPLETE_RECIPIENT_LOCATIONS', () => { - it('should return a new instance of the input agency object', () => { - const action = { - type: 'SET_AUTOCOMPLETE_RECIPIENT_LOCATIONS', - locations: [ - { - place_type: "CITY", - matched_ids: [114, 76, 1319, 1328, 1589, 2460, 2454, 2467, 2461, 2469, 2837, - 2843, 2849, 2852, 3025, 3830, 3838, 3792, 4317, 8051, 8120, 8180, 8053, - 8041, 8206, 8209, 11011, 11004, 10983, 11008, 11005, 10965, 11013, - 10985, 11010, 10953, 11760, 12619, 12623, 12696, 14179, 14526, 14801, - 18603], - place: "McLean", - parent: null - }, - { - place_type: "CITY", - matched_ids: [43315], - place: "MCLEANSBORO", - parent: null - }, - { - place_type: "COUNTY", - matched_ids: [770, 779, 12786, 15678, 16831, 16134, 16651, 16693, 17326, - 20357, 21033, 20885, 22815, 22790, 25014, 24293, 25288, 26140, 28592, - 29512, 29468, 29479, 29262, 31900, 35803, 37905, 38934, 41400, 42689, - 43050, 45451, 45351, 47138, 47743, 47153, 47746, 47695, 47139, 47145, - 47691, 47745, 47742, 47694], - place: "McLean", - parent: null - }, - { - place_type: "COUNTY", - matched_ids: [22796], - place: "McLean", - parent: "KENTUCKY" - }, - { - place_type: "COUNTY", - matched_ids: [15688], - place: "McLean", - parent: "ILLINOIS" - }, - { - place_type: "COUNTY", - matched_ids: [18612], - place: "MCLEAN", - parent: "NEBRASKA" - }, - { - place_type: "COUNTY", - matched_ids: [35810, 47141, 47698, 49171], - place: "MCLEAN", - parent: "ILLINOIS" - }, - { - place_type: "COUNTY", - matched_ids: [29271], - place: "MCLEAN", - parent: "KENTUCKY" - }, - { - place_type: "COUNTY", - matched_ids: [49167], - place: "MCLEAN", - parent: null - }, - { - place_type: "COUNTY", - matched_ids: [38937], - place: "MCLEAN", - parent: "NORTH DAKOTA" - } - ] - }; - - const updatedState = recipientReducer(undefined, action); - - // the value should be equal - expect(updatedState.recipientLocations).toEqual(action.locations); - // but it should be its own instance - expect(updatedState.recipientLocations).not.toBe(action.locations); - }); - }); -});