From bdddf7963691c4bf5866aee4532cc7b3c9aad6fc Mon Sep 17 00:00:00 2001 From: Elizabeth Salita Date: Fri, 4 Aug 2017 09:09:02 -0400 Subject: [PATCH 01/89] Federal account landing page initial commit --- .../accountLanding/AccountLandingContent.jsx | 53 +++++ .../accountLanding/AccountLandingPage.jsx | 32 +++ .../AccountLandingResultsSection.jsx | 53 +++++ .../AccountLandingSearchBar.jsx | 41 ++++ .../header/AccountLandingHeader.jsx | 20 ++ .../table/AccountLandingTable.jsx | 52 +++++ .../accountLanding/table/HeaderRow.jsx | 34 +++ .../accountLanding/table/TableRow.jsx | 54 +++++ .../table/cells/AccountLinkCell.jsx | 47 ++++ .../AccountLandingContainer.jsx | 220 ++++++++++++++++++ .../AccountLandingHeaderCellContainer.jsx | 64 +++++ .../accountLanding/accountsTableFields.js | 20 ++ src/js/helpers/accountLandingHelper.js | 25 ++ .../accountLanding/accountLandingActions.js | 9 + .../accountLanding/accountLandingReducer.js | 27 +++ 15 files changed, 751 insertions(+) create mode 100644 src/js/components/accountLanding/AccountLandingContent.jsx create mode 100644 src/js/components/accountLanding/AccountLandingPage.jsx create mode 100644 src/js/components/accountLanding/AccountLandingResultsSection.jsx create mode 100644 src/js/components/accountLanding/AccountLandingSearchBar.jsx create mode 100644 src/js/components/accountLanding/header/AccountLandingHeader.jsx create mode 100644 src/js/components/accountLanding/table/AccountLandingTable.jsx create mode 100644 src/js/components/accountLanding/table/HeaderRow.jsx create mode 100644 src/js/components/accountLanding/table/TableRow.jsx create mode 100644 src/js/components/accountLanding/table/cells/AccountLinkCell.jsx create mode 100644 src/js/containers/accountLanding/AccountLandingContainer.jsx create mode 100644 src/js/containers/accountLanding/table/AccountLandingHeaderCellContainer.jsx create mode 100644 src/js/dataMapping/accountLanding/accountsTableFields.js create mode 100644 src/js/helpers/accountLandingHelper.js create mode 100644 src/js/redux/actions/accountLanding/accountLandingActions.js create mode 100644 src/js/redux/reducers/accountLanding/accountLandingReducer.js diff --git a/src/js/components/accountLanding/AccountLandingContent.jsx b/src/js/components/accountLanding/AccountLandingContent.jsx new file mode 100644 index 0000000000..63c0ac3a00 --- /dev/null +++ b/src/js/components/accountLanding/AccountLandingContent.jsx @@ -0,0 +1,53 @@ +/** + * AccountLandingContent.jsx + * Created by Lizzie Salita 8/4/17 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import AccountLandingSearchBar from './AccountLandingSearchBar'; +import AccountLandingResultsSection from './AccountLandingResultsSection'; + +const propTypes = { + resultsText: PropTypes.string, + results: PropTypes.array, + accountSearchString: PropTypes.string, + inFlight: PropTypes.bool, + columns: PropTypes.array, + setAccountSearchString: PropTypes.func +}; + +export default class AccountLandingContent extends React.Component { + render() { + return ( +
+
+

Find a Federal Account Profile.

+
Explore spending in greater detail in our federal account profiles.
+

+ There are over 2,000 unique federal accounts used to track the spending of + federal agencies. These help to understand how agencies receive and spend + funding granted by congress to carry out their programs, projects, or activities. +

+
+
+ +
+
+ {this.props.resultsText} +
+
+ +
+
+ ); + } +} + +AccountLandingContent.propTypes = propTypes; diff --git a/src/js/components/accountLanding/AccountLandingPage.jsx b/src/js/components/accountLanding/AccountLandingPage.jsx new file mode 100644 index 0000000000..03adb5b96b --- /dev/null +++ b/src/js/components/accountLanding/AccountLandingPage.jsx @@ -0,0 +1,32 @@ +/** + * AccountLandingPage.jsx + * Created by Lizzie Salita 8/4/17 + */ + +import React from 'react'; + +import { accountLandingPageMetaTags } from 'helpers/metaTagHelper'; + +import MetaTags from 'components/sharedComponents/metaTags/MetaTags'; +import Header from 'components/sharedComponents/header/Header'; +import Footer from 'components/sharedComponents/Footer'; +import AccountLandingContainer from 'containers/accountLanding/AccountLandingContainer'; +import AccountLandingHeader from './header/AccountLandingHeader'; + +export default class AccountLandingPage extends React.Component { + render() { + return ( +
+ +
+ +
+ +
+
+
+ ); + } +} diff --git a/src/js/components/accountLanding/AccountLandingResultsSection.jsx b/src/js/components/accountLanding/AccountLandingResultsSection.jsx new file mode 100644 index 0000000000..44d516d6c9 --- /dev/null +++ b/src/js/components/accountLanding/AccountLandingResultsSection.jsx @@ -0,0 +1,53 @@ +/** + * AccountLandingResultsSection.jsx + * Created by Lizzie Salita 8/4/17 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import ResultsTableMessage from 'components/search/table/ResultsTableMessage'; +import AccountLandingTable from './table/AccountLandingTable'; + +const propTypes = { + inFlight: PropTypes.bool, + results: PropTypes.array, + columns: PropTypes.array, + accountSearchString: PropTypes.string +}; + +export default class AccountLandingResultsSection extends React.Component { + render() { + let loadingWrapper = ''; + let message = null; + if (this.props.inFlight) { + loadingWrapper = 'loading-table'; + message = ; + } + else if (this.props.results.length === 0) { + // no results + if (this.props.accountSearchString) { + message = ( +
+ No results found for “ {this.props.accountSearchString} ”. +
+ ); + } + else { + message = ; + } + } + + return ( +
+
+ +
+ {message} +
+ ); + } +} + +AccountLandingResultsSection.propTypes = propTypes; diff --git a/src/js/components/accountLanding/AccountLandingSearchBar.jsx b/src/js/components/accountLanding/AccountLandingSearchBar.jsx new file mode 100644 index 0000000000..ee4d1d4752 --- /dev/null +++ b/src/js/components/accountLanding/AccountLandingSearchBar.jsx @@ -0,0 +1,41 @@ +/** + * AccountLandingSearchBar.jsx + * Created by Lizzie Salita 8/4/17 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Search } from 'components/sharedComponents/icons/Icons'; + +const propTypes = { + setAccountSearchString: PropTypes.func.isRequired +}; + +export default class AccountLandingSearchBar extends React.Component { + onChange(e) { + const value = e.target.value; + this.props.setAccountSearchString(value); + } + + render() { + return ( +
+
+ + +
+
+ ); + } +} + +AccountLandingSearchBar.propTypes = propTypes; diff --git a/src/js/components/accountLanding/header/AccountLandingHeader.jsx b/src/js/components/accountLanding/header/AccountLandingHeader.jsx new file mode 100644 index 0000000000..9428aa7f5a --- /dev/null +++ b/src/js/components/accountLanding/header/AccountLandingHeader.jsx @@ -0,0 +1,20 @@ +/** + * AccountLandingHeader.jsx + * Created by Lizzie Salita 8/4/17 + */ + +import React from 'react'; + +export default class AccountLandingHeader extends React.Component { + render() { + return ( +
+
+

+ Federal Account Profiles +

+
+
+ ); + } +} diff --git a/src/js/components/accountLanding/table/AccountLandingTable.jsx b/src/js/components/accountLanding/table/AccountLandingTable.jsx new file mode 100644 index 0000000000..26815fe891 --- /dev/null +++ b/src/js/components/accountLanding/table/AccountLandingTable.jsx @@ -0,0 +1,52 @@ +/** + * AccountLandingTable.jsx + * Created by Lizzie Salita 8/4/17 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import HeaderRow from './HeaderRow'; +import TableRow from './TableRow'; + +const propTypes = { + results: PropTypes.array, + columns: PropTypes.array, + accountSearchString: PropTypes.string +}; + +export default class AccountLandingTable extends React.PureComponent { + render() { + let noResultsClass = ''; + if (this.props.results.length === 0) { + // remove duplicated bottom border + noResultsClass = ' no-results'; + } + + + const rows = this.props.results.map((account, index) => ( + + )); + + return ( +
+ + + + + + {rows} + +
+
+ ); + } +} + +AccountLandingTable.propTypes = propTypes; diff --git a/src/js/components/accountLanding/table/HeaderRow.jsx b/src/js/components/accountLanding/table/HeaderRow.jsx new file mode 100644 index 0000000000..18da01a484 --- /dev/null +++ b/src/js/components/accountLanding/table/HeaderRow.jsx @@ -0,0 +1,34 @@ +/** + * HeaderRow.jsx + * Created by Lizzie Salita 8/4/17 + **/ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import AgencyLandingHeaderCellContainer from + 'containers/agencyLanding/table/AgencyLandingHeaderCellContainer'; + +const propTypes = { + columns: PropTypes.array +}; + +export default class HeaderRow extends React.Component { + render() { + const headers = this.props.columns.map((column, i) => ( + + + + )); + + return ( + + {headers} + + ); + } +} + +HeaderRow.propTypes = propTypes; diff --git a/src/js/components/accountLanding/table/TableRow.jsx b/src/js/components/accountLanding/table/TableRow.jsx new file mode 100644 index 0000000000..4fc1e9f76e --- /dev/null +++ b/src/js/components/accountLanding/table/TableRow.jsx @@ -0,0 +1,54 @@ +/** + * TableRow.jsx + * Created by Lizzie Salita 8/4/17 + **/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import AccountLinkCell from './cells/AccountLinkCell'; +import GenericCell from './cells/GenericCell'; + +const propTypes = { + columns: PropTypes.array.isRequired, + account: PropTypes.object, + rowIndex: PropTypes.number.isRequired, + accountSearchString: PropTypes.string +}; + +export default class TableRow extends React.PureComponent { + render() { + const cells = this.props.columns.map((column) => { + if (column.columnName === 'account_name') { + // show the account link cell + return ( + + + + ); + } + return ( + + + + ); + }); + + return ( + + {cells} + + ); + } +} + +TableRow.propTypes = propTypes; diff --git a/src/js/components/accountLanding/table/cells/AccountLinkCell.jsx b/src/js/components/accountLanding/table/cells/AccountLinkCell.jsx new file mode 100644 index 0000000000..9fe0f86ed8 --- /dev/null +++ b/src/js/components/accountLanding/table/cells/AccountLinkCell.jsx @@ -0,0 +1,47 @@ +/** + * AccountLinkCell.jsx + * Created by Lizzie Salita 8/4/17 + **/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import reactStringReplace from 'react-string-replace'; + +const propTypes = { + name: PropTypes.string, + rowIndex: PropTypes.number, + column: PropTypes.string, + id: PropTypes.number, + accountSearchString: PropTypes.string +}; + +export default class AccountLinkCell extends React.Component { + render() { + // calculate even-odd class names + let rowClass = 'row-even'; + if (this.props.rowIndex % 2 === 0) { + // row index is zero-based + rowClass = 'row-odd'; + } + + let name = this.props.name; + // highlight the matched string if applicable + if (this.props.accountSearchString !== '') { + name = reactStringReplace(this.props.name, this.props.accountSearchString, (match, i) => ( + {match} + )); + } + + return ( +
+ +
+ ); + } +} + +AccountLinkCell.propTypes = propTypes; diff --git a/src/js/containers/accountLanding/AccountLandingContainer.jsx b/src/js/containers/accountLanding/AccountLandingContainer.jsx new file mode 100644 index 0000000000..6b4c5990e5 --- /dev/null +++ b/src/js/containers/accountLanding/AccountLandingContainer.jsx @@ -0,0 +1,220 @@ +/** + * AccountLandingContainer.jsx + * Created by Lizzie Salita 8/4/17 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { isCancel } from 'axios'; + +import { Search } from 'js-search'; +import { orderBy } from 'lodash'; + +import AccountsTableFields from 'dataMapping/accountLanding/accountsTableFields'; +import * as AccountLandingHelper from 'helpers/accountLandingHelper'; +import * as MoneyFormatter from 'helpers/moneyFormatter'; + +import AccountLandingContent from 'components/accountLanding/AccountLandingContent'; + +const propTypes = { + accountsOrder: PropTypes.object +}; + +export class AccountLandingContainer extends React.Component { + constructor(props) { + super(props); + + this.state = { + columns: [], + inFlight: false, + currentFY: '', + accountSearchString: '', + fullData: [], + results: [] + }; + + this.accountsRequest = null; + this.setAccountSearchString = this.setAccountSearchString.bind(this); + } + + componentDidMount() { + this.showColumns(); + } + + componentDidUpdate(prevProps) { + if (this.props.accountsOrder !== prevProps.accountsOrder) { + // table sort changed + this.performSearch(); + } + } + + componentWillUnmount() { + if (this.accountsRequest) { + this.accountsRequest.cancel(); + } + } + + setAccountSearchString(accountSearchString) { + let searchValue = ''; + if (accountSearchString.length > 2) { + searchValue = accountSearchString; + } + + this.setState({ + accountSearchString: searchValue + }, () => { + this.performSearch(); + }); + } + + showColumns() { + const columns = []; + const sortOrder = AccountsTableFields.defaultSortDirection; + + AccountsTableFields.order.forEach((col) => { + let displayName = AccountsTableFields[col]; + if ((col === 'budget_authority_amount') || + (col === 'percentage_of_total_budget_authority')) { + // Add (FY YYYY) to Budget Authority and Percent of Total U.S. Budget column headers + if (this.state.fy) { + displayName = `${displayName} (FY ${this.state.currentFY})`; + } + } + const column = { + columnName: col, + displayName, + defaultDirection: sortOrder[col] + }; + columns.push(column); + }); + + this.setState({ + columns + }, () => { + this.fetchAccounts(); + }); + } + + fetchAccounts() { + if (this.accountsRequest) { + // a request is in-flight, cancel it + this.accountsRequest.cancel(); + } + + this.setState({ + inFlight: true + }); + + // generate the params + const params = { + sort: this.props.accountsOrder.field, + order: this.props.accountsOrder.direction + }; + + this.accountsRequest = AccountLandingHelper.fetchAllAccounts(params); + + this.accountsRequest.promise + .then((res) => { + this.setState({ + inFlight: false + }); + + this.parseAccounts(res.data); + }) + .catch((err) => { + this.accountsRequest = null; + if (!isCancel(err)) { + this.setState({ + inFlight: false + }); + console.log(err); + } + }); + } + + parseAccounts(data) { + const accounts = []; + + data.results.forEach((item) => { + // Format budget authority amount + const formattedCurrency = + MoneyFormatter.formatMoneyWithPrecision(item.budget_authority_amount, 0); + + // Convert from decimal value to percentage and round to 2 decimal places + const percentage = (item.percentage_of_total_budget_authority * 100).toFixed(2); + + let percent = `${percentage}%`; + if (percent === '0.00%') { + percent = 'Less than 0.01%'; + } + + const account = { + account_id: item.account_id, + account_name: `${item.account_name} (${item.abbreviation})`, + budget_authority_amount: item.budget_authority_amount, + percentage_of_total_budget_authority: item.percentage_of_total_budget_authority, + display: { + account_name: `${item.account_name} (${item.abbreviation})`, + budget_authority_amount: formattedCurrency, + percentage_of_total_budget_authority: percent + } + }; + accounts.push(account); + }); + + this.setState({ + fullData: accounts + }, () => { + this.performSearch(); + }); + } + + performSearch() { + // perform a local search + const search = new Search('account_id'); + search.addIndex('account_name'); + search.addDocuments(this.state.fullData); + + // return the full data set if no search string is provided + let results = this.state.fullData; + if (this.state.accountSearchString !== '') { + results = search.search(this.state.accountSearchString); + } + + // now sort the results by the appropriate table column and direction + const orderedResults = orderBy(results, + [this.props.accountsOrder.field], [this.props.accountsOrder.direction]); + + this.setState({ + results: orderedResults + }); + } + + render() { + const resultsCount = this.state.results.length; + let resultsText = `${resultsCount} results`; + if (resultsCount === 1) { + resultsText = `${resultsCount} result`; + } + + return ( + + ); + } +} + +AccountLandingContainer.propTypes = propTypes; + +export default connect( + (state) => ({ + accountsOrder: state.accountLanding.accountsOrder + }) +)(AccountLandingContainer); diff --git a/src/js/containers/accountLanding/table/AccountLandingHeaderCellContainer.jsx b/src/js/containers/accountLanding/table/AccountLandingHeaderCellContainer.jsx new file mode 100644 index 0000000000..83ee9bc676 --- /dev/null +++ b/src/js/containers/accountLanding/table/AccountLandingHeaderCellContainer.jsx @@ -0,0 +1,64 @@ +/** + * AccountLandingHeaderCellContainer.jsx + * Created by Lizzie Salita 8/4/17 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; + +// just import the relevant action(s) +import { setAccountsOrder } from 'redux/actions/accountLanding/accountLandingActions'; + +import ResultsTableHeaderCell from 'components/search/table/cells/ResultsTableHeaderCell'; + +// combine the action functions into an object for the react-redux bindings +const actions = { + setAccountsOrder +}; + +const propTypes = { + setAccountsOrder: PropTypes.func, + order: PropTypes.object, + displayName: PropTypes.string, + defaultDirection: PropTypes.string, + columnName: PropTypes.string, + isLast: PropTypes.bool +}; + +class AccountLandingHeaderCellContainer extends React.Component { + constructor(props) { + super(props); + + this.setAccountsOrder = this.setAccountsOrder.bind(this); + } + + setAccountsOrder(field, direction) { + this.props.setAccountsOrder({ + field, + direction + }); + } + + render() { + return ( + + ); + } +} + +AccountLandingHeaderCellContainer.propTypes = propTypes; + +export default connect( + (state) => ({ + order: state.accountLanding.accountsOrder + }), + (dispatch) => bindActionCreators(actions, dispatch) +)(AccountLandingHeaderCellContainer); diff --git a/src/js/dataMapping/accountLanding/accountsTableFields.js b/src/js/dataMapping/accountLanding/accountsTableFields.js new file mode 100644 index 0000000000..b7f8e33698 --- /dev/null +++ b/src/js/dataMapping/accountLanding/accountsTableFields.js @@ -0,0 +1,20 @@ +const agenciesTableFields = { + defaultSortDirection: { + account_number: 'desc', + account_name: 'asc', + managing_agency: 'asc', + budget_authority_amount: 'desc' + }, + order: [ + 'account_number', + 'account_name', + 'managing_agency', + 'budget_authority_amount' + ], + account_number: 'Account Number', + account_name: 'Agency Name', + managing_agency: 'Managing Agency', + budget_authority_amount: 'Budgetary Resources' +}; + +export default agenciesTableFields; diff --git a/src/js/helpers/accountLandingHelper.js b/src/js/helpers/accountLandingHelper.js new file mode 100644 index 0000000000..1543d25983 --- /dev/null +++ b/src/js/helpers/accountLandingHelper.js @@ -0,0 +1,25 @@ +/** + * accountLandingHelper.js + * Created by Lizzie Salita 8/4/17 + **/ + +import Axios, { CancelToken } from 'axios'; + +import kGlobalConstants from 'GlobalConstants'; + +export const fetchAllAgencies = (params) => { + const source = CancelToken.source(); + return { + promise: Axios.request({ + url: 'v2/references/accounts/', + baseURL: kGlobalConstants.API, + method: 'get', + params, + cancelToken: source.token + }), + cancel() { + source.cancel(); + } + }; +}; + diff --git a/src/js/redux/actions/accountLanding/accountLandingActions.js b/src/js/redux/actions/accountLanding/accountLandingActions.js new file mode 100644 index 0000000000..25de53c62b --- /dev/null +++ b/src/js/redux/actions/accountLanding/accountLandingActions.js @@ -0,0 +1,9 @@ +/** + * accountLandingActions.js + * Created by Lizzie Salita 8/4/17 + */ + +export const setAccountsOrder = (state) => ({ + type: 'SET_ACCOUNTS_ORDER', + order: state +}); diff --git a/src/js/redux/reducers/accountLanding/accountLandingReducer.js b/src/js/redux/reducers/accountLanding/accountLandingReducer.js new file mode 100644 index 0000000000..d63166bf51 --- /dev/null +++ b/src/js/redux/reducers/accountLanding/accountLandingReducer.js @@ -0,0 +1,27 @@ +/** + * accountLandingReducer.js + * Created by Lizzie Salita 8/4/17 + */ + +const initialState = { + accountsOrder: { + field: 'budget_authority_amount', + direction: 'desc' + } +}; + +const accountLandingReducer = (state = initialState, action) => { + switch (action.type) { + case 'SET_ACCOUNTS_ORDER': { + const order = Object.assign({}, state.accountsOrder, action.order); + + return Object.assign({}, state, { + accountsOrder: order + }); + } + default: + return state; + } +}; + +export default accountLandingReducer; From 10e7244a0a71d0c6b896c295a92a97da1a2aac88 Mon Sep 17 00:00:00 2001 From: Elizabeth Salita Date: Fri, 4 Aug 2017 16:19:55 -0400 Subject: [PATCH 02/89] Sass files, connect route, mock API response --- src/_scss/custom.scss | 1 + .../accountLanding/accountLandingPage.scss | 17 +++ .../accountLanding/accountLandingSearch.scss | 50 ++++++++ .../pages/accountLanding/header/header.scss | 1 + .../cells/_accountLandingHeaderCell.scss | 49 ++++++++ .../accountLanding/table/resultsSection.scss | 33 +++++ .../pages/accountLanding/table/table.scss | 60 ++++++++++ .../accountLanding/table/TableRow.jsx | 8 +- .../table/cells/AccountLinkCell.jsx | 11 +- .../AccountLandingContainer.jsx | 113 +++++++++++------- src/js/containers/router/RouterRoutes.jsx | 9 ++ .../accountLanding/accountsTableFields.js | 2 +- src/js/helpers/accountLandingHelper.js | 4 +- src/js/redux/reducers/index.js | 4 +- 14 files changed, 308 insertions(+), 54 deletions(-) create mode 100644 src/_scss/pages/accountLanding/accountLandingPage.scss create mode 100644 src/_scss/pages/accountLanding/accountLandingSearch.scss create mode 100644 src/_scss/pages/accountLanding/header/header.scss create mode 100644 src/_scss/pages/accountLanding/table/cells/_accountLandingHeaderCell.scss create mode 100644 src/_scss/pages/accountLanding/table/resultsSection.scss create mode 100644 src/_scss/pages/accountLanding/table/table.scss diff --git a/src/_scss/custom.scss b/src/_scss/custom.scss index 06607d3185..9965b2c03f 100644 --- a/src/_scss/custom.scss +++ b/src/_scss/custom.scss @@ -14,5 +14,6 @@ @import './layouts/article/aboutArticle'; @import './pages/testStyle/testStyle'; @import './pages/agencyLanding/agencyLandingPage'; +@import './pages/accountLanding/accountLandingPage'; // ***** MODALS @import './pages/modals/all'; diff --git a/src/_scss/pages/accountLanding/accountLandingPage.scss b/src/_scss/pages/accountLanding/accountLandingPage.scss new file mode 100644 index 0000000000..e0282061e3 --- /dev/null +++ b/src/_scss/pages/accountLanding/accountLandingPage.scss @@ -0,0 +1,17 @@ +.usa-da-account-landing { + @import "layouts/landingPage/landingPage"; + @import "./header/header"; + @import './accountLandingSearch'; + @import './table/resultsSection'; + + .landing-page-section { + &.results-count { + font-style: italic; + padding: rem(20) 0; + font-size: rem(16); + } + } + @include accountLandingSearch; + @include resultsSection; + +} \ No newline at end of file diff --git a/src/_scss/pages/accountLanding/accountLandingSearch.scss b/src/_scss/pages/accountLanding/accountLandingSearch.scss new file mode 100644 index 0000000000..b1f1bec2c9 --- /dev/null +++ b/src/_scss/pages/accountLanding/accountLandingSearch.scss @@ -0,0 +1,50 @@ +@mixin accountLandingSearch { + .account-landing-search { + @include span-columns(16); + background-color: $color-primary-alt-lightest; + padding: rem(5); + border: 1px solid $color-vis-lightest; + text-align: center; + form { + position: relative; + input.search-field { + color: $color-gray-light; + font-size: rem(14); + font-weight: 300; + line-height: rem(36); + padding: 0 rem(30) 0 rem(4); + width: 100%; + } + .search-button { + position: absolute; + top: rem(5); + left: 90%; + + @include button-unstyled; + width: 25px; + height: 25px; + + svg { + fill: $color-gray-light; + } + } + } + @include media($medium-screen) { + padding: rem(30); + form { + input.search-field { + width: 75%; + font-size: rem(28); + padding: rem(15); + padding-right: rem(30); + } + .search-button { + left: 80%; + top: rem(15); + width: 36px; + height: 36px; + } + } + } + } +} \ No newline at end of file diff --git a/src/_scss/pages/accountLanding/header/header.scss b/src/_scss/pages/accountLanding/header/header.scss new file mode 100644 index 0000000000..f0d2edb6a2 --- /dev/null +++ b/src/_scss/pages/accountLanding/header/header.scss @@ -0,0 +1 @@ +@import "components/pageTitleBar/pageTitleBar"; \ No newline at end of file diff --git a/src/_scss/pages/accountLanding/table/cells/_accountLandingHeaderCell.scss b/src/_scss/pages/accountLanding/table/cells/_accountLandingHeaderCell.scss new file mode 100644 index 0000000000..76ba48ea4a --- /dev/null +++ b/src/_scss/pages/accountLanding/table/cells/_accountLandingHeaderCell.scss @@ -0,0 +1,49 @@ +@mixin accountLandingHeader() { + + .cell-content { + white-space: normal; + font-size: rem(18); + font-weight: 600; + line-height: 25px; + color: $color-base; + cursor: pointer; + + .header-sort { + display: table; + padding: rem(11) rem(20); + + .header-label { + display: table-cell; + vertical-align: middle; + } + + .header-icons { + display: table-cell; + vertical-align: middle; + padding-left: rem(5); + height: rem(10); + + .sort-icon { + @include button-unstyled; + display: block; + line-height: rem(10); + width: rem(14); + height: rem(10); + text-align: center; + + svg { + fill: #86888E; + height: rem(11); + width: rem(11); + } + + &.active { + svg { + fill: $color-active; + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/_scss/pages/accountLanding/table/resultsSection.scss b/src/_scss/pages/accountLanding/table/resultsSection.scss new file mode 100644 index 0000000000..0b776fd8a8 --- /dev/null +++ b/src/_scss/pages/accountLanding/table/resultsSection.scss @@ -0,0 +1,33 @@ +@mixin resultsSection { + .account-landing-results { + transition: opacity 0.25s ease; + + @import './table'; + + .loading-table { + opacity: 0.5; + @include transition(opacity 0.25s ease-in); + } + + .loaded-table { + opacity: 1; + @include transition(opacity 0.25s ease-in); + } + + .account-landing-table-width-master { + width: 100%; + } + + .results-table-message { + margin: rem(90) auto; + font-size: rem(36); + font-weight: 300; + color: $color-base; + text-align: center; + span { + color: $color-active; + font-weight: normal; + } + } + } +} diff --git a/src/_scss/pages/accountLanding/table/table.scss b/src/_scss/pages/accountLanding/table/table.scss new file mode 100644 index 0000000000..7e27d6972c --- /dev/null +++ b/src/_scss/pages/accountLanding/table/table.scss @@ -0,0 +1,60 @@ +.account-landing-results-table { + display: block; + overflow: scroll; + + &.no-results { + border: none; + } + table { + margin-top: rem(15); + width: rem(500); + + @include media($tablet-screen) { + width: auto; + } + + thead { + tr { + border-bottom: solid 1px $color-gray-lighter; + td { + border: 0; + &:last-child { + .cell-content { + float: right; + } + } + } + } + .award-result-header-cell { + @import "./cells/_accountLandingHeaderCell"; + @include accountLandingHeader(); + } + } + tbody { + tr { + td { + border: 0; + &:last-child { + .cell-content { + float: right; + } + } + } + .account-link-cell { + .cell-content { + a { + span { + text-decoration: underline; + font-weight: 600; + } + } + } + } + // gray out even rows + .row-even { + background-color: #f7f7f7; + } + } + } + } +} \ No newline at end of file diff --git a/src/js/components/accountLanding/table/TableRow.jsx b/src/js/components/accountLanding/table/TableRow.jsx index 4fc1e9f76e..dc6ddedd15 100644 --- a/src/js/components/accountLanding/table/TableRow.jsx +++ b/src/js/components/accountLanding/table/TableRow.jsx @@ -5,8 +5,8 @@ import React from 'react'; import PropTypes from 'prop-types'; +import GenericCell from 'components/agencyLanding/table/cells/GenericCell'; import AccountLinkCell from './cells/AccountLinkCell'; -import GenericCell from './cells/GenericCell'; const propTypes = { columns: PropTypes.array.isRequired, @@ -17,11 +17,16 @@ const propTypes = { export default class TableRow extends React.PureComponent { render() { + let rowClass = 'row-even'; + if (this.props.rowIndex % 2 === 0) { + rowClass = 'row-odd'; + } const cells = this.props.columns.map((column) => { if (column.columnName === 'account_name') { // show the account link cell return ( +
diff --git a/src/js/containers/accountLanding/AccountLandingContainer.jsx b/src/js/containers/accountLanding/AccountLandingContainer.jsx index 6b4c5990e5..227b5641ab 100644 --- a/src/js/containers/accountLanding/AccountLandingContainer.jsx +++ b/src/js/containers/accountLanding/AccountLandingContainer.jsx @@ -6,13 +6,13 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { isCancel } from 'axios'; +// import { isCancel } from 'axios'; import { Search } from 'js-search'; import { orderBy } from 'lodash'; import AccountsTableFields from 'dataMapping/accountLanding/accountsTableFields'; -import * as AccountLandingHelper from 'helpers/accountLandingHelper'; +// import * as AccountLandingHelper from 'helpers/accountLandingHelper'; import * as MoneyFormatter from 'helpers/moneyFormatter'; import AccountLandingContent from 'components/accountLanding/AccountLandingContent'; @@ -97,40 +97,71 @@ export class AccountLandingContainer extends React.Component { } fetchAccounts() { - if (this.accountsRequest) { - // a request is in-flight, cancel it - this.accountsRequest.cancel(); - } - - this.setState({ - inFlight: true - }); - - // generate the params - const params = { - sort: this.props.accountsOrder.field, - order: this.props.accountsOrder.direction - }; - - this.accountsRequest = AccountLandingHelper.fetchAllAccounts(params); - - this.accountsRequest.promise - .then((res) => { - this.setState({ - inFlight: false - }); - - this.parseAccounts(res.data); - }) - .catch((err) => { - this.accountsRequest = null; - if (!isCancel(err)) { - this.setState({ - inFlight: false - }); - console.log(err); + const mockData = { + results: [ + { + account_id: 1, + account_number: '123-4567', + account_name: 'Mock Account', + managing_agency: 'Mock Agency', + managing_agency_acronym: 'XYZ', + budget_authority_amount: 5000000 + }, + { + account_id: 2, + account_number: '098-7654', + account_name: 'Mock Account 2', + managing_agency: 'Mock Agency 2', + managing_agency_acronym: 'ABC', + budget_authority_amount: 6000000 + }, + { + account_id: 3, + account_number: '234-5678', + account_name: 'Test Account', + managing_agency: 'Mock Agency 3', + managing_agency_acronym: 'DEF', + budget_authority_amount: 4000000 } - }); + ] + }; + this.parseAccounts(mockData); + + // TODO - Lizzie: add API call when endpoint is ready + // if (this.accountsRequest) { + // // a request is in-flight, cancel it + // this.accountsRequest.cancel(); + // } + // + // this.setState({ + // inFlight: true + // }); + // + // // generate the params + // const params = { + // sort: this.props.accountsOrder.field, + // order: this.props.accountsOrder.direction + // }; + // + // this.accountsRequest = AccountLandingHelper.fetchAllAccounts(params); + // + // this.accountsRequest.promise + // .then((res) => { + // this.setState({ + // inFlight: false + // }); + // + // this.parseAccounts(res.data); + // }) + // .catch((err) => { + // this.accountsRequest = null; + // if (!isCancel(err)) { + // this.setState({ + // inFlight: false + // }); + // console.log(err); + // } + // }); } parseAccounts(data) { @@ -151,13 +182,15 @@ export class AccountLandingContainer extends React.Component { const account = { account_id: item.account_id, - account_name: `${item.account_name} (${item.abbreviation})`, + account_number: item.account_number, + managing_agency: `${item.managing_agency} (${item.managing_agency_acronym})`, + account_name: item.account_name, budget_authority_amount: item.budget_authority_amount, - percentage_of_total_budget_authority: item.percentage_of_total_budget_authority, display: { - account_name: `${item.account_name} (${item.abbreviation})`, - budget_authority_amount: formattedCurrency, - percentage_of_total_budget_authority: percent + account_number: `${item.account_number}`, + account_name: `${item.account_name}`, + managing_agency: `${item.managing_agency} (${item.managing_agency_acronym})`, + budget_authority_amount: formattedCurrency } }; accounts.push(account); diff --git a/src/js/containers/router/RouterRoutes.jsx b/src/js/containers/router/RouterRoutes.jsx index 1c93879706..d82c7c7255 100644 --- a/src/js/containers/router/RouterRoutes.jsx +++ b/src/js/containers/router/RouterRoutes.jsx @@ -145,6 +145,15 @@ const routes = { cb(require('components/agencyLanding/AgencyLandingPage').default); }); } + }, + { + path: '/federal_account', + parent: '/federal_account', + component: (cb) => { + require.ensure([], (require) => { + cb(require('components/accountLanding/AccountLandingPage').default); + }); + } } ], notfound: { diff --git a/src/js/dataMapping/accountLanding/accountsTableFields.js b/src/js/dataMapping/accountLanding/accountsTableFields.js index b7f8e33698..c44e41b6ac 100644 --- a/src/js/dataMapping/accountLanding/accountsTableFields.js +++ b/src/js/dataMapping/accountLanding/accountsTableFields.js @@ -12,7 +12,7 @@ const agenciesTableFields = { 'budget_authority_amount' ], account_number: 'Account Number', - account_name: 'Agency Name', + account_name: 'Account Name', managing_agency: 'Managing Agency', budget_authority_amount: 'Budgetary Resources' }; diff --git a/src/js/helpers/accountLandingHelper.js b/src/js/helpers/accountLandingHelper.js index 1543d25983..d7f7ae3c42 100644 --- a/src/js/helpers/accountLandingHelper.js +++ b/src/js/helpers/accountLandingHelper.js @@ -7,11 +7,11 @@ import Axios, { CancelToken } from 'axios'; import kGlobalConstants from 'GlobalConstants'; -export const fetchAllAgencies = (params) => { +export const fetchAllAccounts = (params) => { const source = CancelToken.source(); return { promise: Axios.request({ - url: 'v2/references/accounts/', + url: 'v2/references/federal_account/', baseURL: kGlobalConstants.API, method: 'get', params, diff --git a/src/js/redux/reducers/index.js b/src/js/redux/reducers/index.js index 7191cdfc63..f90bad9f49 100644 --- a/src/js/redux/reducers/index.js +++ b/src/js/redux/reducers/index.js @@ -24,6 +24,7 @@ import agencyLandingReducer from './agencyLanding/agencyLandingReducer'; import cfdaReducer from './search/cfdaReducer'; import naicsReducer from './search/naicsReducer'; import pscReducer from './search/pscReducer'; +import accountLandingReducer from './accountLanding/accountLandingReducer'; const appReducer = combineReducers({ resultsMeta: resultsMetaReducer, @@ -44,7 +45,8 @@ const appReducer = combineReducers({ account: accountReducer, agency: agencyReducer, glossary: glossaryReducer, - agencyLanding: agencyLandingReducer + agencyLanding: agencyLandingReducer, + accountLanding: accountLandingReducer }); export default appReducer; From 177fa9da5ec8a55cefeea53cad939c52b1db88fe Mon Sep 17 00:00:00 2001 From: Elizabeth Salita Date: Mon, 7 Aug 2017 14:42:43 -0400 Subject: [PATCH 03/89] Fixed column sort --- src/js/components/accountLanding/table/HeaderRow.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/js/components/accountLanding/table/HeaderRow.jsx b/src/js/components/accountLanding/table/HeaderRow.jsx index 18da01a484..359bae34c7 100644 --- a/src/js/components/accountLanding/table/HeaderRow.jsx +++ b/src/js/components/accountLanding/table/HeaderRow.jsx @@ -6,8 +6,8 @@ import React from 'react'; import PropTypes from 'prop-types'; -import AgencyLandingHeaderCellContainer from - 'containers/agencyLanding/table/AgencyLandingHeaderCellContainer'; +import AccountLandingHeaderCellContainer from + 'containers/accountLanding/table/AccountLandingHeaderCellContainer'; const propTypes = { columns: PropTypes.array @@ -17,7 +17,7 @@ export default class HeaderRow extends React.Component { render() { const headers = this.props.columns.map((column, i) => ( - From 4c872cce400e3443eb52ca2a0ea5d6db8be6f711 Mon Sep 17 00:00:00 2001 From: Elizabeth Salita Date: Thu, 10 Aug 2017 14:35:10 -0400 Subject: [PATCH 04/89] Added searching on multiple columns --- .../pages/accountLanding/table/table.scss | 10 +++--- .../accountLanding/table/TableRow.jsx | 18 ++++++++-- .../table/cells/GenericAccountCell.jsx | 36 +++++++++++++++++++ .../AccountLandingContainer.jsx | 2 ++ 4 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 src/js/components/accountLanding/table/cells/GenericAccountCell.jsx diff --git a/src/_scss/pages/accountLanding/table/table.scss b/src/_scss/pages/accountLanding/table/table.scss index 7e27d6972c..bb97a2886a 100644 --- a/src/_scss/pages/accountLanding/table/table.scss +++ b/src/_scss/pages/accountLanding/table/table.scss @@ -40,13 +40,11 @@ } } } - .account-link-cell { + .account-link-cell, .generic-account-cell { .cell-content { - a { - span { - text-decoration: underline; - font-weight: 600; - } + span { + text-decoration: underline; + font-weight: 600; } } } diff --git a/src/js/components/accountLanding/table/TableRow.jsx b/src/js/components/accountLanding/table/TableRow.jsx index dc6ddedd15..4514599864 100644 --- a/src/js/components/accountLanding/table/TableRow.jsx +++ b/src/js/components/accountLanding/table/TableRow.jsx @@ -7,6 +7,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import GenericCell from 'components/agencyLanding/table/cells/GenericCell'; import AccountLinkCell from './cells/AccountLinkCell'; +import GenericAccountCell from './cells/GenericAccountCell'; const propTypes = { columns: PropTypes.array.isRequired, @@ -37,14 +38,27 @@ export default class TableRow extends React.PureComponent { ); } + else if (column.columnName === 'budget_authority_amount') { + return ( + + + + ); + } return ( - + column={column.columnName} + accountSearchString={this.props.accountSearchString} /> ); }); diff --git a/src/js/components/accountLanding/table/cells/GenericAccountCell.jsx b/src/js/components/accountLanding/table/cells/GenericAccountCell.jsx new file mode 100644 index 0000000000..683e4118b0 --- /dev/null +++ b/src/js/components/accountLanding/table/cells/GenericAccountCell.jsx @@ -0,0 +1,36 @@ +/** + * GenericAccountCell.jsx + * Created by Lizzie Salita 08/10/17 + **/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import reactStringReplace from 'react-string-replace'; + +const propTypes = { + data: PropTypes.string, + rowIndex: PropTypes.number, + column: PropTypes.string, + accountSearchString: PropTypes.string +}; + +export default class GenericAccountCell extends React.Component { + render() { + let data = this.props.data; + // highlight the matched string if applicable + if (this.props.accountSearchString !== '') { + data = reactStringReplace(this.props.data, this.props.accountSearchString, (match, i) => ( + {match} + )); + } + return ( +
+
+ {data} +
+
+ ); + } +} + +GenericAccountCell.propTypes = propTypes; diff --git a/src/js/containers/accountLanding/AccountLandingContainer.jsx b/src/js/containers/accountLanding/AccountLandingContainer.jsx index 227b5641ab..59c104f3b4 100644 --- a/src/js/containers/accountLanding/AccountLandingContainer.jsx +++ b/src/js/containers/accountLanding/AccountLandingContainer.jsx @@ -207,6 +207,8 @@ export class AccountLandingContainer extends React.Component { // perform a local search const search = new Search('account_id'); search.addIndex('account_name'); + search.addIndex('account_number'); + search.addIndex('managing_agency'); search.addDocuments(this.state.fullData); // return the full data set if no search string is provided From 601c7053b23a22afdff2d9d6316abba5e7174b41 Mon Sep 17 00:00:00 2001 From: Elizabeth Salita Date: Mon, 14 Aug 2017 12:19:45 -0400 Subject: [PATCH 05/89] Pagination component --- .../accountLanding/AccountLandingContent.jsx | 8 +- .../AccountLandingPagination.jsx | 139 ++++++++++++++++++ .../AccountLandingContainer.jsx | 41 +++++- 3 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 src/js/components/accountLanding/AccountLandingPagination.jsx diff --git a/src/js/components/accountLanding/AccountLandingContent.jsx b/src/js/components/accountLanding/AccountLandingContent.jsx index 63c0ac3a00..b4ffa1f51c 100644 --- a/src/js/components/accountLanding/AccountLandingContent.jsx +++ b/src/js/components/accountLanding/AccountLandingContent.jsx @@ -8,14 +8,17 @@ import PropTypes from 'prop-types'; import AccountLandingSearchBar from './AccountLandingSearchBar'; import AccountLandingResultsSection from './AccountLandingResultsSection'; +import AccountLandingPagination from './AccountLandingPagination'; const propTypes = { resultsText: PropTypes.string, results: PropTypes.array, + items: PropTypes.array, accountSearchString: PropTypes.string, inFlight: PropTypes.bool, columns: PropTypes.array, - setAccountSearchString: PropTypes.func + setAccountSearchString: PropTypes.func, + onChangePage: PropTypes.func }; export default class AccountLandingContent extends React.Component { @@ -37,6 +40,9 @@ export default class AccountLandingContent extends React.Component {
{this.props.resultsText} +
pager.totalPages) { + return; + } + + // get new pager object for specified page + pager = this.getPager(items.length, page, 3); + + // get new page of items from items array + const pageOfItems = items.slice(pager.startIndex, pager.endIndex + 1); + + // update state + this.setState({ pager }); + + // call change page function in parent component + this.props.onChangePage(pageOfItems); + } + + getPager(totalItems, currentPage, pageSize) { + // calculate total pages + const totalPages = Math.ceil(totalItems / pageSize); + + let startPage; + let endPage; + if (totalPages <= 10) { + // less than 10 total pages so show all + startPage = 1; + endPage = totalPages; + } + else if (currentPage <= 6) { + startPage = 1; + endPage = 10; + } + else if (currentPage + 4 >= totalPages) { + startPage = totalPages - 9; + endPage = totalPages; + } + else { + startPage = currentPage - 5; + endPage = currentPage + 4; + } + + // calculate start and end item indexes + const startIndex = (currentPage - 1) * pageSize; + const endIndex = Math.min(startIndex + (pageSize - 1), (totalItems - 1)); + + // create an array of pages to repeat in the pager control + const pages = range(startPage, endPage + 1); + + // return object with all pager properties + return { + totalItems, + currentPage, + pageSize, + totalPages, + startPage, + endPage, + startIndex, + endIndex, + pages + }; + } + + render() { + const pager = this.state.pager; + + if (!pager.pages || pager.pages.length <= 1) { + // don't display pager if there is only 1 page + return null; + } + + return ( +
    +
  • + +
  • +
  • + +
  • + {pager.pages.map((page, index) => +
  • + +
  • + )} +
  • + +
  • +
  • + +
  • +
+ ); + } +} + +AccountLandingPagination.propTypes = propTypes; +AccountLandingPagination.defaultProps = defaultProps; diff --git a/src/js/containers/accountLanding/AccountLandingContainer.jsx b/src/js/containers/accountLanding/AccountLandingContainer.jsx index 59c104f3b4..d704a02890 100644 --- a/src/js/containers/accountLanding/AccountLandingContainer.jsx +++ b/src/js/containers/accountLanding/AccountLandingContainer.jsx @@ -31,11 +31,13 @@ export class AccountLandingContainer extends React.Component { currentFY: '', accountSearchString: '', fullData: [], - results: [] + results: [], + pageOfItems: [] }; this.accountsRequest = null; this.setAccountSearchString = this.setAccountSearchString.bind(this); + this.onChangePage = this.onChangePage.bind(this); } componentDidMount() { @@ -55,6 +57,11 @@ export class AccountLandingContainer extends React.Component { } } + onChangePage(pageOfItems) { + // update state with new page of items + this.setState({ pageOfItems }); + } + setAccountSearchString(accountSearchString) { let searchValue = ''; if (accountSearchString.length > 2) { @@ -113,7 +120,7 @@ export class AccountLandingContainer extends React.Component { account_name: 'Mock Account 2', managing_agency: 'Mock Agency 2', managing_agency_acronym: 'ABC', - budget_authority_amount: 6000000 + budget_authority_amount: 6500000 }, { account_id: 3, @@ -121,6 +128,30 @@ export class AccountLandingContainer extends React.Component { account_name: 'Test Account', managing_agency: 'Mock Agency 3', managing_agency_acronym: 'DEF', + budget_authority_amount: 4500000 + }, + { + account_id: 4, + account_number: '123-4567', + account_name: 'Mock Account 4', + managing_agency: 'Mock Agency 4', + managing_agency_acronym: 'XYZ', + budget_authority_amount: 5500000 + }, + { + account_id: 5, + account_number: '098-7654', + account_name: 'Mock Account 5', + managing_agency: 'Mock Agency 5', + managing_agency_acronym: 'ABC', + budget_authority_amount: 6000000 + }, + { + account_id: 6, + account_number: '234-5678', + account_name: 'Test Account 2', + managing_agency: 'Mock Agency 6', + managing_agency_acronym: 'DEF', budget_authority_amount: 4000000 } ] @@ -236,12 +267,14 @@ export class AccountLandingContainer extends React.Component { return ( + setAccountSearchString={this.setAccountSearchString} + onChangePage={this.onChangePage} /> ); } } From b80cbdfc640de3dec434f12549c665af7df2b27e Mon Sep 17 00:00:00 2001 From: Elizabeth Salita Date: Mon, 14 Aug 2017 15:46:17 -0400 Subject: [PATCH 06/89] Pagination buttons logic --- .../AccountLandingPagination.jsx | 73 ++++++++---- .../AccountLandingContainer.jsx | 112 ++++++++++++++++++ 2 files changed, 164 insertions(+), 21 deletions(-) diff --git a/src/js/components/accountLanding/AccountLandingPagination.jsx b/src/js/components/accountLanding/AccountLandingPagination.jsx index 9f553616bc..16bdcb3c42 100644 --- a/src/js/components/accountLanding/AccountLandingPagination.jsx +++ b/src/js/components/accountLanding/AccountLandingPagination.jsx @@ -46,6 +46,7 @@ export default class AccountLandingPagination extends React.Component { } // get new pager object for specified page + // TODO - Lizzie: update to 50 when endpoint is ready pager = this.getPager(items.length, page, 3); // get new page of items from items array @@ -64,22 +65,50 @@ export default class AccountLandingPagination extends React.Component { let startPage; let endPage; - if (totalPages <= 10) { - // less than 10 total pages so show all - startPage = 1; - endPage = totalPages; - } - else if (currentPage <= 6) { + let prevEllipses = '...'; + let nextEllipses = '...'; + let firstButton = ( +
  • + +
  • + ); + let lastButton = ( +
  • + +
  • + ); + if (totalPages < 5) { + // less than 5 total pages so show all startPage = 1; - endPage = 10; - } - else if (currentPage + 4 >= totalPages) { - startPage = totalPages - 9; endPage = totalPages; + prevEllipses = ''; + nextEllipses = ''; + firstButton = ''; + lastButton = ''; } else { - startPage = currentPage - 5; - endPage = currentPage + 4; + if (currentPage === 1) { + startPage = currentPage; + endPage = currentPage + 2; + } + else if (currentPage === totalPages) { + startPage = currentPage - 2; + endPage = currentPage; + } + else { + startPage = currentPage - 1; + endPage = currentPage + 1; + } + + + if(currentPage < 4) { + prevEllipses = ''; + firstButton = ''; + } + else if (currentPage > (totalPages - 3)) { + nextEllipses = ''; + lastButton = ''; + } } // calculate start and end item indexes @@ -99,7 +128,11 @@ export default class AccountLandingPagination extends React.Component { endPage, startIndex, endIndex, - pages + pages, + prevEllipses, + nextEllipses, + firstButton, + lastButton }; } @@ -114,21 +147,19 @@ export default class AccountLandingPagination extends React.Component { return (
    • - -
    • -
    • - +
    • + {pager.firstButton} + {pager.prevEllipses} {pager.pages.map((page, index) =>
    • )} + {pager.nextEllipses} + {pager.lastButton}
    • - -
    • -
    • - +
    ); diff --git a/src/js/containers/accountLanding/AccountLandingContainer.jsx b/src/js/containers/accountLanding/AccountLandingContainer.jsx index d704a02890..5b045926c0 100644 --- a/src/js/containers/accountLanding/AccountLandingContainer.jsx +++ b/src/js/containers/accountLanding/AccountLandingContainer.jsx @@ -153,6 +153,118 @@ export class AccountLandingContainer extends React.Component { managing_agency: 'Mock Agency 6', managing_agency_acronym: 'DEF', budget_authority_amount: 4000000 + }, + { + account_id: 7, + account_number: '123-4567', + account_name: 'Mock Account 7', + managing_agency: 'Mock Agency 7', + managing_agency_acronym: 'XYZ', + budget_authority_amount: 5000000 + }, + { + account_id: 8, + account_number: '098-7654', + account_name: 'Mock Account 8', + managing_agency: 'Mock Agency 8', + managing_agency_acronym: 'ABC', + budget_authority_amount: 6500000 + }, + { + account_id: 9, + account_number: '234-5678', + account_name: 'Test Account 3', + managing_agency: 'Mock Agency 9', + managing_agency_acronym: 'DEF', + budget_authority_amount: 4500000 + }, + { + account_id: 10, + account_number: '123-4567', + account_name: 'Mock Account 10', + managing_agency: 'Mock Agency 6', + managing_agency_acronym: 'XYZ', + budget_authority_amount: 5500000 + }, + { + account_id: 11, + account_number: '098-7654', + account_name: 'Mock Account 11', + managing_agency: 'Mock Agency', + managing_agency_acronym: 'XYZ', + budget_authority_amount: 6000000 + }, + { + account_id: 12, + account_number: '234-5678', + account_name: 'Test Account 4', + managing_agency: 'Mock Agency 12', + managing_agency_acronym: 'DEF', + budget_authority_amount: 4000000 + }, + { + account_id: 13, + account_number: '123-4567', + account_name: 'Mock Account 13', + managing_agency: 'Mock Agency', + managing_agency_acronym: 'XYZ', + budget_authority_amount: 5000000 + }, + { + account_id: 14, + account_number: '098-7654', + account_name: 'Mock Account 14', + managing_agency: 'Mock Agency 3', + managing_agency_acronym: 'ABC', + budget_authority_amount: 6500000 + }, + { + account_id: 15, + account_number: '234-5678', + account_name: 'Test Account 5', + managing_agency: 'Mock Agency 15', + managing_agency_acronym: 'DEF', + budget_authority_amount: 4500000 + }, + { + account_id: 16, + account_number: '123-4567', + account_name: 'Mock Account 16', + managing_agency: 'Mock Agency 2', + managing_agency_acronym: 'XYZ', + budget_authority_amount: 5500000 + }, + { + account_id: 17, + account_number: '098-7654', + account_name: 'Mock Account 17', + managing_agency: 'Mock Agency 17', + managing_agency_acronym: 'ABC', + budget_authority_amount: 6000000 + }, + { + account_id: 18, + account_number: '234-5678', + account_name: 'Test Account 6', + managing_agency: 'Mock Agency 2', + managing_agency_acronym: 'DEF', + budget_authority_amount: 4000000 + }, + { + account_id: 19, + account_number: '098-7654', + account_name: 'Mock Account 19', + managing_agency: 'Mock Agency 5', + managing_agency_acronym: 'ABC', + budget_authority_amount: 3000000 + }, + { + account_id: 20, + account_number: '234-5678', + account_name: 'Test Account 7', + managing_agency: 'Mock Agency 6', + managing_agency_acronym: 'DEF', + budget_authority_amount: 2000000 } ] }; From 4d1dd33d48aabdb2bf60c3fe3b02c7debff6d2f8 Mon Sep 17 00:00:00 2001 From: Elizabeth Salita Date: Mon, 14 Aug 2017 17:00:23 -0400 Subject: [PATCH 07/89] Pagination styling --- src/_scss/components/_pagination.scss | 27 +++++++++++++++++++ .../accountLanding/accountLandingPage.scss | 7 +++++ .../AccountLandingPagination.jsx | 6 ++--- 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 src/_scss/components/_pagination.scss diff --git a/src/_scss/components/_pagination.scss b/src/_scss/components/_pagination.scss new file mode 100644 index 0000000000..a0d0465a68 --- /dev/null +++ b/src/_scss/components/_pagination.scss @@ -0,0 +1,27 @@ +.pagination { + list-style-type: none; + display: inline-flex; + li { + button { + padding: rem(7) rem(10); + background-color: $color-white; + color: $color-gray; + border: solid rem(1) $color-gray-light; + font-style: normal; + font-size: rem(12); + margin: rem(5) rem(5) rem(5) 0; + } + &.active { + button { + background-color: $color-primary-alt-dark; + color: $color-white; + border-color: $color-primary-alt-dark; + } + } + } + .pagination-ellipsis { + font-weight: bold; + margin-top: 14px; + margin-right: 5px; + } +} \ No newline at end of file diff --git a/src/_scss/pages/accountLanding/accountLandingPage.scss b/src/_scss/pages/accountLanding/accountLandingPage.scss index e0282061e3..73d979e84a 100644 --- a/src/_scss/pages/accountLanding/accountLandingPage.scss +++ b/src/_scss/pages/accountLanding/accountLandingPage.scss @@ -1,5 +1,6 @@ .usa-da-account-landing { @import "layouts/landingPage/landingPage"; + @import "components/pagination"; @import "./header/header"; @import './accountLandingSearch'; @import './table/resultsSection'; @@ -9,7 +10,13 @@ font-style: italic; padding: rem(20) 0; font-size: rem(16); + .pagination { + margin: 0; + } } + .pagination { + float: right; + } } @include accountLandingSearch; @include resultsSection; diff --git a/src/js/components/accountLanding/AccountLandingPagination.jsx b/src/js/components/accountLanding/AccountLandingPagination.jsx index 16bdcb3c42..f741faf2e9 100644 --- a/src/js/components/accountLanding/AccountLandingPagination.jsx +++ b/src/js/components/accountLanding/AccountLandingPagination.jsx @@ -65,8 +65,8 @@ export default class AccountLandingPagination extends React.Component { let startPage; let endPage; - let prevEllipses = '...'; - let nextEllipses = '...'; + let prevEllipses = (...); + let nextEllipses = (...); let firstButton = (
  • @@ -101,7 +101,7 @@ export default class AccountLandingPagination extends React.Component { } - if(currentPage < 4) { + if (currentPage < 4) { prevEllipses = ''; firstButton = ''; } From 9ba5a7f3aa4eeee20e3a79c5d745c26c1aee0de3 Mon Sep 17 00:00:00 2001 From: Elizabeth Salita Date: Tue, 15 Aug 2017 11:57:54 -0400 Subject: [PATCH 08/89] Added page number to redux --- .../redux/actions/accountLanding/accountLandingActions.js | 5 +++++ .../reducers/accountLanding/accountLandingReducer.js | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/js/redux/actions/accountLanding/accountLandingActions.js b/src/js/redux/actions/accountLanding/accountLandingActions.js index 25de53c62b..b1cfacdf5d 100644 --- a/src/js/redux/actions/accountLanding/accountLandingActions.js +++ b/src/js/redux/actions/accountLanding/accountLandingActions.js @@ -7,3 +7,8 @@ export const setAccountsOrder = (state) => ({ type: 'SET_ACCOUNTS_ORDER', order: state }); + +export const setPageNumber = (state) => ({ + type: 'SET_PAGE_NUMBER', + number: state +}); diff --git a/src/js/redux/reducers/accountLanding/accountLandingReducer.js b/src/js/redux/reducers/accountLanding/accountLandingReducer.js index d63166bf51..401425e930 100644 --- a/src/js/redux/reducers/accountLanding/accountLandingReducer.js +++ b/src/js/redux/reducers/accountLanding/accountLandingReducer.js @@ -7,7 +7,8 @@ const initialState = { accountsOrder: { field: 'budget_authority_amount', direction: 'desc' - } + }, + pageNumber: 1 }; const accountLandingReducer = (state = initialState, action) => { @@ -19,6 +20,11 @@ const accountLandingReducer = (state = initialState, action) => { accountsOrder: order }); } + case 'SET_PAGE_NUMBER': { + return Object.assign({}, state, { + pageNumber: action.number + }); + } default: return state; } From b03f3d116b8245387109742ed1149667e0180551 Mon Sep 17 00:00:00 2001 From: Elizabeth Salita Date: Tue, 15 Aug 2017 16:45:01 -0400 Subject: [PATCH 09/89] Moved pagination logic to the container and added second pagination component below the results table --- .../accountLanding/AccountLandingContent.jsx | 11 +- .../AccountLandingPagination.jsx | 127 +---------------- .../AccountLandingContainer.jsx | 132 ++++++++++++++++-- 3 files changed, 132 insertions(+), 138 deletions(-) diff --git a/src/js/components/accountLanding/AccountLandingContent.jsx b/src/js/components/accountLanding/AccountLandingContent.jsx index b4ffa1f51c..4ded56f53e 100644 --- a/src/js/components/accountLanding/AccountLandingContent.jsx +++ b/src/js/components/accountLanding/AccountLandingContent.jsx @@ -13,12 +13,12 @@ import AccountLandingPagination from './AccountLandingPagination'; const propTypes = { resultsText: PropTypes.string, results: PropTypes.array, - items: PropTypes.array, accountSearchString: PropTypes.string, inFlight: PropTypes.bool, columns: PropTypes.array, setAccountSearchString: PropTypes.func, - onChangePage: PropTypes.func + onChangePage: PropTypes.func, + pager: PropTypes.object }; export default class AccountLandingContent extends React.Component { @@ -41,8 +41,8 @@ export default class AccountLandingContent extends React.Component {
    {this.props.resultsText} + onChangePage={this.props.onChangePage} + pager={this.props.pager} />
    +
  • ); diff --git a/src/js/components/accountLanding/AccountLandingPagination.jsx b/src/js/components/accountLanding/AccountLandingPagination.jsx index f741faf2e9..59f56363de 100644 --- a/src/js/components/accountLanding/AccountLandingPagination.jsx +++ b/src/js/components/accountLanding/AccountLandingPagination.jsx @@ -5,139 +5,19 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { range } from 'lodash'; const propTypes = { - items: PropTypes.array.isRequired, onChangePage: PropTypes.func.isRequired, - initialPage: PropTypes.number -}; - -const defaultProps = { - initialPage: 1 + pager: PropTypes.object }; export default class AccountLandingPagination extends React.Component { - constructor(props) { - super(props); - this.state = { pager: {} }; - } - - componentWillMount() { - // set page if items array isn't empty - if (this.props.items && this.props.items.length) { - this.setPage(this.props.initialPage); - } - } - - componentDidUpdate(prevProps) { - // reset page if items array has changed - if (this.props.items !== prevProps.items) { - this.setPage(this.props.initialPage); - } - } - setPage(page) { - const items = this.props.items; - let pager = this.state.pager; - - if (page < 1 || page > pager.totalPages) { - return; - } - - // get new pager object for specified page - // TODO - Lizzie: update to 50 when endpoint is ready - pager = this.getPager(items.length, page, 3); - - // get new page of items from items array - const pageOfItems = items.slice(pager.startIndex, pager.endIndex + 1); - - // update state - this.setState({ pager }); - - // call change page function in parent component - this.props.onChangePage(pageOfItems); - } - - getPager(totalItems, currentPage, pageSize) { - // calculate total pages - const totalPages = Math.ceil(totalItems / pageSize); - - let startPage; - let endPage; - let prevEllipses = (...); - let nextEllipses = (...); - let firstButton = ( -
  • - -
  • - ); - let lastButton = ( -
  • - -
  • - ); - if (totalPages < 5) { - // less than 5 total pages so show all - startPage = 1; - endPage = totalPages; - prevEllipses = ''; - nextEllipses = ''; - firstButton = ''; - lastButton = ''; - } - else { - if (currentPage === 1) { - startPage = currentPage; - endPage = currentPage + 2; - } - else if (currentPage === totalPages) { - startPage = currentPage - 2; - endPage = currentPage; - } - else { - startPage = currentPage - 1; - endPage = currentPage + 1; - } - - - if (currentPage < 4) { - prevEllipses = ''; - firstButton = ''; - } - else if (currentPage > (totalPages - 3)) { - nextEllipses = ''; - lastButton = ''; - } - } - - // calculate start and end item indexes - const startIndex = (currentPage - 1) * pageSize; - const endIndex = Math.min(startIndex + (pageSize - 1), (totalItems - 1)); - - // create an array of pages to repeat in the pager control - const pages = range(startPage, endPage + 1); - - // return object with all pager properties - return { - totalItems, - currentPage, - pageSize, - totalPages, - startPage, - endPage, - startIndex, - endIndex, - pages, - prevEllipses, - nextEllipses, - firstButton, - lastButton - }; + this.props.onChangePage(page); } render() { - const pager = this.state.pager; + const pager = this.props.pager; if (!pager.pages || pager.pages.length <= 1) { // don't display pager if there is only 1 page @@ -167,4 +47,3 @@ export default class AccountLandingPagination extends React.Component { } AccountLandingPagination.propTypes = propTypes; -AccountLandingPagination.defaultProps = defaultProps; diff --git a/src/js/containers/accountLanding/AccountLandingContainer.jsx b/src/js/containers/accountLanding/AccountLandingContainer.jsx index 5b045926c0..384b6a2131 100644 --- a/src/js/containers/accountLanding/AccountLandingContainer.jsx +++ b/src/js/containers/accountLanding/AccountLandingContainer.jsx @@ -7,18 +7,22 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; // import { isCancel } from 'axios'; +import { bindActionCreators } from 'redux'; import { Search } from 'js-search'; -import { orderBy } from 'lodash'; +import { range, orderBy } from 'lodash'; import AccountsTableFields from 'dataMapping/accountLanding/accountsTableFields'; // import * as AccountLandingHelper from 'helpers/accountLandingHelper'; import * as MoneyFormatter from 'helpers/moneyFormatter'; +import * as accountLandingActions from 'redux/actions/accountLanding/accountLandingActions'; import AccountLandingContent from 'components/accountLanding/AccountLandingContent'; const propTypes = { - accountsOrder: PropTypes.object + accountsOrder: PropTypes.object, + pageNumber: PropTypes.number, + setPageNumber: PropTypes.func }; export class AccountLandingContainer extends React.Component { @@ -32,7 +36,8 @@ export class AccountLandingContainer extends React.Component { accountSearchString: '', fullData: [], results: [], - pageOfItems: [] + pageOfItems: [], + pager: {} }; this.accountsRequest = null; @@ -49,6 +54,10 @@ export class AccountLandingContainer extends React.Component { // table sort changed this.performSearch(); } + if (this.props.pageNumber !== prevProps.pageNumber) { + // page number changed + this.setPageOfItems(); + } } componentWillUnmount() { @@ -57,9 +66,86 @@ export class AccountLandingContainer extends React.Component { } } - onChangePage(pageOfItems) { - // update state with new page of items - this.setState({ pageOfItems }); + onChangePage(pageNumber) { + // Change page number in Redux state + this.props.setPageNumber(pageNumber); + } + + getPager(totalItems, currentPage, pageSize) { + // calculate total pages + const totalPages = Math.ceil(totalItems / pageSize); + + let startPage; + let endPage; + let prevEllipses = (...); + let nextEllipses = (...); + let firstButton = ( +
  • + +
  • + ); + let lastButton = ( +
  • + +
  • + ); + if (totalPages < 5) { + // less than 5 total pages so show all + startPage = 1; + endPage = totalPages; + prevEllipses = ''; + nextEllipses = ''; + firstButton = ''; + lastButton = ''; + } + else { + if (currentPage === 1) { + startPage = currentPage; + endPage = currentPage + 2; + } + else if (currentPage === totalPages) { + startPage = currentPage - 2; + endPage = currentPage; + } + else { + startPage = currentPage - 1; + endPage = currentPage + 1; + } + + + if (currentPage < 4) { + prevEllipses = ''; + firstButton = ''; + } + else if (currentPage > (totalPages - 3)) { + nextEllipses = ''; + lastButton = ''; + } + } + + // calculate start and end item indexes + const startIndex = (currentPage - 1) * pageSize; + const endIndex = Math.min(startIndex + (pageSize - 1), (totalItems - 1)); + + // create an array of pages to repeat in the pager control + const pages = range(startPage, endPage + 1); + + // return object with all pager properties + return { + totalItems, + currentPage, + pageSize, + totalPages, + startPage, + endPage, + startIndex, + endIndex, + pages, + prevEllipses, + nextEllipses, + firstButton, + lastButton + }; } setAccountSearchString(accountSearchString) { @@ -75,6 +161,25 @@ export class AccountLandingContainer extends React.Component { }); } + setPageOfItems() { + const page = this.props.pageNumber; + let pager = this.state.pager; + const results = this.state.results; + + // Get new pager object for specified page + // TODO - Lizzie: update to 50 when endpoint is ready + pager = this.getPager(results.length, page, 3); + + // Get new page of items from results + const pageOfItems = results.slice(pager.startIndex, pager.endIndex + 1); + + // update state + this.setState({ + pager, + pageOfItems + }); + } + showColumns() { const columns = []; const sortOrder = AccountsTableFields.defaultSortDirection; @@ -364,8 +469,13 @@ export class AccountLandingContainer extends React.Component { const orderedResults = orderBy(results, [this.props.accountsOrder.field], [this.props.accountsOrder.direction]); + // Reset to page 1 + this.props.setPageNumber(1); + this.setState({ results: orderedResults + }, () => { + this.setPageOfItems(); }); } @@ -380,13 +490,13 @@ export class AccountLandingContainer extends React.Component { + onChangePage={this.onChangePage} + pager={this.state.pager} /> ); } } @@ -395,6 +505,8 @@ AccountLandingContainer.propTypes = propTypes; export default connect( (state) => ({ - accountsOrder: state.accountLanding.accountsOrder - }) + accountsOrder: state.accountLanding.accountsOrder, + pageNumber: state.accountLanding.pageNumber + }), + (dispatch) => bindActionCreators(accountLandingActions, dispatch) )(AccountLandingContainer); From 471ec36a5b49745b3b85345ab89293fd02321e07 Mon Sep 17 00:00:00 2001 From: Elizabeth Salita Date: Tue, 15 Aug 2017 16:51:23 -0400 Subject: [PATCH 10/89] Added hover effect --- src/_scss/components/_pagination.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/_scss/components/_pagination.scss b/src/_scss/components/_pagination.scss index a0d0465a68..d15837479a 100644 --- a/src/_scss/components/_pagination.scss +++ b/src/_scss/components/_pagination.scss @@ -10,6 +10,9 @@ font-style: normal; font-size: rem(12); margin: rem(5) rem(5) rem(5) 0; + &:hover { + background-color: $color-primary-alt-lightest; + } } &.active { button { From 59d3d58c1e5671bc49e612801a8b7b440004fdb3 Mon Sep 17 00:00:00 2001 From: Elizabeth Salita Date: Thu, 17 Aug 2017 13:15:50 -0400 Subject: [PATCH 11/89] Disabled out of range buttons --- src/_scss/components/_pagination.scss | 10 ++++++++++ .../accountLanding/AccountLandingPagination.jsx | 2 +- .../accountLanding/AccountLandingContainer.jsx | 10 ++++++---- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/_scss/components/_pagination.scss b/src/_scss/components/_pagination.scss index d15837479a..2efff53b24 100644 --- a/src/_scss/components/_pagination.scss +++ b/src/_scss/components/_pagination.scss @@ -21,6 +21,16 @@ border-color: $color-primary-alt-dark; } } + &.disabled { + button { + border-color: $color-gray-lighter; + color: $color-gray-light; + &:hover { + background-color: $color-white; + cursor: auto; + } + } + } } .pagination-ellipsis { font-weight: bold; diff --git a/src/js/components/accountLanding/AccountLandingPagination.jsx b/src/js/components/accountLanding/AccountLandingPagination.jsx index 59f56363de..6ab890ad27 100644 --- a/src/js/components/accountLanding/AccountLandingPagination.jsx +++ b/src/js/components/accountLanding/AccountLandingPagination.jsx @@ -13,7 +13,7 @@ const propTypes = { export default class AccountLandingPagination extends React.Component { setPage(page) { - this.props.onChangePage(page); + this.props.onChangePage(page, (page > 0 && page <= this.props.pager.totalPages)); } render() { diff --git a/src/js/containers/accountLanding/AccountLandingContainer.jsx b/src/js/containers/accountLanding/AccountLandingContainer.jsx index 384b6a2131..c4cc2ebc7f 100644 --- a/src/js/containers/accountLanding/AccountLandingContainer.jsx +++ b/src/js/containers/accountLanding/AccountLandingContainer.jsx @@ -66,9 +66,11 @@ export class AccountLandingContainer extends React.Component { } } - onChangePage(pageNumber) { + onChangePage(pageNumber, inRange) { // Change page number in Redux state - this.props.setPageNumber(pageNumber); + if (inRange) { + this.props.setPageNumber(pageNumber); + } } getPager(totalItems, currentPage, pageSize) { @@ -81,12 +83,12 @@ export class AccountLandingContainer extends React.Component { let nextEllipses = (...); let firstButton = (
  • - +
  • ); let lastButton = (
  • - +
  • ); if (totalPages < 5) { From 104c5020a70a5252cb3e725516065ad64036ce5e Mon Sep 17 00:00:00 2001 From: Elizabeth Salita Date: Tue, 22 Aug 2017 10:56:31 -0400 Subject: [PATCH 12/89] Customized column widths --- src/_scss/pages/accountLanding/table/table.scss | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/_scss/pages/accountLanding/table/table.scss b/src/_scss/pages/accountLanding/table/table.scss index bb97a2886a..f197b4afd8 100644 --- a/src/_scss/pages/accountLanding/table/table.scss +++ b/src/_scss/pages/accountLanding/table/table.scss @@ -18,7 +18,14 @@ border-bottom: solid 1px $color-gray-lighter; td { border: 0; + &:first-child { + width: 18%; + } + &:nth-child(2), &:nth-child(3) { + width: 31%; + } &:last-child { + width: 20%; .cell-content { float: right; } @@ -34,7 +41,14 @@ tr { td { border: 0; + &:first-child { + width: 18%; + } + &:nth-child(2), &:nth-child(3) { + width: 31%; + } &:last-child { + width: 20%; .cell-content { float: right; } From 4bc848785defd2d3a6eb8ee99d749bdeb7083e86 Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Tue, 30 Jan 2018 14:04:58 -0500 Subject: [PATCH 13/89] Converts Federal Account page's Sankey visualization to use a v2 endpoint that retrieves all values in a single call --- src/js/components/account/AccountOverview.jsx | 26 +++---- .../containers/account/AccountContainer.jsx | 76 +++++-------------- src/js/dataMapping/accounts/accountFields.js | 10 +++ src/js/helpers/accountHelper.js | 15 ++++ .../redux/reducers/account/accountReducer.js | 3 +- .../account/AccountContainer-test.jsx | 54 ++++--------- tests/containers/account/accountHelper.js | 16 +++- tests/containers/account/mockAccount.js | 33 +++++--- 8 files changed, 105 insertions(+), 128 deletions(-) diff --git a/src/js/components/account/AccountOverview.jsx b/src/js/components/account/AccountOverview.jsx index 83778348c2..03c01e853a 100644 --- a/src/js/components/account/AccountOverview.jsx +++ b/src/js/components/account/AccountOverview.jsx @@ -78,30 +78,28 @@ export default class AccountOverview extends React.Component { let fiscalYearAvailable = true; let authorityValue = 0; let obligatedValue = 0; - let bbfValue = 0; + let balanceBroughtForwardValue = 0; let otherValue = 0; let appropriationsValue = 0; - if ({}.hasOwnProperty.call(account.totals.budgetAuthority, fy)) { - authorityValue = account.totals.budgetAuthority[fy]; + if (account.totals.budgetAuthority) { + authorityValue = account.totals.budgetAuthority; } else { fiscalYearAvailable = false; } - if ({}.hasOwnProperty.call(account.totals.obligated, fy)) { - obligatedValue = account.totals.obligated[fy]; + if (account.totals.obligated) { + obligatedValue = account.totals.obligated; } else { fiscalYearAvailable = false; } if (fiscalYearAvailable) { - const firstBBF = parseFloat(account.totals.balanceBroughtForward1[fy]); - const secondBBF = parseFloat(account.totals.balanceBroughtForward2[fy]); - bbfValue = firstBBF + secondBBF; - otherValue = parseFloat(account.totals.otherBudgetaryResources[fy]); - appropriationsValue = parseFloat(account.totals.appropriations[fy]); + balanceBroughtForwardValue = (account.totals.balanceBroughtForward); + otherValue = parseFloat(account.totals.otherBudgetaryResources); + appropriationsValue = parseFloat(account.totals.appropriations); } const authUnits = MoneyFormatter.calculateUnitForSingleValue(authorityValue); @@ -117,8 +115,8 @@ ${authUnits.unitLabel}`; const amountObligated = `${MoneyFormatter.formatMoney(obligatedValue / obUnits.unit)}\ ${obUnits.unitLabel}`; - const bbfUnits = MoneyFormatter.calculateUnitForSingleValue(bbfValue); - const bbfString = `${MoneyFormatter.formatMoney(bbfValue / bbfUnits.unit)}\ + const bbfUnits = MoneyFormatter.calculateUnitForSingleValue(balanceBroughtForwardValue); + const bbfString = `${MoneyFormatter.formatMoney(balanceBroughtForwardValue / bbfUnits.unit)}\ ${bbfUnits.unitLabel}`; const appropUnits = MoneyFormatter.calculateUnitForSingleValue(appropriationsValue); @@ -142,10 +140,10 @@ ${authority} has been obligated.` budgetAuthority: authorityValue, out: { obligated: obligatedValue, - unobligated: parseFloat(account.totals.unobligated[fy]) + unobligated: parseFloat(account.totals.unobligated) }, in: { - bbf: bbfValue, + bbf: balanceBroughtForwardValue, other: otherValue, appropriations: appropriationsValue } diff --git a/src/js/containers/account/AccountContainer.jsx b/src/js/containers/account/AccountContainer.jsx index 1b4e96f476..f6f8bd418a 100644 --- a/src/js/containers/account/AccountContainer.jsx +++ b/src/js/containers/account/AccountContainer.jsx @@ -8,15 +8,13 @@ import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { isCancel } from 'axios'; -import moment from 'moment'; import * as AccountHelper from 'helpers/accountHelper'; -import * as FiscalYearHelper from 'helpers/fiscalYearHelper'; import * as accountActions from 'redux/actions/account/accountActions'; import * as filterActions from 'redux/actions/account/accountFilterActions'; import FederalAccount from 'models/account/FederalAccount'; -import { balanceFields } from 'dataMapping/accounts/accountFields'; +import { fiscalYearSnapshotFields } from 'dataMapping/accounts/accountFields'; import Account from 'components/account/Account'; import InvalidAccount from 'components/account/InvalidAccount'; @@ -46,7 +44,7 @@ export class AccountContainer extends React.Component { }; this.accountRequest = null; - this.balanceRequests = []; + this.fiscalYearSnapshotRequest = null; } componentDidMount() { @@ -78,7 +76,7 @@ export class AccountContainer extends React.Component { // update the redux store this.parseAccount(res.data); - this.loadBalances(); + this.loadFiscalYearSnapshot(id); this.setState({ validAccount: true @@ -104,80 +102,42 @@ export class AccountContainer extends React.Component { this.props.setSelectedAccount(account); } - loadBalances() { - if (this.balanceRequests.length > 0) { - // cancel all previous requests - this.balanceRequests.forEach((request) => { - request.cancel(); - }); - this.balanceRequests = []; + loadFiscalYearSnapshot(id) { + if (this.fiscalYearSnapshotRequest) { + this.fiscalYearSnapshotRequest.cancel(); } - const requests = []; - const promises = []; - Object.keys(balanceFields).forEach((balanceType) => { - // generate an API call - const request = AccountHelper.fetchTasBalanceTotals({ - group: 'reporting_period_start', - field: balanceFields[balanceType], - aggregate: 'sum', - order: ['reporting_period_start'], - filters: [ - { - field: 'treasury_account_identifier__federal_account_id', - operation: 'equals', - value: this.props.account.id - }, - { - field: ['reporting_period_start', 'reporting_period_end'], - operation: 'range_intersect', - value: FiscalYearHelper.defaultFiscalYear(), - value_format: 'fy' - } - ], - auditTrail: `Sankey - ${balanceType}` - }); - - request.type = balanceType; + this.fiscalYearSnapshotRequest = AccountHelper.fetchFederalAccountFYSnapshot(id); - requests.push(request); - promises.push(request.promise); - }); - - this.balanceRequests = requests; - - Promise.all(promises) + this.fiscalYearSnapshotRequest.promise .then((res) => { - this.parseBalances(res); + this.fiscalYearSnapshotRequest = null; + + // update the redux store + this.parseFYSnapshot(res.data); this.setState({ loading: false }); }) .catch((err) => { + this.fiscalYearSnapshotRequest = null; + if (!isCancel(err)) { this.setState({ loading: false }); + console.log(err); } }); } - parseBalances(data) { + parseFYSnapshot(data) { const balances = {}; - data.forEach((item, i) => { - const type = this.balanceRequests[i].type; - const values = {}; - - item.data.results.forEach((group) => { - const date = moment(group.item, 'YYYY-MM-DD'); - const fy = FiscalYearHelper.convertDateToFY(date); - values[fy] = group.aggregate; - }); - - balances[type] = values; + Object.keys(fiscalYearSnapshotFields).forEach((key) => { + balances[fiscalYearSnapshotFields[key]] = data.results[key]; }); // update the Redux account model with balances diff --git a/src/js/dataMapping/accounts/accountFields.js b/src/js/dataMapping/accounts/accountFields.js index 20e33f06d3..f81a2c35c2 100644 --- a/src/js/dataMapping/accounts/accountFields.js +++ b/src/js/dataMapping/accounts/accountFields.js @@ -29,3 +29,13 @@ export const balanceFieldsNonfiltered = { budgetAuthority: 'budget_authority_available_amount_total_cpe', unobligated: 'unobligated_balance_cpe' }; + +export const fiscalYearSnapshotFields = { + outlay: 'outlay', + budget_authority: 'budgetAuthority', + obligated: 'obligated', + unobligated: 'unobligated', + balance_brought_forward: 'balanceBroughtForward', + other_budgetary_resources: 'otherBudgetaryResources', + appropriations: 'appropriations' +}; diff --git a/src/js/helpers/accountHelper.js b/src/js/helpers/accountHelper.js index acfccc308c..4612684966 100644 --- a/src/js/helpers/accountHelper.js +++ b/src/js/helpers/accountHelper.js @@ -22,6 +22,21 @@ export const fetchFederalAccount = (id) => { }; }; +export const fetchFederalAccountFYSnapshot = (id) => { + const source = CancelToken.source(); + return { + promise: Axios.request({ + url: `v2/federal_accounts/${id}/fiscal_year_snapshot`, + baseURL: kGlobalConstants.API, + method: 'get', + cancelToken: source.token + }), + cancel() { + source.cancel(); + } + }; +}; + export const fetchTasCategoryTotals = (data) => { const source = CancelToken.source(); return { diff --git a/src/js/redux/reducers/account/accountReducer.js b/src/js/redux/reducers/account/accountReducer.js index 65f2ff5784..2c883e9f47 100644 --- a/src/js/redux/reducers/account/accountReducer.js +++ b/src/js/redux/reducers/account/accountReducer.js @@ -36,8 +36,7 @@ export const initialState = { unobligated: {}, budgetAuthority: {}, outlay: {}, - balanceBroughtForward1: {}, - balanceBroughtForward2: {}, + balanceBroughtForward: {}, otherBudgetaryResources: {}, appropriations: {} } diff --git a/tests/containers/account/AccountContainer-test.jsx b/tests/containers/account/AccountContainer-test.jsx index 72d8403a96..8cf2bcd7cc 100644 --- a/tests/containers/account/AccountContainer-test.jsx +++ b/tests/containers/account/AccountContainer-test.jsx @@ -10,14 +10,14 @@ import sinon from 'sinon'; import { AccountContainer } from 'containers/account/AccountContainer'; import FederalAccount from 'models/account/FederalAccount'; -import { mockAccount, mockBalances, mockReduxAccount } from './mockAccount'; +import { mockAccount, mockBalances, mockReduxAccount, mockSnapshot } from './mockAccount'; jest.mock('helpers/accountHelper', () => require('./accountHelper')); jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; // spy on specific functions inside the component const loadAccountSpy = sinon.spy(AccountContainer.prototype, 'loadData'); -const loadBalancesSpy = sinon.spy(AccountContainer.prototype, 'loadBalances'); +const loadFiscalYearSnapshotSpy = sinon.spy(AccountContainer.prototype, 'loadFiscalYearSnapshot'); const parameters = { accountId: 2507 @@ -53,13 +53,13 @@ describe('AccountContainer', () => { account={mockRedux} />); await container.instance().accountRequest.promise; - await container.instance().balanceRequests.promise; + await container.instance().fiscalYearSnapshotRequest.promise; expect(loadAccountSpy.callCount).toEqual(1); - expect(loadBalancesSpy.callCount).toEqual(1); + expect(loadFiscalYearSnapshotSpy.callCount).toEqual(1); loadAccountSpy.reset(); - loadBalancesSpy.reset(); + loadFiscalYearSnapshotSpy.reset(); }); it('should make an API call when the award ID parameter changes', async () => { @@ -73,10 +73,10 @@ describe('AccountContainer', () => { account={mockRedux} />); await container.instance().accountRequest.promise; - await container.instance().balanceRequests.promise; + await container.instance().fiscalYearSnapshotRequest.promise; expect(loadAccountSpy.callCount).toEqual(1); - expect(loadBalancesSpy.callCount).toEqual(1); + expect(loadFiscalYearSnapshotSpy.callCount).toEqual(1); container.setProps({ params: { @@ -85,13 +85,13 @@ describe('AccountContainer', () => { }); await container.instance().accountRequest.promise; - await container.instance().balanceRequests.promise; + await container.instance().fiscalYearSnapshotRequest.promise; expect(loadAccountSpy.callCount).toEqual(2); - expect(loadBalancesSpy.callCount).toEqual(2); + expect(loadFiscalYearSnapshotSpy.callCount).toEqual(2); loadAccountSpy.reset(); - loadBalancesSpy.reset(); + loadFiscalYearSnapshotSpy.reset(); }); describe('parseAccount', () => { @@ -112,8 +112,8 @@ describe('AccountContainer', () => { }); }); - describe('parseBalances', () => { - it('should parse the returned balances and add them to the Redux account object', (done) => { + describe('parseFYSnapshot', () => { + it('should parse the returned fiscal year snapshot and add the data to the Redux account object', (done) => { const initialModel = new FederalAccount(mockAccount); delete initialModel._jsid; initialModel.totals = { @@ -135,35 +135,7 @@ describe('AccountContainer', () => { setSelectedAccount={reduxAction} account={initialModel} />); - container.instance().balanceRequests = [ - { - type: 'outlay' - }, - { - type: 'budgetAuthority' - }, - { - type: 'obligated' - }, - { - type: 'unobligated' - } - ]; - - container.instance().parseBalances([ - { - data: mockBalances.outlay - }, - { - data: mockBalances.budgetAuthority - }, - { - data: mockBalances.obligated - }, - { - data: mockBalances.unobligated - } - ]); + container.instance().parseFYSnapshot(mockSnapshot); }); }); }); diff --git a/tests/containers/account/accountHelper.js b/tests/containers/account/accountHelper.js index 70b9905f50..46790d371b 100644 --- a/tests/containers/account/accountHelper.js +++ b/tests/containers/account/accountHelper.js @@ -1,6 +1,6 @@ import { mockAccountProgramActivities } from './filters/mockAccountProgramActivities'; import { mockAvailableOC } from './filters/mockObjectClass'; -import { mockAccount, mockBalances } from './mockAccount'; +import { mockAccount, mockBalances, mockSnapshot } from './mockAccount'; // Fetch Program Activities export const fetchProgramActivities = () => ( @@ -30,6 +30,20 @@ export const fetchFederalAccount = () => ( } ); +// Fetch Federal Account Fiscal Year Snapshot +export const fetchFederalAccountFYSnapshot = () => ( + { + promise: new Promise((resolve) => { + process.nextTick(() => { + resolve({ + data: mockSnapshot + }); + }); + }), + cancel: jest.fn() + } +); + // Fetch TAS Balances export const fetchTasBalanceTotals = () => ( { diff --git a/tests/containers/account/mockAccount.js b/tests/containers/account/mockAccount.js index 201754f4ee..6e177e1c35 100644 --- a/tests/containers/account/mockAccount.js +++ b/tests/containers/account/mockAccount.js @@ -17,18 +17,13 @@ export const mockReduxAccount = { main_account_code: '0208', description: 'Not available', totals: { - outlay: { - 2016: '-5505246.42' - }, - budgetAuthority: { - 2016: '201404661.47' - }, - obligated: { - 2016: '2696684.86' - }, - unobligated: { - 2016: '198707976.61' - } + appropriations: 0, + balanceBroughtForward: 49394224538.76, + budgetAuthority: 84734289679.5, + obligated: 39762255686.2, + otherBudgetaryResources: 35340065140.74, + outlay: 48474446887.76, + unobligated: 44972033993.3 } }; @@ -751,3 +746,17 @@ export const parsedQuarterYSeriesFiltered = [ } } ]; + +export const mockSnapshot = { + results: { + outlay: 48474446887.76, + name: "Federal-Aid Highways, Liquidation of Contract Authorization, " + + "Federal Highway Administration, Transportation", + unobligated: 44972033993.3, + appropriations: 0.0, + balance_brought_forward: 49394224538.76, + budget_authority: 84734289679.5, + obligated: 39762255686.2, + other_budgetary_resources: 35340065140.74 + } +}; From 9693b7f6580aacc88aa37a6c447559a6533bc6aa Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Tue, 30 Jan 2018 17:53:07 -0500 Subject: [PATCH 14/89] Removed account landing from Redux --- .../accountLanding/AccountLandingContent.jsx | 8 +- .../AccountLandingPagination.jsx | 49 ------------ .../AccountLandingResultsSection.jsx | 4 +- .../table/AccountLandingTable.jsx | 23 +++++- .../accountLanding/table/HeaderRow.jsx | 34 --------- .../AccountLandingContainer.jsx | 75 ++++++++----------- .../AccountLandingHeaderCellContainer.jsx | 64 ---------------- .../accountLanding/accountLandingActions.js | 14 ---- .../accountLanding/accountLandingReducer.js | 33 -------- src/js/redux/reducers/index.js | 4 +- 10 files changed, 62 insertions(+), 246 deletions(-) delete mode 100644 src/js/components/accountLanding/AccountLandingPagination.jsx delete mode 100644 src/js/components/accountLanding/table/HeaderRow.jsx delete mode 100644 src/js/containers/accountLanding/table/AccountLandingHeaderCellContainer.jsx delete mode 100644 src/js/redux/actions/accountLanding/accountLandingActions.js delete mode 100644 src/js/redux/reducers/accountLanding/accountLandingReducer.js diff --git a/src/js/components/accountLanding/AccountLandingContent.jsx b/src/js/components/accountLanding/AccountLandingContent.jsx index ff0dc8d7f4..cfe98532f1 100644 --- a/src/js/components/accountLanding/AccountLandingContent.jsx +++ b/src/js/components/accountLanding/AccountLandingContent.jsx @@ -19,7 +19,9 @@ const propTypes = { onChangePage: PropTypes.func, pageNumber: PropTypes.number, totalItems: PropTypes.number, - pageSize: PropTypes.number + pageSize: PropTypes.number, + order: PropTypes.object, + updateSort: PropTypes.func }; export default class AccountLandingContent extends React.Component { @@ -51,7 +53,9 @@ export default class AccountLandingContent extends React.Component { columns={this.props.columns} results={this.props.results} inFlight={this.props.inFlight} - accountSearchString={this.props.accountSearchString} /> + accountSearchString={this.props.accountSearchString} + order={this.props.order} + updateSort={this.props.updateSort} /> 0 && page <= this.props.pager.totalPages)); - } - - render() { - const pager = this.props.pager; - - if (!pager.pages || pager.pages.length <= 1) { - // don't display pager if there is only 1 page - return null; - } - - return ( -
      -
    • - -
    • - {pager.firstButton} - {pager.prevEllipses} - {pager.pages.map((page, index) => ( -
    • - -
    • - ))} - {pager.nextEllipses} - {pager.lastButton} -
    • - -
    • -
    - ); - } -} - -AccountLandingPagination.propTypes = propTypes; diff --git a/src/js/components/accountLanding/AccountLandingResultsSection.jsx b/src/js/components/accountLanding/AccountLandingResultsSection.jsx index 44d516d6c9..b997a04dac 100644 --- a/src/js/components/accountLanding/AccountLandingResultsSection.jsx +++ b/src/js/components/accountLanding/AccountLandingResultsSection.jsx @@ -13,7 +13,9 @@ const propTypes = { inFlight: PropTypes.bool, results: PropTypes.array, columns: PropTypes.array, - accountSearchString: PropTypes.string + accountSearchString: PropTypes.string, + order: PropTypes.object, + updateSort: PropTypes.func }; export default class AccountLandingResultsSection extends React.Component { diff --git a/src/js/components/accountLanding/table/AccountLandingTable.jsx b/src/js/components/accountLanding/table/AccountLandingTable.jsx index 26815fe891..84fbf524d6 100644 --- a/src/js/components/accountLanding/table/AccountLandingTable.jsx +++ b/src/js/components/accountLanding/table/AccountLandingTable.jsx @@ -6,13 +6,15 @@ import React from 'react'; import PropTypes from 'prop-types'; -import HeaderRow from './HeaderRow'; +import LegacyTableHeaderCell from 'components/account/awards/LegacyTableHeaderCell'; import TableRow from './TableRow'; const propTypes = { results: PropTypes.array, columns: PropTypes.array, - accountSearchString: PropTypes.string + accountSearchString: PropTypes.string, + order: PropTypes.object, + updateSort: PropTypes.func }; export default class AccountLandingTable extends React.PureComponent { @@ -33,12 +35,25 @@ export default class AccountLandingTable extends React.PureComponent { accountSearchString={this.props.accountSearchString} /> )); + const headers = this.props.columns.map((column, index) => ( + + + + )); + return (
    - + + {headers} + {rows} diff --git a/src/js/components/accountLanding/table/HeaderRow.jsx b/src/js/components/accountLanding/table/HeaderRow.jsx deleted file mode 100644 index 359bae34c7..0000000000 --- a/src/js/components/accountLanding/table/HeaderRow.jsx +++ /dev/null @@ -1,34 +0,0 @@ -/** - * HeaderRow.jsx - * Created by Lizzie Salita 8/4/17 - **/ - -import React from 'react'; -import PropTypes from 'prop-types'; - -import AccountLandingHeaderCellContainer from - 'containers/accountLanding/table/AccountLandingHeaderCellContainer'; - -const propTypes = { - columns: PropTypes.array -}; - -export default class HeaderRow extends React.Component { - render() { - const headers = this.props.columns.map((column, i) => ( - - )); - - return ( - - {headers} - - ); - } -} - -HeaderRow.propTypes = propTypes; diff --git a/src/js/containers/accountLanding/AccountLandingContainer.jsx b/src/js/containers/accountLanding/AccountLandingContainer.jsx index 2dc05a8b2d..509132a620 100644 --- a/src/js/containers/accountLanding/AccountLandingContainer.jsx +++ b/src/js/containers/accountLanding/AccountLandingContainer.jsx @@ -4,10 +4,7 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; // import { isCancel } from 'axios'; -import { bindActionCreators } from 'redux'; import { Search } from 'js-search'; import { orderBy } from 'lodash'; @@ -15,23 +12,21 @@ import { orderBy } from 'lodash'; import AccountsTableFields from 'dataMapping/accountLanding/accountsTableFields'; // import * as AccountLandingHelper from 'helpers/accountLandingHelper'; import * as MoneyFormatter from 'helpers/moneyFormatter'; -import * as accountLandingActions from 'redux/actions/accountLanding/accountLandingActions'; import AccountLandingContent from 'components/accountLanding/AccountLandingContent'; require('pages/accountLanding/accountLandingPage.scss'); -const propTypes = { - accountsOrder: PropTypes.object, - pageNumber: PropTypes.number, - setPageNumber: PropTypes.func -}; - -export class AccountLandingContainer extends React.Component { +export default class AccountLandingContainer extends React.Component { constructor(props) { super(props); this.state = { + pageNumber: 1, + order: { + field: 'budget_authority_amount', + direction: 'desc' + }, columns: [], inFlight: false, currentFY: '', @@ -46,23 +41,13 @@ export class AccountLandingContainer extends React.Component { this.accountsRequest = null; this.setAccountSearchString = this.setAccountSearchString.bind(this); this.onChangePage = this.onChangePage.bind(this); + this.updateSort = this.updateSort.bind(this); } componentDidMount() { this.showColumns(); } - componentDidUpdate(prevProps) { - if (this.props.accountsOrder !== prevProps.accountsOrder) { - // table sort changed - this.performSearch(); - } - if (this.props.pageNumber !== prevProps.pageNumber) { - // page number changed - this.setPageOfItems(); - } - } - componentWillUnmount() { if (this.accountsRequest) { this.accountsRequest.cancel(); @@ -70,13 +55,15 @@ export class AccountLandingContainer extends React.Component { } onChangePage(pageNumber) { - console.log(pageNumber); // Change page number in Redux state const totalPages = Math.ceil(this.state.fullData.length / this.state.pageSize); const inRange = (pageNumber > 0) && (pageNumber <= totalPages); if (inRange) { - console.log('inRange'); - this.props.setPageNumber(pageNumber); + this.setState({ + pageNumber + }, () => { + this.setPageOfItems(); + }); } } @@ -96,7 +83,7 @@ export class AccountLandingContainer extends React.Component { setPageOfItems() { const results = this.state.fullData; // calculate start and end item indexes - const startIndex = (this.props.pageNumber - 1) * this.state.pageSize; + const startIndex = (this.state.pageNumber - 1) * this.state.pageSize; const endIndex = Math.min(startIndex + (this.state.pageSize - 1), (results.length - 1)); // Get new page of items from results @@ -106,6 +93,17 @@ export class AccountLandingContainer extends React.Component { }); } + updateSort(field, direction) { + this.setState({ + order: { + field, + direction + } + }, () => { + this.performSearch(); + }); + } + showColumns() { const columns = []; const sortOrder = AccountsTableFields.defaultSortDirection; @@ -313,8 +311,8 @@ export class AccountLandingContainer extends React.Component { // // // generate the params // const params = { - // sort: this.props.accountsOrder.field, - // order: this.props.accountsOrder.direction + // sort: this.state.order.field, + // order: this.state.order.direction // }; // // this.accountsRequest = AccountLandingHelper.fetchAllAccounts(params); @@ -393,10 +391,12 @@ export class AccountLandingContainer extends React.Component { // now sort the results by the appropriate table column and direction const orderedResults = orderBy(results, - [this.props.accountsOrder.field], [this.props.accountsOrder.direction]); + [this.state.order.field], [this.state.order.direction]); // Reset to page 1 - this.props.setPageNumber(1); + this.setState({ + pageNumber: 1 + }); this.setState({ results: orderedResults @@ -412,22 +412,13 @@ export class AccountLandingContainer extends React.Component { accountSearchString={this.state.accountSearchString} inFlight={this.state.inFlight} columns={this.state.columns} - sort={this.props.accountsOrder} + order={this.state.order} + updateSort={this.updateSort} setAccountSearchString={this.setAccountSearchString} onChangePage={this.onChangePage} - pageNumber={this.props.pageNumber} + pageNumber={this.state.pageNumber} totalItems={this.state.fullData.length} pageSize={this.state.pageSize} /> ); } } - -AccountLandingContainer.propTypes = propTypes; - -export default connect( - (state) => ({ - accountsOrder: state.accountLanding.accountsOrder, - pageNumber: state.accountLanding.pageNumber - }), - (dispatch) => bindActionCreators(accountLandingActions, dispatch) -)(AccountLandingContainer); diff --git a/src/js/containers/accountLanding/table/AccountLandingHeaderCellContainer.jsx b/src/js/containers/accountLanding/table/AccountLandingHeaderCellContainer.jsx deleted file mode 100644 index 83ee9bc676..0000000000 --- a/src/js/containers/accountLanding/table/AccountLandingHeaderCellContainer.jsx +++ /dev/null @@ -1,64 +0,0 @@ -/** - * AccountLandingHeaderCellContainer.jsx - * Created by Lizzie Salita 8/4/17 - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; - -// just import the relevant action(s) -import { setAccountsOrder } from 'redux/actions/accountLanding/accountLandingActions'; - -import ResultsTableHeaderCell from 'components/search/table/cells/ResultsTableHeaderCell'; - -// combine the action functions into an object for the react-redux bindings -const actions = { - setAccountsOrder -}; - -const propTypes = { - setAccountsOrder: PropTypes.func, - order: PropTypes.object, - displayName: PropTypes.string, - defaultDirection: PropTypes.string, - columnName: PropTypes.string, - isLast: PropTypes.bool -}; - -class AccountLandingHeaderCellContainer extends React.Component { - constructor(props) { - super(props); - - this.setAccountsOrder = this.setAccountsOrder.bind(this); - } - - setAccountsOrder(field, direction) { - this.props.setAccountsOrder({ - field, - direction - }); - } - - render() { - return ( - - ); - } -} - -AccountLandingHeaderCellContainer.propTypes = propTypes; - -export default connect( - (state) => ({ - order: state.accountLanding.accountsOrder - }), - (dispatch) => bindActionCreators(actions, dispatch) -)(AccountLandingHeaderCellContainer); diff --git a/src/js/redux/actions/accountLanding/accountLandingActions.js b/src/js/redux/actions/accountLanding/accountLandingActions.js deleted file mode 100644 index b1cfacdf5d..0000000000 --- a/src/js/redux/actions/accountLanding/accountLandingActions.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * accountLandingActions.js - * Created by Lizzie Salita 8/4/17 - */ - -export const setAccountsOrder = (state) => ({ - type: 'SET_ACCOUNTS_ORDER', - order: state -}); - -export const setPageNumber = (state) => ({ - type: 'SET_PAGE_NUMBER', - number: state -}); diff --git a/src/js/redux/reducers/accountLanding/accountLandingReducer.js b/src/js/redux/reducers/accountLanding/accountLandingReducer.js deleted file mode 100644 index 401425e930..0000000000 --- a/src/js/redux/reducers/accountLanding/accountLandingReducer.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * accountLandingReducer.js - * Created by Lizzie Salita 8/4/17 - */ - -const initialState = { - accountsOrder: { - field: 'budget_authority_amount', - direction: 'desc' - }, - pageNumber: 1 -}; - -const accountLandingReducer = (state = initialState, action) => { - switch (action.type) { - case 'SET_ACCOUNTS_ORDER': { - const order = Object.assign({}, state.accountsOrder, action.order); - - return Object.assign({}, state, { - accountsOrder: order - }); - } - case 'SET_PAGE_NUMBER': { - return Object.assign({}, state, { - pageNumber: action.number - }); - } - default: - return state; - } -}; - -export default accountLandingReducer; diff --git a/src/js/redux/reducers/index.js b/src/js/redux/reducers/index.js index 595496e0e0..ae587715f9 100644 --- a/src/js/redux/reducers/index.js +++ b/src/js/redux/reducers/index.js @@ -16,7 +16,6 @@ import glossaryReducer from './glossary/glossaryReducer'; import agencyLandingReducer from './agencyLanding/agencyLandingReducer'; import downloadReducer from './search/downloadReducer'; import bulkDownloadReducer from './bulkDownload/bulkDownloadReducer'; -import accountLandingReducer from './accountLanding/accountLandingReducer'; const appReducer = combineReducers({ filters: filtersReducer, @@ -29,8 +28,7 @@ const appReducer = combineReducers({ glossary: glossaryReducer, agencyLanding: agencyLandingReducer, explorer: explorerReducer, - bulkDownload: bulkDownloadReducer, - accountLanding: accountLandingReducer + bulkDownload: bulkDownloadReducer }); export default appReducer; From 03cc939cd9f5a27ce49fa5f69948815820dc96ab Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Wed, 31 Jan 2018 12:28:57 -0500 Subject: [PATCH 15/89] Handling the special case of not showing Q1 data until a month after the Q1 reporting period ends by adding a Fiscal Year parameter to the Fiscal Year Snapshot API request --- src/js/containers/account/AccountContainer.jsx | 8 +++++++- src/js/helpers/accountHelper.js | 4 ++-- src/js/redux/reducers/account/accountReducer.js | 14 +++++++------- tests/containers/account/AccountContainer-test.jsx | 13 ++++++++----- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/js/containers/account/AccountContainer.jsx b/src/js/containers/account/AccountContainer.jsx index f6f8bd418a..c81f5c71ba 100644 --- a/src/js/containers/account/AccountContainer.jsx +++ b/src/js/containers/account/AccountContainer.jsx @@ -10,6 +10,7 @@ import { connect } from 'react-redux'; import { isCancel } from 'axios'; import * as AccountHelper from 'helpers/accountHelper'; +import * as FiscalYearHelper from 'helpers/fiscalYearHelper'; import * as accountActions from 'redux/actions/account/accountActions'; import * as filterActions from 'redux/actions/account/accountFilterActions'; @@ -107,7 +108,12 @@ export class AccountContainer extends React.Component { this.fiscalYearSnapshotRequest.cancel(); } - this.fiscalYearSnapshotRequest = AccountHelper.fetchFederalAccountFYSnapshot(id); + const currentFiscalYear = FiscalYearHelper.defaultFiscalYear(); + + this.fiscalYearSnapshotRequest = AccountHelper.fetchFederalAccountFYSnapshot( + id, + currentFiscalYear + ); this.fiscalYearSnapshotRequest.promise .then((res) => { diff --git a/src/js/helpers/accountHelper.js b/src/js/helpers/accountHelper.js index 4612684966..fb7f182d5d 100644 --- a/src/js/helpers/accountHelper.js +++ b/src/js/helpers/accountHelper.js @@ -22,11 +22,11 @@ export const fetchFederalAccount = (id) => { }; }; -export const fetchFederalAccountFYSnapshot = (id) => { +export const fetchFederalAccountFYSnapshot = (id, fy) => { const source = CancelToken.source(); return { promise: Axios.request({ - url: `v2/federal_accounts/${id}/fiscal_year_snapshot`, + url: `v2/federal_accounts/${id}/fiscal_year_snapshot/${fy}`, baseURL: kGlobalConstants.API, method: 'get', cancelToken: source.token diff --git a/src/js/redux/reducers/account/accountReducer.js b/src/js/redux/reducers/account/accountReducer.js index 2c883e9f47..f4a8d86cd2 100644 --- a/src/js/redux/reducers/account/accountReducer.js +++ b/src/js/redux/reducers/account/accountReducer.js @@ -32,13 +32,13 @@ export const initialState = { title: '', description: '', totals: { - obligated: {}, - unobligated: {}, - budgetAuthority: {}, - outlay: {}, - balanceBroughtForward: {}, - otherBudgetaryResources: {}, - appropriations: {} + obligated: 0, + unobligated: 0, + budgetAuthority: 0, + outlay: 0, + balanceBroughtForward: 0, + otherBudgetaryResources: 0, + appropriations: 0 } }, totalSpending: 0 diff --git a/tests/containers/account/AccountContainer-test.jsx b/tests/containers/account/AccountContainer-test.jsx index 8cf2bcd7cc..bdf1613cb4 100644 --- a/tests/containers/account/AccountContainer-test.jsx +++ b/tests/containers/account/AccountContainer-test.jsx @@ -10,7 +10,7 @@ import sinon from 'sinon'; import { AccountContainer } from 'containers/account/AccountContainer'; import FederalAccount from 'models/account/FederalAccount'; -import { mockAccount, mockBalances, mockReduxAccount, mockSnapshot } from './mockAccount'; +import { mockAccount, mockReduxAccount, mockSnapshot } from './mockAccount'; jest.mock('helpers/accountHelper', () => require('./accountHelper')); jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; @@ -117,10 +117,13 @@ describe('AccountContainer', () => { const initialModel = new FederalAccount(mockAccount); delete initialModel._jsid; initialModel.totals = { - outlay: {}, - obligated: {}, - unobligated: {}, - budgetAuthority: {} + obligated: 0, + unobligated: 0, + budgetAuthority: 0, + outlay: 0, + balanceBroughtForward: 0, + otherBudgetaryResources: 0, + appropriations: 0 }; const reduxAction = jest.fn((args) => { From 0f47e64116a5c81478f444c2969ace0495f7c862 Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Thu, 1 Feb 2018 12:49:33 -0500 Subject: [PATCH 16/89] Update mock API --- .../AccountLandingContainer.jsx | 365 ++++-------------- .../accountLanding/accountsTableFields.js | 6 +- src/js/helpers/accountLandingHelper.js | 131 ++++++- 3 files changed, 187 insertions(+), 315 deletions(-) diff --git a/src/js/containers/accountLanding/AccountLandingContainer.jsx b/src/js/containers/accountLanding/AccountLandingContainer.jsx index 509132a620..1d03c98e09 100644 --- a/src/js/containers/accountLanding/AccountLandingContainer.jsx +++ b/src/js/containers/accountLanding/AccountLandingContainer.jsx @@ -4,14 +4,12 @@ */ import React from 'react'; -// import { isCancel } from 'axios'; - -import { Search } from 'js-search'; -import { orderBy } from 'lodash'; +import { isCancel } from 'axios'; import AccountsTableFields from 'dataMapping/accountLanding/accountsTableFields'; -// import * as AccountLandingHelper from 'helpers/accountLandingHelper'; +import * as AccountLandingHelper from 'helpers/accountLandingHelper'; import * as MoneyFormatter from 'helpers/moneyFormatter'; +import * as FiscalYearHelper from 'helpers/fiscalYearHelper'; import AccountLandingContent from 'components/accountLanding/AccountLandingContent'; @@ -24,18 +22,16 @@ export default class AccountLandingContainer extends React.Component { this.state = { pageNumber: 1, order: { - field: 'budget_authority_amount', + field: 'budgetary_resources', direction: 'desc' }, columns: [], inFlight: false, - currentFY: '', - accountSearchString: '', - fullData: [], + searchString: '', results: [], - pageOfItems: [], - // TODO - Lizzie: update page size - pageSize: 3 + totalItems: 0, + // TODO - Lizzie: update to 50 + pageSize: 10 }; this.accountsRequest = null; @@ -55,52 +51,34 @@ export default class AccountLandingContainer extends React.Component { } onChangePage(pageNumber) { - // Change page number in Redux state - const totalPages = Math.ceil(this.state.fullData.length / this.state.pageSize); - const inRange = (pageNumber > 0) && (pageNumber <= totalPages); - if (inRange) { - this.setState({ - pageNumber - }, () => { - this.setPageOfItems(); - }); - } - } - - setAccountSearchString(accountSearchString) { - let searchValue = ''; - if (accountSearchString.length > 2) { - searchValue = accountSearchString; - } - + // Change page number in the state and make a new request this.setState({ - accountSearchString: searchValue + pageNumber }, () => { - this.performSearch(); + this.fetchAccounts(); }); } - setPageOfItems() { - const results = this.state.fullData; - // calculate start and end item indexes - const startIndex = (this.state.pageNumber - 1) * this.state.pageSize; - const endIndex = Math.min(startIndex + (this.state.pageSize - 1), (results.length - 1)); - - // Get new page of items from results - const pageOfItems = results.slice(startIndex, endIndex + 1); - this.setState({ - pageOfItems - }); + setAccountSearchString(searchString) { + // Change search string in the state and make a new request + if (searchString.length > 2) { + this.setState({ + searchString + }, () => { + this.fetchAccounts(); + }); + } } updateSort(field, direction) { + // Change sort in the state and make a new request this.setState({ order: { field, direction } }, () => { - this.performSearch(); + this.fetchAccounts(); }); } @@ -110,12 +88,10 @@ export default class AccountLandingContainer extends React.Component { AccountsTableFields.order.forEach((col) => { let displayName = AccountsTableFields[col]; - if ((col === 'budget_authority_amount') || - (col === 'percentage_of_total_budget_authority')) { - // Add (FY YYYY) to Budget Authority and Percent of Total U.S. Budget column headers - if (this.state.fy) { - displayName = `${displayName} (FY ${this.state.currentFY})`; - } + if (col === 'budgetary_resources') { + // Add default fiscal year to Budgetary Resources column header + const fy = FiscalYearHelper.defaultFiscalYear(); + displayName = `${fy} ${displayName}`; } const column = { columnName: col, @@ -133,283 +109,78 @@ export default class AccountLandingContainer extends React.Component { } fetchAccounts() { - const mockData = { - results: [ - { - account_id: 1, - account_number: '123-4567', - account_name: 'Mock Account', - managing_agency: 'Mock Agency', - managing_agency_acronym: 'XYZ', - budget_authority_amount: 5000000 - }, - { - account_id: 2, - account_number: '098-7654', - account_name: 'Mock Account 2', - managing_agency: 'Mock Agency 2', - managing_agency_acronym: 'ABC', - budget_authority_amount: 6500000 - }, - { - account_id: 3, - account_number: '234-5678', - account_name: 'Test Account', - managing_agency: 'Mock Agency 3', - managing_agency_acronym: 'DEF', - budget_authority_amount: 4500000 - }, - { - account_id: 4, - account_number: '123-4567', - account_name: 'Mock Account 4', - managing_agency: 'Mock Agency 4', - managing_agency_acronym: 'XYZ', - budget_authority_amount: 5500000 - }, - { - account_id: 5, - account_number: '098-7654', - account_name: 'Mock Account 5', - managing_agency: 'Mock Agency 5', - managing_agency_acronym: 'ABC', - budget_authority_amount: 6000000 - }, - { - account_id: 6, - account_number: '234-5678', - account_name: 'Test Account 2', - managing_agency: 'Mock Agency 6', - managing_agency_acronym: 'DEF', - budget_authority_amount: 4000000 - }, - { - account_id: 7, - account_number: '123-4567', - account_name: 'Mock Account 7', - managing_agency: 'Mock Agency 7', - managing_agency_acronym: 'XYZ', - budget_authority_amount: 5000000 - }, - { - account_id: 8, - account_number: '098-7654', - account_name: 'Mock Account 8', - managing_agency: 'Mock Agency 8', - managing_agency_acronym: 'ABC', - budget_authority_amount: 6500000 - }, - { - account_id: 9, - account_number: '234-5678', - account_name: 'Test Account 3', - managing_agency: 'Mock Agency 9', - managing_agency_acronym: 'DEF', - budget_authority_amount: 4500000 - }, - { - account_id: 10, - account_number: '123-4567', - account_name: 'Mock Account 10', - managing_agency: 'Mock Agency 6', - managing_agency_acronym: 'XYZ', - budget_authority_amount: 5500000 - }, - { - account_id: 11, - account_number: '098-7654', - account_name: 'Mock Account 11', - managing_agency: 'Mock Agency', - managing_agency_acronym: 'XYZ', - budget_authority_amount: 6000000 - }, - { - account_id: 12, - account_number: '234-5678', - account_name: 'Test Account 4', - managing_agency: 'Mock Agency 12', - managing_agency_acronym: 'DEF', - budget_authority_amount: 4000000 - }, - { - account_id: 13, - account_number: '123-4567', - account_name: 'Mock Account 13', - managing_agency: 'Mock Agency', - managing_agency_acronym: 'XYZ', - budget_authority_amount: 5000000 - }, - { - account_id: 14, - account_number: '098-7654', - account_name: 'Mock Account 14', - managing_agency: 'Mock Agency 3', - managing_agency_acronym: 'ABC', - budget_authority_amount: 6500000 - }, - { - account_id: 15, - account_number: '234-5678', - account_name: 'Test Account 5', - managing_agency: 'Mock Agency 15', - managing_agency_acronym: 'DEF', - budget_authority_amount: 4500000 - }, - { - account_id: 16, - account_number: '123-4567', - account_name: 'Mock Account 16', - managing_agency: 'Mock Agency 2', - managing_agency_acronym: 'XYZ', - budget_authority_amount: 5500000 - }, - { - account_id: 17, - account_number: '098-7654', - account_name: 'Mock Account 17', - managing_agency: 'Mock Agency 17', - managing_agency_acronym: 'ABC', - budget_authority_amount: 6000000 - }, - { - account_id: 18, - account_number: '234-5678', - account_name: 'Test Account 6', - managing_agency: 'Mock Agency 2', - managing_agency_acronym: 'DEF', - budget_authority_amount: 4000000 - }, - { - account_id: 19, - account_number: '098-7654', - account_name: 'Mock Account 19', - managing_agency: 'Mock Agency 5', - managing_agency_acronym: 'ABC', - budget_authority_amount: 3000000 - }, - { - account_id: 20, - account_number: '234-5678', - account_name: 'Test Account 7', - managing_agency: 'Mock Agency 6', - managing_agency_acronym: 'DEF', - budget_authority_amount: 2000000 - } - ] + if (this.accountsRequest) { + // a request is in-flight, cancel it + this.accountsRequest.cancel(); + } + + this.setState({ + inFlight: true + }); + + // generate the params + const pageSize = 50; + const params = { + sort: this.state.order, + page: this.state.pageNumber, + limit: pageSize }; - this.parseAccounts(mockData); - // TODO - Lizzie: add API call when endpoint is ready - // if (this.accountsRequest) { - // // a request is in-flight, cancel it - // this.accountsRequest.cancel(); - // } - // - // this.setState({ - // inFlight: true - // }); - // - // // generate the params - // const params = { - // sort: this.state.order.field, - // order: this.state.order.direction - // }; - // - // this.accountsRequest = AccountLandingHelper.fetchAllAccounts(params); - // - // this.accountsRequest.promise - // .then((res) => { - // this.setState({ - // inFlight: false - // }); - // - // this.parseAccounts(res.data); - // }) - // .catch((err) => { - // this.accountsRequest = null; - // if (!isCancel(err)) { - // this.setState({ - // inFlight: false - // }); - // console.log(err); - // } - // }); + this.accountsRequest = AccountLandingHelper.fetchAllAccounts(params); + + this.accountsRequest.promise + .then((res) => { + this.setState({ + inFlight: false + }); + + this.parseAccounts(res.data); + }) + .catch((err) => { + this.accountsRequest = null; + if (!isCancel(err)) { + this.setState({ + inFlight: false + }); + console.log(err); + } + }); } parseAccounts(data) { const accounts = []; data.results.forEach((item) => { - // Format budget authority amount + // Format budgetary resources const formattedCurrency = - MoneyFormatter.formatMoneyWithPrecision(item.budget_authority_amount, 0); - - // Convert from decimal value to percentage and round to 2 decimal places - const percentage = (item.percentage_of_total_budget_authority * 100).toFixed(2); - - let percent = `${percentage}%`; - if (percent === '0.00%') { - percent = 'Less than 0.01%'; - } + MoneyFormatter.formatMoneyWithPrecision(item.budgetary_resources, 0); const account = { account_id: item.account_id, account_number: item.account_number, managing_agency: `${item.managing_agency} (${item.managing_agency_acronym})`, account_name: item.account_name, - budget_authority_amount: item.budget_authority_amount, + budgetary_resources: item.budgetary_resources, display: { account_number: `${item.account_number}`, account_name: `${item.account_name}`, managing_agency: `${item.managing_agency} (${item.managing_agency_acronym})`, - budget_authority_amount: formattedCurrency + budgetary_resources: formattedCurrency } }; accounts.push(account); }); this.setState({ - fullData: accounts - }, () => { - this.performSearch(); - }); - } - - performSearch() { - // perform a local search - const search = new Search('account_id'); - search.addIndex('account_name'); - search.addIndex('account_number'); - search.addIndex('managing_agency'); - search.addDocuments(this.state.fullData); - - // return the full data set if no search string is provided - let results = this.state.fullData; - if (this.state.accountSearchString !== '') { - results = search.search(this.state.accountSearchString); - } - - // now sort the results by the appropriate table column and direction - const orderedResults = orderBy(results, - [this.state.order.field], [this.state.order.direction]); - - // Reset to page 1 - this.setState({ - pageNumber: 1 - }); - - this.setState({ - results: orderedResults - }, () => { - this.setPageOfItems(); + totalItems: data.count, + results: accounts }); } render() { return ( ); } diff --git a/src/js/dataMapping/accountLanding/accountsTableFields.js b/src/js/dataMapping/accountLanding/accountsTableFields.js index c44e41b6ac..c92ce989d9 100644 --- a/src/js/dataMapping/accountLanding/accountsTableFields.js +++ b/src/js/dataMapping/accountLanding/accountsTableFields.js @@ -3,18 +3,18 @@ const agenciesTableFields = { account_number: 'desc', account_name: 'asc', managing_agency: 'asc', - budget_authority_amount: 'desc' + budgetary_resources: 'desc' }, order: [ 'account_number', 'account_name', 'managing_agency', - 'budget_authority_amount' + 'budgetary_resources' ], account_number: 'Account Number', account_name: 'Account Name', managing_agency: 'Managing Agency', - budget_authority_amount: 'Budgetary Resources' + budgetary_resources: 'Budgetary Resources' }; export default agenciesTableFields; diff --git a/src/js/helpers/accountLandingHelper.js b/src/js/helpers/accountLandingHelper.js index d7f7ae3c42..c15b741336 100644 --- a/src/js/helpers/accountLandingHelper.js +++ b/src/js/helpers/accountLandingHelper.js @@ -3,23 +3,124 @@ * Created by Lizzie Salita 8/4/17 **/ -import Axios, { CancelToken } from 'axios'; +// import Axios, { CancelToken } from 'axios'; -import kGlobalConstants from 'GlobalConstants'; +// import kGlobalConstants from 'GlobalConstants'; -export const fetchAllAccounts = (params) => { - const source = CancelToken.source(); - return { - promise: Axios.request({ - url: 'v2/references/federal_account/', - baseURL: kGlobalConstants.API, - method: 'get', - params, - cancelToken: source.token - }), - cancel() { - source.cancel(); +// TODO - Lizzie: update when API is ready +// export const fetchAllAccounts = (params) => { +// const source = CancelToken.source(); +// return { +// promise: Axios.request({ +// url: 'v2/references/federal_account/', +// baseURL: kGlobalConstants.API, +// method: 'get', +// params, +// cancelToken: source.token +// }), +// cancel() { +// source.cancel(); +// } +// }; +// }; + +const mockResponse = { + page: 1, + limit: 10, + count: 20, + results: [ + { + account_id: 1, + account_number: '123-4567', + account_name: 'Mock Account', + managing_agency: 'Mock Agency', + managing_agency_acronym: 'XYZ', + budgetary_resources: 5000000 + }, + { + account_id: 2, + account_number: '098-7654', + account_name: 'Mock Account 2', + managing_agency: 'Mock Agency 2', + managing_agency_acronym: 'ABC', + budgetary_resources: 6500000 + }, + { + account_id: 3, + account_number: '234-5678', + account_name: 'Test Account', + managing_agency: 'Mock Agency 3', + managing_agency_acronym: 'DEF', + budgetary_resources: 4500000 + }, + { + account_id: 4, + account_number: '123-4567', + account_name: 'Mock Account 4', + managing_agency: 'Mock Agency 4', + managing_agency_acronym: 'XYZ', + budgetary_resources: 5500000 + }, + { + account_id: 5, + account_number: '098-7654', + account_name: 'Mock Account 5', + managing_agency: 'Mock Agency 5', + managing_agency_acronym: 'ABC', + budgetary_resources: 6000000 + }, + { + account_id: 6, + account_number: '234-5678', + account_name: 'Test Account 2', + managing_agency: 'Mock Agency 6', + managing_agency_acronym: 'DEF', + budgetary_resources: 4000000 + }, + { + account_id: 7, + account_number: '123-4567', + account_name: 'Mock Account 7', + managing_agency: 'Mock Agency 7', + managing_agency_acronym: 'XYZ', + budgetary_resources: 5000000 + }, + { + account_id: 8, + account_number: '098-7654', + account_name: 'Mock Account 8', + managing_agency: 'Mock Agency 8', + managing_agency_acronym: 'ABC', + budgetary_resources: 6500000 + }, + { + account_id: 9, + account_number: '234-5678', + account_name: 'Test Account 3', + managing_agency: 'Mock Agency 9', + managing_agency_acronym: 'DEF', + budgetary_resources: 4500000 + }, + { + account_id: 10, + account_number: '123-4567', + account_name: 'Mock Account 10', + managing_agency: 'Mock Agency 6', + managing_agency_acronym: 'XYZ', + budgetary_resources: 5500000 } - }; + ] }; +export const fetchAllAccounts = () => ( + { + promise: new Promise((resolve) => { + setTimeout(() => { + resolve({ + data: mockResponse + }); + }, 1000); + }) + } +); + From c5270ac93292947effa37230c297b7a55afa0d92 Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Thu, 1 Feb 2018 14:03:54 -0500 Subject: [PATCH 17/89] BEM'd landing page layout and search section; hid search bar on account landing page --- .../layouts/landingPage/landingPage.scss | 13 ++--- .../accountLanding/accountLandingPage.scss | 18 ++----- .../accountLanding/accountLandingSearch.scss | 50 ------------------- .../pages/accountLanding/searchSection.scss | 46 +++++++++++++++++ .../accountLanding/table/resultsSection.scss | 48 +++++++++--------- .../agencyLanding/agencyLandingPage.scss | 16 +++--- .../agencyLanding/agencyLandingSearch.scss | 50 ------------------- .../accountLanding/AccountLandingContent.jsx | 48 ++++++++---------- .../AccountLandingSearchBar.jsx | 8 +-- .../agencyLanding/AgencyLandingContent.jsx | 24 ++++----- .../agencyLanding/AgencyLandingSearchBar.jsx | 8 +-- 11 files changed, 120 insertions(+), 209 deletions(-) delete mode 100644 src/_scss/pages/accountLanding/accountLandingSearch.scss create mode 100644 src/_scss/pages/accountLanding/searchSection.scss delete mode 100644 src/_scss/pages/agencyLanding/agencyLandingSearch.scss diff --git a/src/_scss/layouts/landingPage/landingPage.scss b/src/_scss/layouts/landingPage/landingPage.scss index 067f46694f..35bc35776f 100644 --- a/src/_scss/layouts/landingPage/landingPage.scss +++ b/src/_scss/layouts/landingPage/landingPage.scss @@ -1,12 +1,8 @@ -@import "../default/default"; @import "../summary/summary"; -.landing-page-content { +.landing-page { padding: $global-pad; - .landing-page-overview { - @include span-columns(16); - @include shift(0); - + .landing-page__overview { text-align: center; h3 { margin-top: 0; @@ -16,12 +12,9 @@ margin-bottom: rem(30); } } - .landing-page-section { - @include span-columns(16); - } @include media($medium-screen) { padding: ($global-pad * 2); - .landing-page-overview { + .landing-page__overview { width: 75%; margin: auto; float: none; diff --git a/src/_scss/pages/accountLanding/accountLandingPage.scss b/src/_scss/pages/accountLanding/accountLandingPage.scss index 52dded111f..acc3302fc4 100644 --- a/src/_scss/pages/accountLanding/accountLandingPage.scss +++ b/src/_scss/pages/accountLanding/accountLandingPage.scss @@ -1,24 +1,12 @@ .usa-da-account-landing { @import "all"; - @import "layouts/search/search"; - @import "./header/header"; @import "layouts/landingPage/landingPage"; @import "components/pagination"; @import "./header/header"; - @import './accountLandingSearch'; + @import './searchSection'; @import './table/resultsSection'; - .landing-page-section { - &.results-count { - font-style: italic; - padding: rem(20) 0; - font-size: rem(16); - .pagination { - margin: 0; - } - } + .search-section { + display: none; } - @include accountLandingSearch; - @include resultsSection; - } \ No newline at end of file diff --git a/src/_scss/pages/accountLanding/accountLandingSearch.scss b/src/_scss/pages/accountLanding/accountLandingSearch.scss deleted file mode 100644 index b1f1bec2c9..0000000000 --- a/src/_scss/pages/accountLanding/accountLandingSearch.scss +++ /dev/null @@ -1,50 +0,0 @@ -@mixin accountLandingSearch { - .account-landing-search { - @include span-columns(16); - background-color: $color-primary-alt-lightest; - padding: rem(5); - border: 1px solid $color-vis-lightest; - text-align: center; - form { - position: relative; - input.search-field { - color: $color-gray-light; - font-size: rem(14); - font-weight: 300; - line-height: rem(36); - padding: 0 rem(30) 0 rem(4); - width: 100%; - } - .search-button { - position: absolute; - top: rem(5); - left: 90%; - - @include button-unstyled; - width: 25px; - height: 25px; - - svg { - fill: $color-gray-light; - } - } - } - @include media($medium-screen) { - padding: rem(30); - form { - input.search-field { - width: 75%; - font-size: rem(28); - padding: rem(15); - padding-right: rem(30); - } - .search-button { - left: 80%; - top: rem(15); - width: 36px; - height: 36px; - } - } - } - } -} \ No newline at end of file diff --git a/src/_scss/pages/accountLanding/searchSection.scss b/src/_scss/pages/accountLanding/searchSection.scss new file mode 100644 index 0000000000..a195bdf157 --- /dev/null +++ b/src/_scss/pages/accountLanding/searchSection.scss @@ -0,0 +1,46 @@ +.search-section { + background-color: $color-primary-alt-lightest; + padding: rem(5); + border: 1px solid $color-vis-lightest; + text-align: center; + .search-section__form { + position: relative; + .search-section__input { + color: $color-gray-light; + font-weight: 300; + line-height: rem(28); + padding: 0 rem(30) 0 rem(4); + width: 100%; + } + .search-section__button { + position: absolute; + top: rem(5); + left: 90%; + + @include button-unstyled; + width: 25px; + height: 25px; + + svg { + fill: $color-gray-light; + } + } + } + @include media($medium-screen) { + padding: rem(30); + .search-section__form { + .search-section__input { + width: 75%; + font-size: rem(22); + padding: rem(15); + padding-right: rem(30); + } + .search-section__button { + left: 80%; + top: rem(15); + width: 36px; + height: 36px; + } + } + } +} \ No newline at end of file diff --git a/src/_scss/pages/accountLanding/table/resultsSection.scss b/src/_scss/pages/accountLanding/table/resultsSection.scss index 0b776fd8a8..b0b2fc5b25 100644 --- a/src/_scss/pages/accountLanding/table/resultsSection.scss +++ b/src/_scss/pages/accountLanding/table/resultsSection.scss @@ -1,33 +1,31 @@ -@mixin resultsSection { - .account-landing-results { - transition: opacity 0.25s ease; +.account-landing-results { + transition: opacity 0.25s ease; - @import './table'; + @import './table'; - .loading-table { - opacity: 0.5; - @include transition(opacity 0.25s ease-in); - } + .loading-table { + opacity: 0.5; + @include transition(opacity 0.25s ease-in); + } - .loaded-table { - opacity: 1; - @include transition(opacity 0.25s ease-in); - } + .loaded-table { + opacity: 1; + @include transition(opacity 0.25s ease-in); + } - .account-landing-table-width-master { - width: 100%; - } + .account-landing-table-width-master { + width: 100%; + } - .results-table-message { - margin: rem(90) auto; - font-size: rem(36); - font-weight: 300; - color: $color-base; - text-align: center; - span { - color: $color-active; - font-weight: normal; - } + .results-table-message { + margin: rem(90) auto; + font-size: rem(36); + font-weight: 300; + color: $color-base; + text-align: center; + span { + color: $color-active; + font-weight: normal; } } } diff --git a/src/_scss/pages/agencyLanding/agencyLandingPage.scss b/src/_scss/pages/agencyLanding/agencyLandingPage.scss index 239f7f027e..cf7fbeca32 100644 --- a/src/_scss/pages/agencyLanding/agencyLandingPage.scss +++ b/src/_scss/pages/agencyLanding/agencyLandingPage.scss @@ -2,17 +2,13 @@ @import "all"; @import "layouts/landingPage/landingPage"; @import "./header/header"; - @import './agencyLandingSearch'; + @import "pages/accountLanding/searchSection"; @import './table/resultsSection'; - .landing-page-section { - &.results-count { - font-style: italic; - padding: rem(20) 0; - font-size: rem(16); - } - } - @include agencyLandingSearch; @include resultsSection; - + .results-count { + font-style: italic; + padding: rem(20) 0; + font-size: rem(16); + } } \ No newline at end of file diff --git a/src/_scss/pages/agencyLanding/agencyLandingSearch.scss b/src/_scss/pages/agencyLanding/agencyLandingSearch.scss deleted file mode 100644 index 5d21c8b6d2..0000000000 --- a/src/_scss/pages/agencyLanding/agencyLandingSearch.scss +++ /dev/null @@ -1,50 +0,0 @@ -@mixin agencyLandingSearch { - .agency-landing-search { - @include span-columns(16); - background-color: $color-primary-alt-lightest; - padding: rem(5); - border: 1px solid $color-vis-lightest; - text-align: center; - form { - position: relative; - input.search-field { - color: $color-gray-light; - font-size: rem(14); - font-weight: 300; - line-height: rem(36); - padding: 0 rem(30) 0 rem(4); - width: 100%; - } - .search-button { - position: absolute; - top: rem(5); - left: 90%; - - @include button-unstyled; - width: 25px; - height: 25px; - - svg { - fill: $color-gray-light; - } - } - } - @include media($medium-screen) { - padding: rem(30); - form { - input.search-field { - width: 75%; - font-size: rem(28); - padding: rem(15); - padding-right: rem(30); - } - .search-button { - left: 80%; - top: rem(15); - width: 36px; - height: 36px; - } - } - } - } -} \ No newline at end of file diff --git a/src/js/components/accountLanding/AccountLandingContent.jsx b/src/js/components/accountLanding/AccountLandingContent.jsx index cfe98532f1..81f37b324a 100644 --- a/src/js/components/accountLanding/AccountLandingContent.jsx +++ b/src/js/components/accountLanding/AccountLandingContent.jsx @@ -27,8 +27,8 @@ const propTypes = { export default class AccountLandingContent extends React.Component { render() { return ( -
    -
    +
    +

    Find a Federal Account Profile.

    Explore spending in greater detail in our federal account profiles.

    @@ -37,31 +37,25 @@ export default class AccountLandingContent extends React.Component { funding granted by congress to carry out their programs, projects, or activities.

    -
    - -
    -
    - -
    -
    - - -
    + + + +
    ); } diff --git a/src/js/components/accountLanding/AccountLandingSearchBar.jsx b/src/js/components/accountLanding/AccountLandingSearchBar.jsx index ee4d1d4752..2a9b384db3 100644 --- a/src/js/components/accountLanding/AccountLandingSearchBar.jsx +++ b/src/js/components/accountLanding/AccountLandingSearchBar.jsx @@ -20,16 +20,16 @@ export default class AccountLandingSearchBar extends React.Component { render() { return ( -
    -
    +
    + diff --git a/src/js/components/agencyLanding/AgencyLandingContent.jsx b/src/js/components/agencyLanding/AgencyLandingContent.jsx index b0fd1c5964..6bffb9e478 100644 --- a/src/js/components/agencyLanding/AgencyLandingContent.jsx +++ b/src/js/components/agencyLanding/AgencyLandingContent.jsx @@ -21,8 +21,8 @@ const propTypes = { export default class AgencyLandingContent extends React.Component { render() { return ( -
    -
    +
    +

    Find an Agency Profile.

    Understand the current spending of agencies in our agency profiles.

    These include the 15 executive departments whose leaders sit on the @@ -30,20 +30,16 @@ export default class AgencyLandingContent extends React.Component { commissions. They range in size from $700 billion down to less than $200,000.

    -
    - -
    -
    + +
    {this.props.resultsText}
    -
    - -
    +
    ); } diff --git a/src/js/components/agencyLanding/AgencyLandingSearchBar.jsx b/src/js/components/agencyLanding/AgencyLandingSearchBar.jsx index fbf96048cf..144843c733 100644 --- a/src/js/components/agencyLanding/AgencyLandingSearchBar.jsx +++ b/src/js/components/agencyLanding/AgencyLandingSearchBar.jsx @@ -20,16 +20,16 @@ export default class AgencyLandingSearchBar extends React.Component { render() { return ( -
    -
    +
    + From d9c491c68f44066605c534fb75ac00c06ba734f4 Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Thu, 1 Feb 2018 15:35:33 -0500 Subject: [PATCH 18/89] BEM'd results table section --- .../accountLanding/table/resultsSection.scss | 17 +-- .../pages/accountLanding/table/table.scss | 122 ++++++++---------- .../AccountLandingResultsSection.jsx | 26 ++-- .../table/AccountLandingTable.jsx | 31 ++--- .../accountLanding/table/TableRow.jsx | 18 +-- .../table/cells/AccountLinkCell.jsx | 8 +- ...ricAccountCell.jsx => HighlightedCell.jsx} | 18 +-- .../AccountLandingContainer.jsx | 1 + 8 files changed, 112 insertions(+), 129 deletions(-) rename src/js/components/accountLanding/table/cells/{GenericAccountCell.jsx => HighlightedCell.jsx} (54%) diff --git a/src/_scss/pages/accountLanding/table/resultsSection.scss b/src/_scss/pages/accountLanding/table/resultsSection.scss index b0b2fc5b25..da9b1f0028 100644 --- a/src/_scss/pages/accountLanding/table/resultsSection.scss +++ b/src/_scss/pages/accountLanding/table/resultsSection.scss @@ -1,29 +1,20 @@ -.account-landing-results { +.results-table-section { transition: opacity 0.25s ease; @import './table'; - .loading-table { + .results-table-section__loading-wrapper_loading { opacity: 0.5; @include transition(opacity 0.25s ease-in); } - .loaded-table { - opacity: 1; - @include transition(opacity 0.25s ease-in); - } - - .account-landing-table-width-master { - width: 100%; - } - - .results-table-message { + .results-table-section__message { margin: rem(90) auto; font-size: rem(36); font-weight: 300; color: $color-base; text-align: center; - span { + .results-table-section__search-string { color: $color-active; font-weight: normal; } diff --git a/src/_scss/pages/accountLanding/table/table.scss b/src/_scss/pages/accountLanding/table/table.scss index f197b4afd8..a76e62572d 100644 --- a/src/_scss/pages/accountLanding/table/table.scss +++ b/src/_scss/pages/accountLanding/table/table.scss @@ -1,72 +1,62 @@ -.account-landing-results-table { +.results-table { display: block; - overflow: scroll; + overflow: scroll; + margin-top: rem(15); - &.no-results { - border: none; + .results-table__head { + .results-table__row { + border-bottom: solid 1px $color-gray-lighter; + .results-table__data { + border: 0; + &:first-child { + width: 18%; + } + &:nth-child(2), &:nth-child(3) { + width: 31%; + } + &:last-child { + width: 20%; + .cell-content { + float: right; + } + } + } + } + .award-result-header-cell { + @import "./cells/_accountLandingHeaderCell"; + @include accountLandingHeader(); + } } - table { - margin-top: rem(15); - width: rem(500); - - @include media($tablet-screen) { - width: auto; + .results-table__body { + .results-table__row { + .results-table__data { + border: 0; + &:first-child { + width: 18%; + } + &:nth-child(2), &:nth-child(3) { + width: 31%; + } + &:last-child { + width: 20%; + .results-table-cell__content, .cell-content { + float: right; + } + } + } + .results-table-cell { + .results-table-cell__content { + .results-table-cell__highlight { + text-decoration: underline; + font-weight: 600; + } + } + } + // gray out even rows + .results-table__data_even { + background-color: #f7f7f7; + } } + } - thead { - tr { - border-bottom: solid 1px $color-gray-lighter; - td { - border: 0; - &:first-child { - width: 18%; - } - &:nth-child(2), &:nth-child(3) { - width: 31%; - } - &:last-child { - width: 20%; - .cell-content { - float: right; - } - } - } - } - .award-result-header-cell { - @import "./cells/_accountLandingHeaderCell"; - @include accountLandingHeader(); - } - } - tbody { - tr { - td { - border: 0; - &:first-child { - width: 18%; - } - &:nth-child(2), &:nth-child(3) { - width: 31%; - } - &:last-child { - width: 20%; - .cell-content { - float: right; - } - } - } - .account-link-cell, .generic-account-cell { - .cell-content { - span { - text-decoration: underline; - font-weight: 600; - } - } - } - // gray out even rows - .row-even { - background-color: #f7f7f7; - } - } - } - } } \ No newline at end of file diff --git a/src/js/components/accountLanding/AccountLandingResultsSection.jsx b/src/js/components/accountLanding/AccountLandingResultsSection.jsx index b997a04dac..eeaa259e1d 100644 --- a/src/js/components/accountLanding/AccountLandingResultsSection.jsx +++ b/src/js/components/accountLanding/AccountLandingResultsSection.jsx @@ -6,7 +6,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import ResultsTableMessage from 'components/search/table/ResultsTableMessage'; import AccountLandingTable from './table/AccountLandingTable'; const propTypes = { @@ -20,28 +19,39 @@ const propTypes = { export default class AccountLandingResultsSection extends React.Component { render() { - let loadingWrapper = ''; + let loadingWrapper = 'results-table-section__loading-wrapper'; let message = null; if (this.props.inFlight) { - loadingWrapper = 'loading-table'; - message = ; + loadingWrapper = 'results-table-section__loading-wrapper results-table-section__loading-wrapper_loading'; + message = ( +
    + Loading data... +
    + ); } else if (this.props.results.length === 0) { // no results if (this.props.accountSearchString) { message = ( -
    - No results found for “ {this.props.accountSearchString} ”. +
    + No results found for “ + + {this.props.accountSearchString} + ”.
    ); } else { - message = ; + message = ( +
    + No results found. +
    + ); } } return ( -
    +
    diff --git a/src/js/components/accountLanding/table/AccountLandingTable.jsx b/src/js/components/accountLanding/table/AccountLandingTable.jsx index 84fbf524d6..1e14e297d9 100644 --- a/src/js/components/accountLanding/table/AccountLandingTable.jsx +++ b/src/js/components/accountLanding/table/AccountLandingTable.jsx @@ -19,13 +19,6 @@ const propTypes = { export default class AccountLandingTable extends React.PureComponent { render() { - let noResultsClass = ''; - if (this.props.results.length === 0) { - // remove duplicated bottom border - noResultsClass = ' no-results'; - } - - const rows = this.props.results.map((account, index) => ( ( -
    ); }); return ( - + {cells} ); diff --git a/src/js/components/accountLanding/table/cells/AccountLinkCell.jsx b/src/js/components/accountLanding/table/cells/AccountLinkCell.jsx index 1c27565ca0..dd93f7dca5 100644 --- a/src/js/components/accountLanding/table/cells/AccountLinkCell.jsx +++ b/src/js/components/accountLanding/table/cells/AccountLinkCell.jsx @@ -19,15 +19,15 @@ export default class AccountLinkCell extends React.Component { render() { let name = this.props.name; // highlight the matched string if applicable - if (this.props.accountSearchString !== '') { + if (this.props.accountSearchString) { name = reactStringReplace(this.props.name, this.props.accountSearchString, (match, i) => ( - {match} + {match} )); } return ( -
    -
    +
    +
    {name} diff --git a/src/js/components/accountLanding/table/cells/GenericAccountCell.jsx b/src/js/components/accountLanding/table/cells/HighlightedCell.jsx similarity index 54% rename from src/js/components/accountLanding/table/cells/GenericAccountCell.jsx rename to src/js/components/accountLanding/table/cells/HighlightedCell.jsx index 683e4118b0..7981164efb 100644 --- a/src/js/components/accountLanding/table/cells/GenericAccountCell.jsx +++ b/src/js/components/accountLanding/table/cells/HighlightedCell.jsx @@ -1,5 +1,5 @@ /** - * GenericAccountCell.jsx + * HighlightedCell.jsx * Created by Lizzie Salita 08/10/17 **/ @@ -11,21 +11,21 @@ const propTypes = { data: PropTypes.string, rowIndex: PropTypes.number, column: PropTypes.string, - accountSearchString: PropTypes.string + searchString: PropTypes.string }; -export default class GenericAccountCell extends React.Component { +export default class HighlightedCell extends React.Component { render() { let data = this.props.data; // highlight the matched string if applicable - if (this.props.accountSearchString !== '') { - data = reactStringReplace(this.props.data, this.props.accountSearchString, (match, i) => ( - {match} + if (this.props.searchString) { + data = reactStringReplace(this.props.data, this.props.searchString, (match, i) => ( + {match} )); } return ( -
    -
    +
    +
    {data}
    @@ -33,4 +33,4 @@ export default class GenericAccountCell extends React.Component { } } -GenericAccountCell.propTypes = propTypes; +HighlightedCell.propTypes = propTypes; diff --git a/src/js/containers/accountLanding/AccountLandingContainer.jsx b/src/js/containers/accountLanding/AccountLandingContainer.jsx index 1d03c98e09..b82363dd48 100644 --- a/src/js/containers/accountLanding/AccountLandingContainer.jsx +++ b/src/js/containers/accountLanding/AccountLandingContainer.jsx @@ -185,6 +185,7 @@ export default class AccountLandingContainer extends React.Component { columns={this.state.columns} order={this.state.order} updateSort={this.updateSort} + accountSearchString={this.state.searchString} setAccountSearchString={this.setAccountSearchString} onChangePage={this.onChangePage} pageNumber={this.state.pageNumber} From 89558bcdf6544741013984a61c4c836020e3e64b Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Thu, 1 Feb 2018 15:51:31 -0500 Subject: [PATCH 19/89] Update landing page headers to thinner style --- .../pages/accountLanding/header/header.scss | 2 +- src/_scss/pages/agencyLanding/header/header.scss | 2 +- .../header/AccountLandingHeader.jsx | 16 +++++++++++----- .../agencyLanding/header/AgencyLandingHeader.jsx | 16 +++++++++++----- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/_scss/pages/accountLanding/header/header.scss b/src/_scss/pages/accountLanding/header/header.scss index f0d2edb6a2..51ec9e11e4 100644 --- a/src/_scss/pages/accountLanding/header/header.scss +++ b/src/_scss/pages/accountLanding/header/header.scss @@ -1 +1 @@ -@import "components/pageTitleBar/pageTitleBar"; \ No newline at end of file +@import "layouts/tabbedSearch/header/header"; \ No newline at end of file diff --git a/src/_scss/pages/agencyLanding/header/header.scss b/src/_scss/pages/agencyLanding/header/header.scss index f0d2edb6a2..51ec9e11e4 100644 --- a/src/_scss/pages/agencyLanding/header/header.scss +++ b/src/_scss/pages/agencyLanding/header/header.scss @@ -1 +1 @@ -@import "components/pageTitleBar/pageTitleBar"; \ No newline at end of file +@import "layouts/tabbedSearch/header/header"; \ No newline at end of file diff --git a/src/js/components/accountLanding/header/AccountLandingHeader.jsx b/src/js/components/accountLanding/header/AccountLandingHeader.jsx index 9428aa7f5a..d88e26d474 100644 --- a/src/js/components/accountLanding/header/AccountLandingHeader.jsx +++ b/src/js/components/accountLanding/header/AccountLandingHeader.jsx @@ -8,11 +8,17 @@ import React from 'react'; export default class AccountLandingHeader extends React.Component { render() { return ( -
    -
    -

    - Federal Account Profiles -

    +
    +
    +
    +
    +

    + Federal Account Profiles +

    +
    +
    ); diff --git a/src/js/components/agencyLanding/header/AgencyLandingHeader.jsx b/src/js/components/agencyLanding/header/AgencyLandingHeader.jsx index 52a9cf6f38..59427096e2 100644 --- a/src/js/components/agencyLanding/header/AgencyLandingHeader.jsx +++ b/src/js/components/agencyLanding/header/AgencyLandingHeader.jsx @@ -8,11 +8,17 @@ import React from 'react'; export default class AgencyLandingHeader extends React.Component { render() { return ( -
    -
    -

    - Agency Profiles -

    +
    +
    +
    +
    +

    + Agency Profiles +

    +
    +
    ); From 7d163ebc9054f27e283df426a25124b9f6b9483a Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Fri, 2 Feb 2018 09:41:20 -0500 Subject: [PATCH 20/89] sass code cleanup --- .../accountLanding/accountLandingPage.scss | 18 ++--- .../cells/_accountLandingHeaderCell.scss | 67 +++++++++---------- .../pages/accountLanding/table/table.scss | 1 - 3 files changed, 41 insertions(+), 45 deletions(-) diff --git a/src/_scss/pages/accountLanding/accountLandingPage.scss b/src/_scss/pages/accountLanding/accountLandingPage.scss index acc3302fc4..4c35980cc0 100644 --- a/src/_scss/pages/accountLanding/accountLandingPage.scss +++ b/src/_scss/pages/accountLanding/accountLandingPage.scss @@ -1,12 +1,12 @@ .usa-da-account-landing { - @import "all"; - @import "layouts/landingPage/landingPage"; - @import "components/pagination"; - @import "./header/header"; - @import './searchSection'; - @import './table/resultsSection'; + @import "all"; + @import "layouts/landingPage/landingPage"; + @import "components/pagination"; + @import "./header/header"; + @import './searchSection'; + @import './table/resultsSection'; - .search-section { - display: none; - } + .search-section { + display: none; + } } \ No newline at end of file diff --git a/src/_scss/pages/accountLanding/table/cells/_accountLandingHeaderCell.scss b/src/_scss/pages/accountLanding/table/cells/_accountLandingHeaderCell.scss index 76ba48ea4a..a35e8acdf6 100644 --- a/src/_scss/pages/accountLanding/table/cells/_accountLandingHeaderCell.scss +++ b/src/_scss/pages/accountLanding/table/cells/_accountLandingHeaderCell.scss @@ -1,46 +1,43 @@ -@mixin accountLandingHeader() { +.cell-content { + white-space: normal; + font-size: rem(18); + font-weight: 600; + line-height: 25px; + color: $color-base; + cursor: pointer; - .cell-content { - white-space: normal; - font-size: rem(18); - font-weight: 600; - line-height: 25px; - color: $color-base; - cursor: pointer; + .header-sort { + display: table; + padding: rem(11) rem(20); - .header-sort { - display: table; - padding: rem(11) rem(20); + .header-label { + display: table-cell; + vertical-align: middle; + } - .header-label { - display: table-cell; - vertical-align: middle; - } + .header-icons { + display: table-cell; + vertical-align: middle; + padding-left: rem(5); + height: rem(10); - .header-icons { - display: table-cell; - vertical-align: middle; - padding-left: rem(5); + .sort-icon { + @include button-unstyled; + display: block; + line-height: rem(10); + width: rem(14); height: rem(10); + text-align: center; - .sort-icon { - @include button-unstyled; - display: block; - line-height: rem(10); - width: rem(14); - height: rem(10); - text-align: center; + svg { + fill: #86888E; + height: rem(11); + width: rem(11); + } + &.active { svg { - fill: #86888E; - height: rem(11); - width: rem(11); - } - - &.active { - svg { - fill: $color-active; - } + fill: $color-active; } } } diff --git a/src/_scss/pages/accountLanding/table/table.scss b/src/_scss/pages/accountLanding/table/table.scss index a76e62572d..6e2321f6e3 100644 --- a/src/_scss/pages/accountLanding/table/table.scss +++ b/src/_scss/pages/accountLanding/table/table.scss @@ -24,7 +24,6 @@ } .award-result-header-cell { @import "./cells/_accountLandingHeaderCell"; - @include accountLandingHeader(); } } .results-table__body { From 07108db9fef9b72c63f42bef185d8e4e8fb207c8 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Fri, 2 Feb 2018 12:55:42 -0500 Subject: [PATCH 21/89] Initial analytics implementation --- package.json | 1 - src/index.html | 2 + .../glossary/search/GlossarySearchResults.jsx | 5 +-- .../awardAmount/SpecificAwardAmountItem.jsx | 5 +-- .../filters/timePeriod/AllFiscalYears.jsx | 6 +-- .../search/filters/timePeriod/TimePeriod.jsx | 6 +-- .../FloatingGlossaryButton.jsx | 7 ++-- .../checkbox/PrimaryCheckboxType.jsx | 8 ++-- .../checkbox/SecondaryCheckboxType.jsx | 8 ++-- .../checkbox/SingleCheckboxType.jsx | 8 ++-- .../filterSidebar/FilterOption.jsx | 5 +-- .../header/NavBarGlossaryLink.jsx | 6 +-- src/js/containers/router/RouterContainer.jsx | 16 ++------ .../search/filters/AgencyContainer.jsx | 6 +-- .../search/filters/KeywordContainer.jsx | 6 +-- .../awardID/AwardIDSearchContainer.jsx | 6 +-- .../filters/cfda/CFDASearchContainer.jsx | 6 +-- .../filters/location/POPFilterContainer.jsx | 6 +-- .../location/RecipientFilterContainer.jsx | 6 +-- .../filters/naics/NAICSSearchContainer.jsx | 6 +-- .../search/filters/psc/PSCSearchContainer.jsx | 6 +-- .../recipient/RecipientSearchContainer.jsx | 6 +-- .../recipient/RecipientTypeContainer.jsx | 5 +-- .../search/table/ResultsTableContainer.jsx | 5 +-- src/js/helpers/analytics/Analytics.js | 41 +++++++++++++++++++ 25 files changed, 108 insertions(+), 80 deletions(-) create mode 100644 src/js/helpers/analytics/Analytics.js diff --git a/package.json b/package.json index e2c7411d42..a1a18d03b4 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,6 @@ "react-dnd": "^2.4.0", "react-dnd-html5-backend": "^2.4.1", "react-dom": "15.6.1", - "react-ga": "2.2.0", "react-helmet": "^5.0.0", "react-map-gl": "^2.0.2", "react-markdown": "^2.5.0", diff --git a/src/index.html b/src/index.html index 50d4ac1bf8..e25b8c1e48 100644 --- a/src/index.html +++ b/src/index.html @@ -29,6 +29,8 @@ + + diff --git a/src/js/components/glossary/search/GlossarySearchResults.jsx b/src/js/components/glossary/search/GlossarySearchResults.jsx index 4a09ac6520..aefbd14eac 100644 --- a/src/js/components/glossary/search/GlossarySearchResults.jsx +++ b/src/js/components/glossary/search/GlossarySearchResults.jsx @@ -6,6 +6,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { concat, sortBy } from 'lodash'; +import Analytics from 'helpers/analytics/Analytics'; import ResultGroup from './ResultGroup'; @@ -15,11 +16,9 @@ const propTypes = { setGlossaryTerm: PropTypes.func }; -const ga = require('react-ga'); - export default class GlossarySearchResults extends React.Component { static logGlossaryTermEvent(term) { - ga.event({ + Analytics.event({ category: 'Glossary', action: 'Clicked Glossary Term', label: term diff --git a/src/js/components/search/filters/awardAmount/SpecificAwardAmountItem.jsx b/src/js/components/search/filters/awardAmount/SpecificAwardAmountItem.jsx index ed230ca485..017eaa5095 100644 --- a/src/js/components/search/filters/awardAmount/SpecificAwardAmountItem.jsx +++ b/src/js/components/search/filters/awardAmount/SpecificAwardAmountItem.jsx @@ -5,6 +5,7 @@ import React from 'react'; import PropTypes from 'prop-types'; +import Analytics from 'helpers/analytics/Analytics'; import IndividualSubmit from 'components/search/filters/IndividualSubmit'; @@ -15,11 +16,9 @@ const propTypes = { searchSpecificRange: PropTypes.func }; -const ga = require('react-ga'); - export default class SpecificAwardAmountItem extends React.Component { static logAmountRangeEvent(range) { - ga.event({ + Analytics.event({ category: 'Search Page Filter Applied', action: 'Applied Award Amount Range Filter', label: range diff --git a/src/js/components/search/filters/timePeriod/AllFiscalYears.jsx b/src/js/components/search/filters/timePeriod/AllFiscalYears.jsx index c72d880c8f..2750ae061d 100644 --- a/src/js/components/search/filters/timePeriod/AllFiscalYears.jsx +++ b/src/js/components/search/filters/timePeriod/AllFiscalYears.jsx @@ -7,6 +7,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Set } from 'immutable'; +import Analytics from 'helpers/analytics/Analytics'; + import FiscalYear from './FiscalYear'; const propTypes = { @@ -15,11 +17,9 @@ const propTypes = { updateFilter: PropTypes.func }; -const ga = require('react-ga'); - export default class AllFiscalYears extends React.Component { static logFYEvent(year) { - ga.event({ + Analytics.event({ category: 'Search Page Filter Applied', action: 'Applied Fiscal Year Filter', label: year diff --git a/src/js/components/search/filters/timePeriod/TimePeriod.jsx b/src/js/components/search/filters/timePeriod/TimePeriod.jsx index bd18684bb4..1bf2ad0cfe 100644 --- a/src/js/components/search/filters/timePeriod/TimePeriod.jsx +++ b/src/js/components/search/filters/timePeriod/TimePeriod.jsx @@ -8,6 +8,8 @@ import PropTypes from 'prop-types'; import moment from 'moment'; import { Set } from 'immutable'; +import Analytics from 'helpers/analytics/Analytics'; + import SubmitHint from 'components/sharedComponents/filterSidebar/SubmitHint'; import DateRange from './DateRange'; @@ -33,8 +35,6 @@ const propTypes = { dirtyFilters: PropTypes.symbol }; -const ga = require('react-ga'); - export default class TimePeriod extends React.Component { static logDateRangeEvent(start, end) { let label = `${start} to ${end}`; @@ -45,7 +45,7 @@ export default class TimePeriod extends React.Component { label = `On or after ${start}`; } - ga.event({ + Analytics.event({ label, category: 'Search Page Filter Applied', action: 'Applied Date Range Filter' diff --git a/src/js/components/sharedComponents/FloatingGlossaryButton.jsx b/src/js/components/sharedComponents/FloatingGlossaryButton.jsx index c15500c02f..75f594fd19 100644 --- a/src/js/components/sharedComponents/FloatingGlossaryButton.jsx +++ b/src/js/components/sharedComponents/FloatingGlossaryButton.jsx @@ -6,17 +6,18 @@ import React from 'react'; import PropTypes from 'prop-types'; import { throttle } from 'lodash'; + +import Analytics from 'helpers/analytics/Analytics'; + import { Glossary } from './icons/Icons'; const propTypes = { toggleGlossary: PropTypes.func }; -const ga = require('react-ga'); - export default class FloatingGlossaryButton extends React.Component { static logGlossaryButtonEvent() { - ga.event({ + Analytics.event({ category: 'Glossary', action: 'Opened Glossary', label: 'Floating Glossary Button' diff --git a/src/js/components/sharedComponents/checkbox/PrimaryCheckboxType.jsx b/src/js/components/sharedComponents/checkbox/PrimaryCheckboxType.jsx index 8ac465f22e..7c428adb17 100644 --- a/src/js/components/sharedComponents/checkbox/PrimaryCheckboxType.jsx +++ b/src/js/components/sharedComponents/checkbox/PrimaryCheckboxType.jsx @@ -8,6 +8,8 @@ import PropTypes from 'prop-types'; import { Set } from 'immutable'; import { uniqueId } from 'lodash'; +import Analytics from 'helpers/analytics/Analytics'; + import SecondaryCheckboxType from './SecondaryCheckboxType'; import CollapsedCheckboxType from './CollapsedCheckboxType'; import SingleCheckboxType from './SingleCheckboxType'; @@ -34,11 +36,9 @@ const defaultProps = { enableAnalytics: false }; -const ga = require('react-ga'); - export default class PrimaryCheckboxType extends React.Component { static logPrimaryTypeFilterEvent(type, filter) { - ga.event({ + Analytics.event({ category: 'Search Page Filter Applied', action: `Selected ${filter} Type`, label: type @@ -46,7 +46,7 @@ export default class PrimaryCheckboxType extends React.Component { } static logDeselectFilterEvent(type, filter) { - ga.event({ + Analytics.event({ category: 'Search Page Filter Applied', action: `Deselected ${filter} Type Children`, label: type diff --git a/src/js/components/sharedComponents/checkbox/SecondaryCheckboxType.jsx b/src/js/components/sharedComponents/checkbox/SecondaryCheckboxType.jsx index 2d50b82233..54ad5d3860 100644 --- a/src/js/components/sharedComponents/checkbox/SecondaryCheckboxType.jsx +++ b/src/js/components/sharedComponents/checkbox/SecondaryCheckboxType.jsx @@ -7,6 +7,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { uniqueId } from 'lodash'; +import Analytics from 'helpers/analytics/Analytics'; + const propTypes = { id: PropTypes.string, code: PropTypes.string, @@ -24,11 +26,9 @@ const defaultProps = { enableAnalytics: false }; -const ga = require('react-ga'); - export default class SecondaryCheckboxType extends React.Component { static logSecondaryTypeFilterEvent(type, filter) { - ga.event({ + Analytics.event({ category: 'Search Page Filter Applied', action: `Selected Secondary ${filter} Type`, label: type @@ -36,7 +36,7 @@ export default class SecondaryCheckboxType extends React.Component { } static logDeselectFilterEvent(type, filter) { - ga.event({ + Analytics.event({ category: 'Search Page Filter Applied', action: `Deselected Secondary ${filter} Type`, label: type diff --git a/src/js/components/sharedComponents/checkbox/SingleCheckboxType.jsx b/src/js/components/sharedComponents/checkbox/SingleCheckboxType.jsx index 893063885b..174c53728b 100644 --- a/src/js/components/sharedComponents/checkbox/SingleCheckboxType.jsx +++ b/src/js/components/sharedComponents/checkbox/SingleCheckboxType.jsx @@ -7,6 +7,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { uniqueId } from 'lodash'; +import Analytics from 'helpers/analytics/Analytics'; + const propTypes = { id: PropTypes.string, code: PropTypes.string, @@ -23,11 +25,9 @@ const defaultProps = { enableAnalytics: false }; -const ga = require('react-ga'); - export default class SingleCheckboxType extends React.Component { static logSingleTypeFilterEvent(type, filter) { - ga.event({ + Analytics.event({ category: 'Search Page Filter Applied', action: `Selected ${filter} Type`, label: type @@ -35,7 +35,7 @@ export default class SingleCheckboxType extends React.Component { } static logDeselectSingleTypeFilterEvent(type, filter) { - ga.event({ + Analytics.event({ category: 'Search Page Filter Applied', action: `Deselected ${filter} Type`, label: type diff --git a/src/js/components/sharedComponents/filterSidebar/FilterOption.jsx b/src/js/components/sharedComponents/filterSidebar/FilterOption.jsx index f3931dc7d0..dd2b293236 100644 --- a/src/js/components/sharedComponents/filterSidebar/FilterOption.jsx +++ b/src/js/components/sharedComponents/filterSidebar/FilterOption.jsx @@ -6,6 +6,7 @@ import React from 'react'; import PropTypes from 'prop-types'; +import Analytics from 'helpers/analytics/Analytics'; import ComingSoonLabel from 'components/sharedComponents/ComingSoonLabel'; import FilterExpandButton from './FilterExpandButton'; @@ -22,11 +23,9 @@ const defaultProps = { defaultExpand: true }; -const ga = require('react-ga'); - export default class FilterOption extends React.Component { static logFilterEvent(name) { - ga.event({ + Analytics.event({ category: 'Search Filters', action: 'Expanded Filter', label: name diff --git a/src/js/components/sharedComponents/header/NavBarGlossaryLink.jsx b/src/js/components/sharedComponents/header/NavBarGlossaryLink.jsx index 8aa1d86a33..9940ca38b6 100644 --- a/src/js/components/sharedComponents/header/NavBarGlossaryLink.jsx +++ b/src/js/components/sharedComponents/header/NavBarGlossaryLink.jsx @@ -6,17 +6,17 @@ import React from 'react'; import PropTypes from 'prop-types'; +import Analytics from 'helpers/analytics/Analytics'; + import { Glossary } from '../icons/Icons'; const propTypes = { toggleGlossary: PropTypes.func }; -const ga = require('react-ga'); - export default class NavBarGlossaryLink extends React.Component { static logGlossaryButtonEvent() { - ga.event({ + Analytics.event({ category: 'Glossary', action: 'Opened Glossary', label: 'Nav Bar Glossary Link' diff --git a/src/js/containers/router/RouterContainer.jsx b/src/js/containers/router/RouterContainer.jsx index 7ccc31be70..675c00c306 100644 --- a/src/js/containers/router/RouterContainer.jsx +++ b/src/js/containers/router/RouterContainer.jsx @@ -4,18 +4,16 @@ **/ import React from 'react'; -import kGlobalConstants from 'GlobalConstants'; + +import Analytics from 'helpers/analytics/Analytics'; import GlossaryListenerSingleton from './GlossaryListenerSingleton'; import Router from './Router'; -const ga = require('react-ga'); - -const GA_OPTIONS = { debug: false }; export default class RouterContainer extends React.Component { static logPageView(path) { - ga.pageview(path); + Analytics.pageview(path); } constructor(props) { @@ -41,13 +39,6 @@ export default class RouterContainer extends React.Component { Router.startRouter(); } - componentDidMount() { - // don't initialize Google Analytics if no tracking ID is provided - if (kGlobalConstants.GA_TRACKING_ID !== '') { - ga.initialize(kGlobalConstants.GA_TRACKING_ID, GA_OPTIONS); - } - } - componentWillUnmount() { Router.reactContainer = null; } @@ -60,7 +51,6 @@ export default class RouterContainer extends React.Component { if (this.state.lastPath !== path) { // log with Google Analytics RouterContainer.logPageView(path); - ga.pageview(window.location.hash); if (this.state.lastParent !== parent || !Router.state.silentlyUpdate) { // scroll to top of page, but only if the parent has changed (ignore in-page URL changes) diff --git a/src/js/containers/search/filters/AgencyContainer.jsx b/src/js/containers/search/filters/AgencyContainer.jsx index 6d3e338050..07c7e2c016 100644 --- a/src/js/containers/search/filters/AgencyContainer.jsx +++ b/src/js/containers/search/filters/AgencyContainer.jsx @@ -9,6 +9,8 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { is } from 'immutable'; +import Analytics from 'helpers/analytics/Analytics'; + import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; import Agency from 'components/search/filters/agency/Agency'; @@ -22,11 +24,9 @@ const propTypes = { appliedAwardingAgencies: PropTypes.object }; -const ga = require('react-ga'); - export class AgencyContainer extends React.Component { static logAgencyFilterEvent(agencyType, agency) { - ga.event({ + Analytics.event({ category: 'Search Page Filter Applied', action: `Applied ${agencyType} Agency Filter`, label: agency.toLowerCase() diff --git a/src/js/containers/search/filters/KeywordContainer.jsx b/src/js/containers/search/filters/KeywordContainer.jsx index b708f502bd..4f5b22036c 100644 --- a/src/js/containers/search/filters/KeywordContainer.jsx +++ b/src/js/containers/search/filters/KeywordContainer.jsx @@ -9,6 +9,8 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { is } from 'immutable'; +import Analytics from 'helpers/analytics/Analytics'; + import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; import Keyword from 'components/search/filters/keyword/Keyword'; @@ -19,11 +21,9 @@ const propTypes = { updateTextSearchInput: PropTypes.func }; -const ga = require('react-ga'); - export class KeywordContainer extends React.Component { static logSelectedKeywordEvent(keyword) { - ga.event({ + Analytics.event({ category: 'Search Page Filter Applied', action: 'Applied Keyword Filter', label: keyword diff --git a/src/js/containers/search/filters/awardID/AwardIDSearchContainer.jsx b/src/js/containers/search/filters/awardID/AwardIDSearchContainer.jsx index 8e4346f52d..bb487f2c23 100644 --- a/src/js/containers/search/filters/awardID/AwardIDSearchContainer.jsx +++ b/src/js/containers/search/filters/awardID/AwardIDSearchContainer.jsx @@ -9,6 +9,8 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { OrderedMap, is } from 'immutable'; +import Analytics from 'helpers/analytics/Analytics'; + import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; import AwardIDSearch from 'components/search/filters/awardID/AwardIDSearch'; @@ -19,11 +21,9 @@ const propTypes = { updateGenericFilter: PropTypes.func }; -const ga = require('react-ga'); - export class AwardIDSearchContainer extends React.Component { static logIdEvent(id, type) { - ga.event({ + Analytics.event({ category: 'Search Page Filter Applied', action: `Toggled Award ${type} Filter`, label: id diff --git a/src/js/containers/search/filters/cfda/CFDASearchContainer.jsx b/src/js/containers/search/filters/cfda/CFDASearchContainer.jsx index faa2c11136..7bed9150aa 100644 --- a/src/js/containers/search/filters/cfda/CFDASearchContainer.jsx +++ b/src/js/containers/search/filters/cfda/CFDASearchContainer.jsx @@ -9,6 +9,8 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { is } from 'immutable'; +import Analytics from 'helpers/analytics/Analytics'; + import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; import CFDASearch from 'components/search/filters/cfda/CFDASearch'; @@ -19,11 +21,9 @@ const propTypes = { updateSelectedCFDA: PropTypes.func }; -const ga = require('react-ga'); - export class CFDASearchContainer extends React.Component { static logCFDAFilterEvent(place) { - ga.event({ + Analytics.event({ category: 'Search Page Filter Applied', action: `Applied CFDA Filter`, label: place.toLowerCase() diff --git a/src/js/containers/search/filters/location/POPFilterContainer.jsx b/src/js/containers/search/filters/location/POPFilterContainer.jsx index d3913cee79..4c4778ec2f 100644 --- a/src/js/containers/search/filters/location/POPFilterContainer.jsx +++ b/src/js/containers/search/filters/location/POPFilterContainer.jsx @@ -8,13 +8,13 @@ import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; +import Analytics from 'helpers/analytics/Analytics'; + import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; import SelectedLocations from 'components/search/filters/location/SelectedLocations'; import LocationPickerContainer from './LocationPickerContainer'; -const ga = require('react-ga'); - const propTypes = { addPOPLocationObject: PropTypes.func, updateGenericFilter: PropTypes.func, @@ -23,7 +23,7 @@ const propTypes = { export class POPFilterContainer extends React.Component { static logLocationFilterEvent(label, event) { - ga.event({ + Analytics.event({ label, category: 'Search Page Filter Applied', action: `${event} Place of Performance Location Filter` diff --git a/src/js/containers/search/filters/location/RecipientFilterContainer.jsx b/src/js/containers/search/filters/location/RecipientFilterContainer.jsx index a9ccc0b6ed..f5632c8bfe 100644 --- a/src/js/containers/search/filters/location/RecipientFilterContainer.jsx +++ b/src/js/containers/search/filters/location/RecipientFilterContainer.jsx @@ -8,13 +8,13 @@ import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; +import Analytics from 'helpers/analytics/Analytics'; + import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; import SelectedLocations from 'components/search/filters/location/SelectedLocations'; import LocationPickerContainer from './LocationPickerContainer'; -const ga = require('react-ga'); - const propTypes = { addRecipientLocationObject: PropTypes.func, updateGenericFilter: PropTypes.func, @@ -23,7 +23,7 @@ const propTypes = { export class RecipientFilterContainer extends React.Component { static logLocationFilterEvent(label, event) { - ga.event({ + Analytics.event({ label, category: 'Search Page Filter Applied', action: `${event} Recipient Location Filter` diff --git a/src/js/containers/search/filters/naics/NAICSSearchContainer.jsx b/src/js/containers/search/filters/naics/NAICSSearchContainer.jsx index 1f68b06752..bbba49ba1c 100644 --- a/src/js/containers/search/filters/naics/NAICSSearchContainer.jsx +++ b/src/js/containers/search/filters/naics/NAICSSearchContainer.jsx @@ -9,6 +9,8 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { is } from 'immutable'; +import Analytics from 'helpers/analytics/Analytics'; + import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; import NAICSSearch from 'components/search/filters/naics/NAICSSearch'; @@ -19,11 +21,9 @@ const propTypes = { appliedNAICS: PropTypes.object }; -const ga = require('react-ga'); - export class NAICSSearchContainer extends React.Component { static logPlaceFilterEvent(naics) { - ga.event({ + Analytics.event({ category: 'Search Page Filter Applied', action: `Applied NAICS Filter`, label: naics.toLowerCase() diff --git a/src/js/containers/search/filters/psc/PSCSearchContainer.jsx b/src/js/containers/search/filters/psc/PSCSearchContainer.jsx index 3eeb2ce2b3..9cb647dbe9 100644 --- a/src/js/containers/search/filters/psc/PSCSearchContainer.jsx +++ b/src/js/containers/search/filters/psc/PSCSearchContainer.jsx @@ -9,6 +9,8 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { is } from 'immutable'; +import Analytics from 'helpers/analytics/Analytics'; + import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; import PSCSearch from 'components/search/filters/psc/PSCSearch'; @@ -19,11 +21,9 @@ const propTypes = { appliedPSC: PropTypes.object }; -const ga = require('react-ga'); - export class PSCSearchContainer extends React.Component { static logPSCFilterEvent(psc) { - ga.event({ + Analytics.event({ category: 'Search Page Filter Applied', action: `Applied PSC Filter`, label: psc diff --git a/src/js/containers/search/filters/recipient/RecipientSearchContainer.jsx b/src/js/containers/search/filters/recipient/RecipientSearchContainer.jsx index c70e8c0d62..08226d4dc5 100644 --- a/src/js/containers/search/filters/recipient/RecipientSearchContainer.jsx +++ b/src/js/containers/search/filters/recipient/RecipientSearchContainer.jsx @@ -9,6 +9,8 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { is } from 'immutable'; +import Analytics from 'helpers/analytics/Analytics'; + import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; import RecipientSearch from 'components/search/filters/recipient/RecipientSearch'; @@ -19,11 +21,9 @@ const propTypes = { appliedRecipients: PropTypes.object }; -const ga = require('react-ga'); - export class RecipientSearchContainer extends React.Component { static logRecipientFilterEvent(name) { - ga.event({ + Analytics.event({ category: 'Search Page Filter Applied', action: 'Applied Recipient Name/DUNS Filter', label: name.toLowerCase() diff --git a/src/js/containers/search/filters/recipient/RecipientTypeContainer.jsx b/src/js/containers/search/filters/recipient/RecipientTypeContainer.jsx index 01a26e0b7d..018e864b37 100644 --- a/src/js/containers/search/filters/recipient/RecipientTypeContainer.jsx +++ b/src/js/containers/search/filters/recipient/RecipientTypeContainer.jsx @@ -12,6 +12,7 @@ import { connect } from 'react-redux'; import { keyBy } from 'lodash'; import { recipientTypeGroups } from 'dataMapping/search/recipientType'; +import Analytics from 'helpers/analytics/Analytics'; import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; @@ -24,11 +25,9 @@ const propTypes = { appliedType: PropTypes.object }; -const ga = require('react-ga'); - export class RecipientTypeContainer extends React.Component { static logLocationFilterEvent(placeType, place, event) { - ga.event({ + Analytics.event({ category: 'Search Page Filter Applied', action: `${event} Recipient ${placeType.toLowerCase()} Filter`, label: place.toLowerCase() diff --git a/src/js/containers/search/table/ResultsTableContainer.jsx b/src/js/containers/search/table/ResultsTableContainer.jsx index c3a6a3c766..80228692e8 100644 --- a/src/js/containers/search/table/ResultsTableContainer.jsx +++ b/src/js/containers/search/table/ResultsTableContainer.jsx @@ -12,6 +12,7 @@ import { uniqueId, difference, intersection } from 'lodash'; import SearchAwardsOperation from 'models/search/SearchAwardsOperation'; import * as SearchHelper from 'helpers/searchHelper'; +import Analytics from 'helpers/analytics/Analytics'; import { awardTypeGroups } from 'dataMapping/search/awardType'; @@ -63,8 +64,6 @@ const tableTypes = [ } ]; -const ga = require('react-ga'); - export class ResultsTableContainer extends React.Component { static logLoadNextPageEvent(page, tableType) { // Get the display name for the current table type @@ -73,7 +72,7 @@ export class ResultsTableContainer extends React.Component { ); const typeLabel = currentType[0].label; - ga.event({ + Analytics.event({ category: 'Search Page Spending By Award', action: `Scrolled to next page of ${typeLabel}`, label: page diff --git a/src/js/helpers/analytics/Analytics.js b/src/js/helpers/analytics/Analytics.js new file mode 100644 index 0000000000..c938c9b467 --- /dev/null +++ b/src/js/helpers/analytics/Analytics.js @@ -0,0 +1,41 @@ + +const Analytics = { + _lib(...args) { + if (window.gas && typeof window.gas === 'function') { + return window.gas(...args); + } + else if (window.ga && typeof window.gas === 'function') { + return window.ga(...args); + } + return null; + }, + event(args) { + if (!args.category || !args.action) { + return; + } + this._lib( + 'send', + 'event', + args.category, + args.action, + args.label || undefined, + args.value || undefined, + args.noninteraction || undefined + ); + }, + pageview(args) { + let path = args; + let title; + if (typeof args === 'object') { + ({ path, title } = args); + } + this._lib( + 'send', + 'pageview', + path, + title + ); + } +}; + +export default Analytics; From 8e4b0c97e2831ed51c16e723484014fad455ca2a Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Fri, 2 Feb 2018 16:53:36 -0500 Subject: [PATCH 22/89] Update analytics obj --- src/js/helpers/analytics/Analytics.js | 30 ++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/js/helpers/analytics/Analytics.js b/src/js/helpers/analytics/Analytics.js index c938c9b467..6b9c44178b 100644 --- a/src/js/helpers/analytics/Analytics.js +++ b/src/js/helpers/analytics/Analytics.js @@ -1,26 +1,39 @@ +/** + * Analytics.js + * Created by Kevin Li 2/2/18 + */ + const Analytics = { - _lib(...args) { - if (window.gas && typeof window.gas === 'function') { + _prefix: 'USAspending - ', + _execute(...args) { + if (this.isDAP) { return window.gas(...args); } - else if (window.ga && typeof window.gas === 'function') { + else if (this.GA) { return window.ga(...args); } + // fall back if no library is loaded (most likely due to adblocking) return null; }, + get isDAP() { + return Boolean(window.gas && typeof window.gas === 'function'); + }, + get isGA() { + return Boolean(!this.isDAP && window.ga && typeof window.ga === 'function'); + }, event(args) { if (!args.category || !args.action) { return; } - this._lib( + this._execute( 'send', 'event', - args.category, + `${this._prefix}${args.category}`, args.action, args.label || undefined, args.value || undefined, - args.noninteraction || undefined + args.nonInteraction || undefined ); }, pageview(args) { @@ -29,7 +42,7 @@ const Analytics = { if (typeof args === 'object') { ({ path, title } = args); } - this._lib( + this._execute( 'send', 'pageview', path, @@ -38,4 +51,7 @@ const Analytics = { } }; +// no hack approaches allowed +Object.freeze(Analytics); + export default Analytics; From d3e49bed8d3133a25d33309126203097499dc445 Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Fri, 2 Feb 2018 17:20:10 -0500 Subject: [PATCH 23/89] Adds a generic Sticky Header that replaces Search Header and Keyword Header, converts Sticky Header CSS to BEM, replaces all instances of "Bulk Download" with "Download Center", limits Download Center functionality to a year's worth of data, removes the "All Time" option in Download Center, fixes lingering bug with Date Pickers failing to open to a monthly view with the selected date in non-current years --- .../stickyHeader}/header.scss | 14 +-- .../layouts/tabbedSearch/tabbedSearch.scss | 2 +- .../pages/bulkDownload/bulkDownloadPage.scss | 4 +- src/_scss/pages/keyword/header/header.scss | 26 +++-- .../bulkDownload/BulkDownloadPage.jsx | 52 ++++----- .../filters/dateRange/DownloadDateRange.jsx | 4 +- .../filters/dateRange/TimePeriodFilter.jsx | 33 +++--- .../dateRange/buttons/DateRangeButton.jsx | 4 +- .../dateRange/buttons/FiscalYearButton.jsx | 4 +- .../bulkDownload/modal/BulkDownloadModal.jsx | 2 +- src/js/components/keyword/KeywordPage.jsx | 77 +++++++++++-- .../keyword/header/KeywordHeader.jsx | 105 ------------------ src/js/components/search/SearchPage.jsx | 18 ++- .../search/header/NoDownloadHover.jsx | 2 +- .../sharedComponents/DatePicker.jsx | 22 +++- .../stickyHeader/StickyHeader.jsx} | 34 ++---- .../bulkDownload/bulkDownloadOptions.js | 5 - 17 files changed, 195 insertions(+), 213 deletions(-) rename src/_scss/layouts/{tabbedSearch/header => default/stickyHeader}/header.scss (81%) delete mode 100644 src/js/components/keyword/header/KeywordHeader.jsx rename src/js/components/{search/header/SearchHeader.jsx => sharedComponents/stickyHeader/StickyHeader.jsx} (66%) diff --git a/src/_scss/layouts/tabbedSearch/header/header.scss b/src/_scss/layouts/default/stickyHeader/header.scss similarity index 81% rename from src/_scss/layouts/tabbedSearch/header/header.scss rename to src/_scss/layouts/default/stickyHeader/header.scss index 804f9f894e..3ca04e1717 100644 --- a/src/_scss/layouts/tabbedSearch/header/header.scss +++ b/src/_scss/layouts/default/stickyHeader/header.scss @@ -1,18 +1,18 @@ $search-header-height: rem(66); -.search-header-wrapper { +.sticky-header__wrapper { position: relative; height: $search-header-height; } -.search-header-container { +.sticky-header__container { width: 100%; background-color: #4A4A4A; color: $color-white; // bottom shadow cast on the content box-shadow: 0 2px 2px rgba(0,0,0,.3); - &.sticky { + &.sticky-header__container_sticky { @include media($medium-screen) { position: fixed; top: 0; @@ -20,7 +20,7 @@ $search-header-height: rem(66); } } - .search-header { + .sticky-header { @import "mixins/fullSectionWrap"; @include fullSectionWrap(0, 0); @include display(flex); @@ -33,7 +33,7 @@ $search-header-height: rem(66); height: $search-header-height; - .search-title { + .sticky-header__title { @include flex(1 1 auto); h1 { font-size: rem(24); @@ -43,9 +43,9 @@ $search-header-height: rem(66); } } - .search-options { + .sticky-header__options { @include flex(0 0 auto); - @import "./_downloadButton"; + @import "../../tabbedSearch/header/downloadButton"; } } } \ No newline at end of file diff --git a/src/_scss/layouts/tabbedSearch/tabbedSearch.scss b/src/_scss/layouts/tabbedSearch/tabbedSearch.scss index c4139c24b2..4f3b18c9db 100644 --- a/src/_scss/layouts/tabbedSearch/tabbedSearch.scss +++ b/src/_scss/layouts/tabbedSearch/tabbedSearch.scss @@ -1,7 +1,7 @@ @import "layouts/default/default"; #main-content { - @import "./header/header"; + @import "../default/stickyHeader/header"; .search-contents { @include outer-container(100%); diff --git a/src/_scss/pages/bulkDownload/bulkDownloadPage.scss b/src/_scss/pages/bulkDownload/bulkDownloadPage.scss index 57c2360feb..e7a3b21718 100644 --- a/src/_scss/pages/bulkDownload/bulkDownloadPage.scss +++ b/src/_scss/pages/bulkDownload/bulkDownloadPage.scss @@ -1,9 +1,9 @@ .usa-da-bulk-download-page { @import "all"; @import "layouts/search/search"; - @import "./header/header"; + @import "../../layouts/default/stickyHeader/header"; - .main-content { + .bulk-download-content { @import "../../mixins/fullSectionWrap"; @include fullSectionWrap(($global-mrg * 4), ($global-mrg * 4)); padding: 0; diff --git a/src/_scss/pages/keyword/header/header.scss b/src/_scss/pages/keyword/header/header.scss index b3b7e04566..ab64847be3 100644 --- a/src/_scss/pages/keyword/header/header.scss +++ b/src/_scss/pages/keyword/header/header.scss @@ -1,39 +1,46 @@ -@import "layouts/tabbedSearch/header/header"; +@import "layouts/default/stickyHeader/header"; -.search-header-container { - .search-header { - .search-title { +.sticky-header__container { + .sticky-header { + .sticky-header__title { @include span-columns(8); @include media($medium-screen) { @include span-columns(4); } } - .search-summary { + + .sticky-header__summary { // Hide the search summary for small screens display: none; + font-size: $small-font-size; + @include media($medium-screen) { @include span-columns(8); @include display(flex); } - font-size: $small-font-size; - .summary-title { + + .sticky-header__summary_title { font-weight: $font-semibold; padding-right: rem(10); margin-right: rem(10); border-right: solid 1px $color-white; } - .award-amounts, .award-counts { + + .sticky-header__summary_award-amounts, + .sticky-header__summary_award-counts { margin-right: rem(20); .number { font-weight: $font-semibold; } } } - .search-options { + + .sticky-header__options { @include span-columns(8); @include media($medium-screen) { @include span-columns(4); } + .download-wrap { .download-hover-spacer { left: rem(-140); @@ -43,6 +50,7 @@ float: right; } } + &.no-hover { .download-wrap { .download-hover-spacer { diff --git a/src/js/components/bulkDownload/BulkDownloadPage.jsx b/src/js/components/bulkDownload/BulkDownloadPage.jsx index 9e945ad412..52bb1bf026 100644 --- a/src/js/components/bulkDownload/BulkDownloadPage.jsx +++ b/src/js/components/bulkDownload/BulkDownloadPage.jsx @@ -11,6 +11,7 @@ import Router from 'containers/router/Router'; import MetaTags from 'components/sharedComponents/metaTags/MetaTags'; import Header from 'components/sharedComponents/header/Header'; +import StickyHeader from 'components/sharedComponents/stickyHeader/StickyHeader'; import Footer from 'components/sharedComponents/Footer'; import AwardDataContainer from 'containers/bulkDownload/awards/AwardDataContainer'; @@ -111,34 +112,35 @@ export default class BulkDownloadPage extends React.Component {
    -
    -
    -

    - Bulk Download -

    -
    -
    -
    - -
    -
    Interested in our API?
    -

    - Take a look at our API documentation. -

    + id="main-content"> + +
    +

    + Download Center +

    +
    +
    +
    + +
    +
    Interested in our API?
    +

    + Take a look at our API documentation. +

    +
    +
    +
    + {downloadDataContent} +
    +
    -
    - {downloadDataContent} -
    -
    diff --git a/src/js/components/bulkDownload/awards/filters/dateRange/DownloadDateRange.jsx b/src/js/components/bulkDownload/awards/filters/dateRange/DownloadDateRange.jsx index 44c3cb9f39..4c046e0b25 100644 --- a/src/js/components/bulkDownload/awards/filters/dateRange/DownloadDateRange.jsx +++ b/src/js/components/bulkDownload/awards/filters/dateRange/DownloadDateRange.jsx @@ -46,7 +46,7 @@ export default class DownloadDateRange extends React.Component { return (
    @@ -221,8 +220,8 @@ export default class TimePeriodFilter extends React.Component {
    diff --git a/src/js/components/bulkDownload/awards/filters/dateRange/buttons/DateRangeButton.jsx b/src/js/components/bulkDownload/awards/filters/dateRange/buttons/DateRangeButton.jsx index 4620133dec..4747d8c1f0 100644 --- a/src/js/components/bulkDownload/awards/filters/dateRange/buttons/DateRangeButton.jsx +++ b/src/js/components/bulkDownload/awards/filters/dateRange/buttons/DateRangeButton.jsx @@ -24,8 +24,8 @@ export default class DateRangeButton extends React.Component { onClick(e) { e.preventDefault(); - this.props.handleDateChange(this.props.startDate, 'startDate'); - this.props.handleDateChange(this.props.endDate, 'endDate'); + this.props.handleDateChange(this.props.startDate, 'startDateBulk'); + this.props.handleDateChange(this.props.endDate, 'endDateBulk'); } render() { diff --git a/src/js/components/bulkDownload/awards/filters/dateRange/buttons/FiscalYearButton.jsx b/src/js/components/bulkDownload/awards/filters/dateRange/buttons/FiscalYearButton.jsx index f4e9498a50..8b1952e054 100644 --- a/src/js/components/bulkDownload/awards/filters/dateRange/buttons/FiscalYearButton.jsx +++ b/src/js/components/bulkDownload/awards/filters/dateRange/buttons/FiscalYearButton.jsx @@ -26,8 +26,8 @@ export default class FiscalYearButton extends React.Component { e.preventDefault(); const dates = fiscalYearHelper.convertFYToDateRange(this.props.year); - this.props.handleDateChange(dates[0], 'startDate'); - this.props.handleDateChange(dates[1], 'endDate'); + this.props.handleDateChange(dates[0], 'startDateBulk'); + this.props.handleDateChange(dates[1], 'endDateBulk'); } render() { diff --git a/src/js/components/bulkDownload/modal/BulkDownloadModal.jsx b/src/js/components/bulkDownload/modal/BulkDownloadModal.jsx index 6467fa1f18..24ab046828 100644 --- a/src/js/components/bulkDownload/modal/BulkDownloadModal.jsx +++ b/src/js/components/bulkDownload/modal/BulkDownloadModal.jsx @@ -40,7 +40,7 @@ export default class BulkDownloadModal extends React.Component { diff --git a/src/js/components/keyword/KeywordPage.jsx b/src/js/components/keyword/KeywordPage.jsx index 3dcfa84374..84c4199950 100644 --- a/src/js/components/keyword/KeywordPage.jsx +++ b/src/js/components/keyword/KeywordPage.jsx @@ -7,17 +7,19 @@ import React from 'react'; import PropTypes from 'prop-types'; import * as MetaTagHelper from 'helpers/metaTagHelper'; +import * as MoneyFormatter from 'helpers/moneyFormatter'; import { InfoCircle } from 'components/sharedComponents/icons/Icons'; import ResultsTableContainer from 'containers/keyword/table/ResultsTableContainer'; import BulkDownloadModalContainer from 'containers/bulkDownload/modal/BulkDownloadModalContainer'; +import DownloadButton from 'components/search/header/DownloadButton'; import MetaTags from '../sharedComponents/metaTags/MetaTags'; import Header from '../sharedComponents/header/Header'; +import StickyHeader from '../sharedComponents/stickyHeader/StickyHeader'; import Footer from '../sharedComponents/Footer'; -import KeywordHeader from './header/KeywordHeader'; import KeywordSearchBar from './KeywordSearchBar'; import KeywordSearchHover from './KeywordSearchHover'; @@ -85,24 +87,85 @@ export default class KeywordPage extends React.Component { }); } + generateSummary() { + let formattedPrimeCount = ( — ); + let formattedPrimeAmount = ( — ); + if (!this.props.summaryInFlight) { + const primeCount = this.props.summary.primeCount; + const primeAmount = this.props.summary.primeAmount; + + const primeCountUnits = MoneyFormatter.calculateUnitForSingleValue(primeCount); + const primeAmountUnits = MoneyFormatter.calculateUnitForSingleValue(primeAmount); + + if (primeCountUnits.unit >= MoneyFormatter.unitValues.MILLION) { + // Abbreviate numbers greater than or equal to 1M + formattedPrimeCount = + `${MoneyFormatter.formatNumberWithPrecision(primeCount / primeCountUnits.unit, 1)}${primeCountUnits.unitLabel}`; + } + else { + formattedPrimeCount = + `${MoneyFormatter.formatNumberWithPrecision(primeCount, 0)}`; + } + + if (primeAmountUnits.unit >= MoneyFormatter.unitValues.MILLION) { + // Abbreviate amounts greater than or equal to $1M + formattedPrimeAmount = + `${MoneyFormatter.formatMoneyWithPrecision(primeAmount / primeAmountUnits.unit, 1)}${primeAmountUnits.unitLabel}`; + } + else { + formattedPrimeAmount = + `${MoneyFormatter.formatMoneyWithPrecision(primeAmount, 0)}`; + } + } + + return ( +
    +
    + Search Summary +
    +
    +
    + Total Prime Award Amount: {formattedPrimeAmount} +
    +
    +
    +
    + Prime Award Transaction Count: {formattedPrimeCount} +
    +
    +
    + ); + } + render() { let hover = null; if (this.state.showHover) { hover = (); } + + let searchSummary = null; + if (this.props.summary || this.props.summaryInFlight) { + searchSummary = this.generateSummary(); + } + return (
    - + +
    +

    Keyword Search

    +
    + {searchSummary} +
    + +
    +
     — ); - let formattedPrimeAmount = ( — ); - if (!this.props.inFlight) { - const primeCount = this.props.summary.primeCount; - const primeAmount = this.props.summary.primeAmount; - - const primeCountUnits = MoneyFormatter.calculateUnitForSingleValue(primeCount); - const primeAmountUnits = MoneyFormatter.calculateUnitForSingleValue(primeAmount); - - if (primeCountUnits.unit >= MoneyFormatter.unitValues.MILLION) { - // Abbreviate numbers greater than or equal to 1M - formattedPrimeCount = - `${MoneyFormatter.formatNumberWithPrecision(primeCount / primeCountUnits.unit, 1)}${primeCountUnits.unitLabel}`; - } - else { - formattedPrimeCount = - `${MoneyFormatter.formatNumberWithPrecision(primeCount, 0)}`; - } - - if (primeAmountUnits.unit >= MoneyFormatter.unitValues.MILLION) { - // Abbreviate amounts greater than or equal to $1M - formattedPrimeAmount = - `${MoneyFormatter.formatMoneyWithPrecision(primeAmount / primeAmountUnits.unit, 1)}${primeAmountUnits.unitLabel}`; - } - else { - formattedPrimeAmount = - `${MoneyFormatter.formatMoneyWithPrecision(primeAmount, 0)}`; - } - } - - return ( -
    -
    - Search Summary -
    -
    -
    - Total Prime Award Amount: {formattedPrimeAmount} -
    -
    -
    -
    - Prime Award Transaction Count: {formattedPrimeCount} -
    -
    -
    - ); - } - - render() { - let searchSummary = null; - if (this.props.summary || this.props.inFlight) { - searchSummary = this.generateSummary(); - } - return ( -
    -
    -
    -
    -

    Keyword Search

    -
    - {searchSummary} -
    - -
    -
    -
    -
    - ); - } -} - - -KeywordHeader.propTypes = propTypes; - -export default KeywordHeader; diff --git a/src/js/components/search/SearchPage.jsx b/src/js/components/search/SearchPage.jsx index f88ba8f81f..76df24f369 100644 --- a/src/js/components/search/SearchPage.jsx +++ b/src/js/components/search/SearchPage.jsx @@ -11,11 +11,12 @@ import * as MetaTagHelper from 'helpers/metaTagHelper'; import FullDownloadModalContainer from 'containers/search/modals/fullDownload/FullDownloadModalContainer'; +import DownloadButton from 'components/search/header/DownloadButton'; import MetaTags from '../sharedComponents/metaTags/MetaTags'; import Header from '../sharedComponents/header/Header'; +import StickyHeader from '../sharedComponents/stickyHeader/StickyHeader'; import Footer from '../sharedComponents/Footer'; -import SearchHeader from './header/SearchHeader'; import SearchSidebar from './SearchSidebar'; import SearchResults from './SearchResults'; @@ -137,9 +138,18 @@ export default class SearchPage extends React.Component {
    - + +
    +

    + Advanced Search +

    +
    +
    + +
    +
    { fullSidebar } diff --git a/src/js/components/search/header/NoDownloadHover.jsx b/src/js/components/search/header/NoDownloadHover.jsx index 937688d6df..d63f84db2c 100644 --- a/src/js/components/search/header/NoDownloadHover.jsx +++ b/src/js/components/search/header/NoDownloadHover.jsx @@ -18,7 +18,7 @@ const NoDownloadHover = () => (
    - Please visit the Bulk Download page to export + Please visit the Download Center page to export more than 500,000 records or limit your results with additional filters.
    diff --git a/src/js/components/sharedComponents/DatePicker.jsx b/src/js/components/sharedComponents/DatePicker.jsx index 80e5a4ee2e..a6a8a37bf6 100644 --- a/src/js/components/sharedComponents/DatePicker.jsx +++ b/src/js/components/sharedComponents/DatePicker.jsx @@ -227,6 +227,26 @@ export default class DatePicker extends React.Component { before: this.props.opposite.toDate() }); } + else if (this.props.type === 'startDateBulk' && this.props.opposite) { + // Cutoff date represents the latest possible date + // We only want users to be able to download 1 year's worth of data at a time, + // So we set the start date a year before the end date + // This requires adding a day after subtracting a year + disabledDays.push({ + after: this.props.opposite.toDate(), + before: moment(this.props.opposite).subtract(1, 'y').add(1, 'd').toDate() + }); + } + else if (this.props.type === 'endDateBulk' && this.props.opposite) { + // Cutoff date represents the earliest possible date, based on the start date + // We only want users to be able to download 1 year's worth of data at a time, + // So we set the end date a year after the start date + // This requires subtracting a day after adding a year + disabledDays.push({ + before: this.props.opposite.toDate(), + after: moment(this.props.opposite).add(1, 'y').subtract(1, 'd').toDate() + }); + } else if (!this.props.value) { disabledDays = []; } @@ -263,7 +283,7 @@ export default class DatePicker extends React.Component { ref={(daypicker) => { this.datepicker = daypicker; }} - initialMonth={pickedDay} + month={pickedDay} disabledDays={disabledDays} selectedDays={(day) => DateUtils.isSameDay(pickedDay, day)} onDayClick={this.handleDatePick} diff --git a/src/js/components/search/header/SearchHeader.jsx b/src/js/components/sharedComponents/stickyHeader/StickyHeader.jsx similarity index 66% rename from src/js/components/search/header/SearchHeader.jsx rename to src/js/components/sharedComponents/stickyHeader/StickyHeader.jsx index a5aad9b102..098d8ab526 100644 --- a/src/js/components/search/header/SearchHeader.jsx +++ b/src/js/components/sharedComponents/stickyHeader/StickyHeader.jsx @@ -1,19 +1,18 @@ /** - * SearchHeader.jsx - * Created by Kevin Li 11/10/16 - **/ + * StickyHeader.jsx + * Created by Mike Bray 02/02/2018 + **/ import React from 'react'; import PropTypes from 'prop-types'; -import DownloadButton from './DownloadButton'; - const propTypes = { showDownloadModal: PropTypes.func, - downloadAvailable: PropTypes.bool + downloadAvailable: PropTypes.bool, + children: PropTypes.node }; -export default class SearchHeader extends React.Component { +export default class StickyHeader extends React.Component { constructor(props) { super(props); @@ -65,33 +64,24 @@ export default class SearchHeader extends React.Component { render() { let stickyClass = ''; if (this.state.isSticky) { - stickyClass = 'sticky'; + stickyClass = 'sticky-header__container_sticky'; } return (
    { this.wrapper = div; }}>
    { this.content = div; }}>
    -
    -

    - Advanced Search -

    -
    -
    - -
    + {this.props.children}
    @@ -99,4 +89,4 @@ export default class SearchHeader extends React.Component { } } -SearchHeader.propTypes = propTypes; +StickyHeader.propTypes = propTypes; diff --git a/src/js/dataMapping/bulkDownload/bulkDownloadOptions.js b/src/js/dataMapping/bulkDownload/bulkDownloadOptions.js index b69bfb896e..b9232b58bc 100644 --- a/src/js/dataMapping/bulkDownload/bulkDownloadOptions.js +++ b/src/js/dataMapping/bulkDownload/bulkDownloadOptions.js @@ -122,11 +122,6 @@ export const awardDownloadOptions = { label: 'last year', startDate: moment().subtract(1, 'year').startOf('year').format('YYYY-MM-DD'), endDate: moment().subtract(1, 'year').endOf('year').format('YYYY-MM-DD') - }, - { - label: 'all time', - startDate: '', - endDate: moment().format('YYYY-MM-DD') } ] } From 53ac57a8213faa6b424a12295cd89fabe243ebdc Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Mon, 5 Feb 2018 12:38:14 -0500 Subject: [PATCH 24/89] Changed names of Award Data and Account Data to Custom Award Data and Custom Account Data (respectively), added a new Award Data Archive link to the top of the Download Center sidebar, validated keyed-in dates are at most 1 year apart, added a new error message for the previous error instance --- .../bulkDownload/BulkDownloadPage.jsx | 10 +++++-- .../filters/dateRange/TimePeriodFilter.jsx | 26 ++++++++++++++++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/js/components/bulkDownload/BulkDownloadPage.jsx b/src/js/components/bulkDownload/BulkDownloadPage.jsx index 52bb1bf026..4142cd2e91 100644 --- a/src/js/components/bulkDownload/BulkDownloadPage.jsx +++ b/src/js/components/bulkDownload/BulkDownloadPage.jsx @@ -21,15 +21,21 @@ import BulkDownloadModalContainer from import BulkDownloadSidebar from './sidebar/BulkDownloadSidebar'; export const dataTypes = [ + { + type: 'award_data_archive', + label: 'Award Data Archive', + disabled: false, + url: '' + }, { type: 'awards', - label: 'Award Data', + label: 'Custom Award Data', disabled: false, url: '' }, { type: 'accounts', - label: 'Account Data', + label: 'Custom Account Data', disabled: true, url: '' }, diff --git a/src/js/components/bulkDownload/awards/filters/dateRange/TimePeriodFilter.jsx b/src/js/components/bulkDownload/awards/filters/dateRange/TimePeriodFilter.jsx index 4ca540d82d..60522f46fd 100644 --- a/src/js/components/bulkDownload/awards/filters/dateRange/TimePeriodFilter.jsx +++ b/src/js/components/bulkDownload/awards/filters/dateRange/TimePeriodFilter.jsx @@ -21,6 +21,17 @@ const propTypes = { setValidDates: PropTypes.func }; +const errorTypes = { + order: { + title: 'Invalid Dates', + message: 'The end date cannot be earlier than the start date.' + }, + range: { + title: 'Invalid Date Range', + message: 'The date range entered must be 1 year or less.' + } +}; + export default class TimePeriodFilter extends React.Component { constructor(props) { super(props); @@ -121,18 +132,27 @@ export default class TimePeriodFilter extends React.Component { validateDates() { // validate that dates are provided for both fields and the end dates - // don't come before the start dates + // don't come before the start dates, and that the range is less than one year // validate the date ranges const start = this.state.startDateBulkUI; const end = this.state.endDateBulkUI; + + const yearBeforeEnd = moment(this.state.endDateBulkUI).subtract(1, 'y').add(1, 'd'); + if (start && end) { // both sets of dates exist if (!end.isSameOrAfter(start)) { // end date comes before start date, invalid // show an error message - this.showError('Invalid Dates', - 'The end date cannot be earlier than the start date.'); + const error = errorTypes.order; + this.showError(error.title, error.message); + } + else if (!start.isSameOrAfter(yearBeforeEnd)) { + // Start date is more than one year before the end date + // show an error message + const error = errorTypes.range; + this.showError(error.title, error.message); } else { // valid! From 75ba899ac66076a6e28f4eb796f9a1f0a4e25651 Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Mon, 5 Feb 2018 14:10:11 -0500 Subject: [PATCH 25/89] Addressing PR comments with BEM and disabledDate logic --- .../layouts/default/stickyHeader/header.scss | 74 ++++++------ src/_scss/pages/keyword/header/header.scss | 111 +++++++++--------- .../filters/dateRange/DownloadDateRange.jsx | 40 +++++++ .../filters/dateRange/TimePeriodFilter.jsx | 4 +- src/js/components/keyword/KeywordPage.jsx | 20 ++-- .../search/filters/timePeriod/DateRange.jsx | 40 ++++++- .../sharedComponents/DatePicker.jsx | 41 +------ .../stickyHeader/StickyHeader.jsx | 4 +- 8 files changed, 184 insertions(+), 150 deletions(-) diff --git a/src/_scss/layouts/default/stickyHeader/header.scss b/src/_scss/layouts/default/stickyHeader/header.scss index 3ca04e1717..25b1ed3cee 100644 --- a/src/_scss/layouts/default/stickyHeader/header.scss +++ b/src/_scss/layouts/default/stickyHeader/header.scss @@ -1,51 +1,51 @@ $search-header-height: rem(66); -.sticky-header__wrapper { +.sticky-header { position: relative; height: $search-header-height; -} -.sticky-header__container { - width: 100%; - background-color: #4A4A4A; - color: $color-white; - // bottom shadow cast on the content - box-shadow: 0 2px 2px rgba(0,0,0,.3); + .sticky-header__container { + width: 100%; + background-color: #4a4a4a; + color: $color-white; + // bottom shadow cast on the content + box-shadow: 0 2px 2px rgba(0, 0, 0, .3); - &.sticky-header__container_sticky { - @include media($medium-screen) { - position: fixed; - top: 0; - z-index: $z-header; + &.sticky-header__container_sticky { + @include media($medium-screen) { + position: fixed; + top: 0; + z-index: $z-header; + } } - } - .sticky-header { - @import "mixins/fullSectionWrap"; - @include fullSectionWrap(0, 0); - @include display(flex); - @include justify-content(space-between); - @include flex-direction(row); - @include align-items(center); - @include align-self(stretch); - @include flex-flow(row wrap); - position: relative; + .sticky-header__header { + @import "mixins/fullSectionWrap"; + @include fullSectionWrap(0, 0); + @include display(flex); + @include justify-content(space-between); + @include flex-direction(row); + @include align-items(center); + @include align-self(stretch); + @include flex-flow(row wrap); + position: relative; - height: $search-header-height; + height: $search-header-height; - .sticky-header__title { - @include flex(1 1 auto); - h1 { - font-size: rem(24); - line-height: rem(31); - font-weight: $font-semibold; - margin: 0; + .sticky-header__title { + @include flex(1 1 auto); + h1 { + font-size: rem(24); + line-height: rem(31); + font-weight: $font-semibold; + margin: 0; + } } - } - .sticky-header__options { - @include flex(0 0 auto); - @import "../../tabbedSearch/header/downloadButton"; + .sticky-header__options { + @include flex(0 0 auto); + @import "../../tabbedSearch/header/downloadButton"; + } } } -} \ No newline at end of file +} diff --git a/src/_scss/pages/keyword/header/header.scss b/src/_scss/pages/keyword/header/header.scss index ab64847be3..227e9246e0 100644 --- a/src/_scss/pages/keyword/header/header.scss +++ b/src/_scss/pages/keyword/header/header.scss @@ -1,66 +1,71 @@ @import "layouts/default/stickyHeader/header"; -.sticky-header__container { - .sticky-header { - .sticky-header__title { - @include span-columns(8); - @include media($medium-screen) { - @include span-columns(4); - } - } +.keyword-header__title { + @include flex(1 1 auto); + @include span-columns(8); + @include media($medium-screen) { + @include span-columns(4); + } - .sticky-header__summary { - // Hide the search summary for small screens - display: none; - font-size: $small-font-size; + h1 { + font-size: rem(24); + line-height: rem(31); + font-weight: $font-semibold; + margin: 0; + } +} - @include media($medium-screen) { - @include span-columns(8); - @include display(flex); - } +.keyword-header__summary { + // Hide the search summary for small screens + display: none; + font-size: $small-font-size; - .sticky-header__summary_title { - font-weight: $font-semibold; - padding-right: rem(10); - margin-right: rem(10); - border-right: solid 1px $color-white; - } + @include media($medium-screen) { + @include span-columns(8); + @include display(flex); + } - .sticky-header__summary_award-amounts, - .sticky-header__summary_award-counts { - margin-right: rem(20); - .number { - font-weight: $font-semibold; - } - } + .keyword-header__summary-title { + font-weight: $font-semibold; + padding-right: rem(10); + margin-right: rem(10); + border-right: solid 1px $color-white; + } + + .keyword-header__summary-award-amounts, + .keyword-header__summary-award-counts { + margin-right: rem(20); + + .keyword-header__summary-number_bold { + font-weight: $font-semibold; } + } +} - .sticky-header__options { - @include span-columns(8); - @include media($medium-screen) { - @include span-columns(4); - } +.keyword-header__options { + @include flex(0 0 auto); + @import "../../../layouts/tabbedSearch/header/downloadButton"; - .download-wrap { - .download-hover-spacer { - left: rem(-140); - width: rem(290); - } - .download-button { - float: right; - } - } + @include span-columns(8); + @include media($medium-screen) { + @include span-columns(4); + } - &.no-hover { - .download-wrap { - .download-hover-spacer { - display: none; - } - } - } + .download-wrap { + .download-hover-spacer { + left: rem(-140); + width: rem(290); + } + .download-button { + float: right; } - &:after { - display: block; + } + + &.no-hover { + .download-wrap { + .download-hover-spacer { + display: none; + } } } -} \ No newline at end of file +} diff --git a/src/js/components/bulkDownload/awards/filters/dateRange/DownloadDateRange.jsx b/src/js/components/bulkDownload/awards/filters/dateRange/DownloadDateRange.jsx index 4c046e0b25..cbaee6ec96 100644 --- a/src/js/components/bulkDownload/awards/filters/dateRange/DownloadDateRange.jsx +++ b/src/js/components/bulkDownload/awards/filters/dateRange/DownloadDateRange.jsx @@ -5,6 +5,7 @@ import React from 'react'; import PropTypes from 'prop-types'; +import moment from 'moment'; import DatePicker from 'components/sharedComponents/DatePicker'; const defaultProps = { @@ -42,7 +43,44 @@ export default class DownloadDateRange extends React.Component { } } + generateStartDateDisabledDays() { + let disabledDays = []; + + if (this.props.endDate) { + // Cutoff date represents the latest possible date + // We only want users to be able to download 1 year's worth of data at a time, + // So we set the start date a year before the end date + // This requires adding a day after subtracting a year + disabledDays.push({ + after: this.props.endDate.toDate(), + before: moment(this.props.endDate).subtract(1, 'y').add(1, 'd').toDate() + }); + } + + return disabledDays; + } + + generateEndDateDisabledDays() { + let disabledDays = []; + + if (this.props.startDate) { + // Cutoff date represents the earliest possible date, based on the start date + // We only want users to be able to download 1 year's worth of data at a time, + // So we set the end date a year after the start date + // This requires subtracting a day after adding a year + disabledDays.push({ + before: this.props.startDate.toDate(), + after: moment(this.props.startDate).add(1, 'y').subtract(1, 'd').toDate() + }); + } + + return disabledDays; + } + render() { + const startDateDisabledDays = this.generateStartDateDisabledDays(); + const endDateDisabledDays = this.generateEndDateDisabledDays(); + return (
    { this.startPicker = component; }} @@ -67,6 +106,7 @@ export default class DownloadDateRange extends React.Component { opposite={this.props.startDate} showError={this.props.showError} hideError={this.props.hideError} + disabledDays={endDateDisabledDays} ref={(component) => { this.endPicker = component; }} diff --git a/src/js/components/bulkDownload/awards/filters/dateRange/TimePeriodFilter.jsx b/src/js/components/bulkDownload/awards/filters/dateRange/TimePeriodFilter.jsx index 60522f46fd..ba28ef3502 100644 --- a/src/js/components/bulkDownload/awards/filters/dateRange/TimePeriodFilter.jsx +++ b/src/js/components/bulkDownload/awards/filters/dateRange/TimePeriodFilter.jsx @@ -28,7 +28,7 @@ const errorTypes = { }, range: { title: 'Invalid Date Range', - message: 'The date range entered must be 1 year or less.' + message: 'Choose one of the ranges below or set your own range of one year or less.' } }; @@ -138,7 +138,7 @@ export default class TimePeriodFilter extends React.Component { const start = this.state.startDateBulkUI; const end = this.state.endDateBulkUI; - const yearBeforeEnd = moment(this.state.endDateBulkUI).subtract(1, 'y').add(1, 'd'); + const yearBeforeEnd = moment(this.state.endDateBulkUI).subtract(1, 'y'); if (start && end) { // both sets of dates exist diff --git a/src/js/components/keyword/KeywordPage.jsx b/src/js/components/keyword/KeywordPage.jsx index 84c4199950..fd5d9762b6 100644 --- a/src/js/components/keyword/KeywordPage.jsx +++ b/src/js/components/keyword/KeywordPage.jsx @@ -119,18 +119,18 @@ export default class KeywordPage extends React.Component { } return ( -
    -
    +
    +
    Search Summary
    -
    -
    - Total Prime Award Amount: {formattedPrimeAmount} +
    +
    + Total Prime Award Amount: {formattedPrimeAmount}
    -
    -
    - Prime Award Transaction Count: {formattedPrimeCount} +
    +
    + Prime Award Transaction Count: {formattedPrimeCount}
    @@ -156,11 +156,11 @@ export default class KeywordPage extends React.Component {
    -
    +

    Keyword Search

    {searchSummary} -
    +
    diff --git a/src/js/components/search/filters/timePeriod/DateRange.jsx b/src/js/components/search/filters/timePeriod/DateRange.jsx index 7ddbd73666..638784b655 100644 --- a/src/js/components/search/filters/timePeriod/DateRange.jsx +++ b/src/js/components/search/filters/timePeriod/DateRange.jsx @@ -69,11 +69,43 @@ export default class DateRange extends React.Component { this.props.removeDateRange(); } + generateStartDateDisabledDays(earliestDate) { + // handle the cutoff dates (preventing end dates from coming before + // start dates or vice versa) + console.log(earliestDate); + let disabledDays = [earliestDate]; + + if (this.props.endDate) { + // the cutoff date represents the latest possible date + disabledDays.push({ + after: this.props.endDate.toDate() + }); + } + + return disabledDays; + } + + generateEndDateDisabledDays(earliestDate) { + let disabledDays = [earliestDate]; + + if (this.props.startDate) { + // cutoff date represents the earliest possible date + disabledDays.push({ + before: this.props.startDate.toDate() + }); + } + + return disabledDays; + } + render() { const earliestDateString = FiscalYearHelper.convertFYToDateRange(FiscalYearHelper.earliestFiscalYear)[0]; const earliestDate = moment(earliestDateString, 'YYYY-MM-DD').toDate(); + const startDateDisabledDays = this.generateStartDateDisabledDays(earliestDate); + const endDateDisabledDays = this.generateEndDateDisabledDays(earliestDate); + let dateLabel = ''; let hideTags = 'hide'; if (this.props.selectedStart || this.props.selectedEnd) { @@ -119,9 +151,7 @@ export default class DateRange extends React.Component { opposite={this.props.endDate} showError={this.props.showError} hideError={this.props.hideError} - disabledDays={[{ - before: earliestDate - }]} + disabledDays={startDateDisabledDays} ref={(component) => { this.startPicker = component; }} @@ -134,9 +164,7 @@ export default class DateRange extends React.Component { opposite={this.props.startDate} showError={this.props.showError} hideError={this.props.hideError} - disabledDays={[{ - before: earliestDate - }]} + disabledDays={endDateDisabledDays} ref={(component) => { this.endPicker = component; }} diff --git a/src/js/components/sharedComponents/DatePicker.jsx b/src/js/components/sharedComponents/DatePicker.jsx index a6a8a37bf6..5b6a4ae3b0 100644 --- a/src/js/components/sharedComponents/DatePicker.jsx +++ b/src/js/components/sharedComponents/DatePicker.jsx @@ -212,45 +212,6 @@ export default class DatePicker extends React.Component { pickedDay = moment().toDate(); } - // handle the cutoff dates (preventing end dates from coming before - // start dates or vice versa) - let disabledDays = this.props.disabledDays.slice(0); - if (this.props.type === 'startDate' && this.props.opposite) { - // the cutoff date represents the latest possible date - disabledDays.push({ - after: this.props.opposite.toDate() - }); - } - else if (this.props.type === 'endDate' && this.props.opposite) { - // cutoff date represents the earliest possible date - disabledDays.push({ - before: this.props.opposite.toDate() - }); - } - else if (this.props.type === 'startDateBulk' && this.props.opposite) { - // Cutoff date represents the latest possible date - // We only want users to be able to download 1 year's worth of data at a time, - // So we set the start date a year before the end date - // This requires adding a day after subtracting a year - disabledDays.push({ - after: this.props.opposite.toDate(), - before: moment(this.props.opposite).subtract(1, 'y').add(1, 'd').toDate() - }); - } - else if (this.props.type === 'endDateBulk' && this.props.opposite) { - // Cutoff date represents the earliest possible date, based on the start date - // We only want users to be able to download 1 year's worth of data at a time, - // So we set the end date a year after the start date - // This requires subtracting a day after adding a year - disabledDays.push({ - before: this.props.opposite.toDate(), - after: moment(this.props.opposite).add(1, 'y').subtract(1, 'd').toDate() - }); - } - else if (!this.props.value) { - disabledDays = []; - } - const inputId = `picker-${uniqueId()}`; return ( @@ -284,7 +245,7 @@ export default class DatePicker extends React.Component { this.datepicker = daypicker; }} month={pickedDay} - disabledDays={disabledDays} + disabledDays={this.props.disabledDays} selectedDays={(day) => DateUtils.isSameDay(pickedDay, day)} onDayClick={this.handleDatePick} onFocus={this.handleDateFocus} diff --git a/src/js/components/sharedComponents/stickyHeader/StickyHeader.jsx b/src/js/components/sharedComponents/stickyHeader/StickyHeader.jsx index 098d8ab526..63406c8d49 100644 --- a/src/js/components/sharedComponents/stickyHeader/StickyHeader.jsx +++ b/src/js/components/sharedComponents/stickyHeader/StickyHeader.jsx @@ -69,7 +69,7 @@ export default class StickyHeader extends React.Component { return (
    { this.wrapper = div; }}> @@ -79,7 +79,7 @@ export default class StickyHeader extends React.Component { this.content = div; }}>
    {this.props.children}
    From e3a31e5dbde004e4a7833322cf6219bde48c4cda Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Mon, 5 Feb 2018 14:22:12 -0500 Subject: [PATCH 26/89] Fixing lint issues --- .../awards/filters/dateRange/DownloadDateRange.jsx | 4 ++-- src/js/components/search/filters/timePeriod/DateRange.jsx | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/js/components/bulkDownload/awards/filters/dateRange/DownloadDateRange.jsx b/src/js/components/bulkDownload/awards/filters/dateRange/DownloadDateRange.jsx index cbaee6ec96..cf7be517c0 100644 --- a/src/js/components/bulkDownload/awards/filters/dateRange/DownloadDateRange.jsx +++ b/src/js/components/bulkDownload/awards/filters/dateRange/DownloadDateRange.jsx @@ -44,7 +44,7 @@ export default class DownloadDateRange extends React.Component { } generateStartDateDisabledDays() { - let disabledDays = []; + const disabledDays = []; if (this.props.endDate) { // Cutoff date represents the latest possible date @@ -61,7 +61,7 @@ export default class DownloadDateRange extends React.Component { } generateEndDateDisabledDays() { - let disabledDays = []; + const disabledDays = []; if (this.props.startDate) { // Cutoff date represents the earliest possible date, based on the start date diff --git a/src/js/components/search/filters/timePeriod/DateRange.jsx b/src/js/components/search/filters/timePeriod/DateRange.jsx index 638784b655..745a4443f8 100644 --- a/src/js/components/search/filters/timePeriod/DateRange.jsx +++ b/src/js/components/search/filters/timePeriod/DateRange.jsx @@ -72,8 +72,7 @@ export default class DateRange extends React.Component { generateStartDateDisabledDays(earliestDate) { // handle the cutoff dates (preventing end dates from coming before // start dates or vice versa) - console.log(earliestDate); - let disabledDays = [earliestDate]; + const disabledDays = [earliestDate]; if (this.props.endDate) { // the cutoff date represents the latest possible date @@ -86,7 +85,7 @@ export default class DateRange extends React.Component { } generateEndDateDisabledDays(earliestDate) { - let disabledDays = [earliestDate]; + const disabledDays = [earliestDate]; if (this.props.startDate) { // cutoff date represents the earliest possible date From 89a4fb0244e616d45b35c8430b673447a3aedc2b Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Mon, 5 Feb 2018 14:49:26 -0500 Subject: [PATCH 27/89] BEM updates --- .../layouts/default/stickyHeader/header.scss | 9 +- src/_scss/pages/keyword/header/header.scss | 112 ++++++++++-------- src/_scss/pages/keyword/keywordPage.scss | 1 + src/js/components/keyword/KeywordPage.jsx | 26 ++-- 4 files changed, 81 insertions(+), 67 deletions(-) diff --git a/src/_scss/layouts/default/stickyHeader/header.scss b/src/_scss/layouts/default/stickyHeader/header.scss index 25b1ed3cee..b9a59a47ab 100644 --- a/src/_scss/layouts/default/stickyHeader/header.scss +++ b/src/_scss/layouts/default/stickyHeader/header.scss @@ -1,8 +1,8 @@ -$search-header-height: rem(66); - .sticky-header { + $sticky-header-height: rem(66); + position: relative; - height: $search-header-height; + height: $sticky-header-height; .sticky-header__container { width: 100%; @@ -29,8 +29,7 @@ $search-header-height: rem(66); @include align-self(stretch); @include flex-flow(row wrap); position: relative; - - height: $search-header-height; + height: $sticky-header-height; .sticky-header__title { @include flex(1 1 auto); diff --git a/src/_scss/pages/keyword/header/header.scss b/src/_scss/pages/keyword/header/header.scss index 227e9246e0..1223259a5e 100644 --- a/src/_scss/pages/keyword/header/header.scss +++ b/src/_scss/pages/keyword/header/header.scss @@ -1,70 +1,82 @@ -@import "layouts/default/stickyHeader/header"; +.keyword-header { + $keyword-header-height: rem(66); -.keyword-header__title { - @include flex(1 1 auto); - @include span-columns(8); - @include media($medium-screen) { - @include span-columns(4); - } - - h1 { - font-size: rem(24); - line-height: rem(31); - font-weight: $font-semibold; - margin: 0; - } -} - -.keyword-header__summary { - // Hide the search summary for small screens - display: none; - font-size: $small-font-size; + @include display(flex); + @include justify-content(space-between); + @include flex-direction(row); + @include align-items(center); + @include align-self(stretch); + @include flex-flow(row wrap); + width: 100%; + position: relative; + height: $keyword-header-height; - @include media($medium-screen) { + .keyword-header__title { + @include flex(1 1 auto); @include span-columns(8); - @include display(flex); - } + @include media($medium-screen) { + @include span-columns(4); + } - .keyword-header__summary-title { - font-weight: $font-semibold; - padding-right: rem(10); - margin-right: rem(10); - border-right: solid 1px $color-white; + h1 { + font-size: rem(24); + line-height: rem(31); + font-weight: $font-semibold; + margin: 0; + } } - .keyword-header__summary-award-amounts, - .keyword-header__summary-award-counts { - margin-right: rem(20); + .keyword-header__summary { + // Hide the search summary for small screens + display: none; + font-size: $small-font-size; - .keyword-header__summary-number_bold { + @include media($medium-screen) { + @include span-columns(8); + @include display(flex); + } + + .keyword-header__summary-title { font-weight: $font-semibold; + padding-right: rem(10); + margin-right: rem(10); + border-right: solid 1px $color-white; } - } -} -.keyword-header__options { - @include flex(0 0 auto); - @import "../../../layouts/tabbedSearch/header/downloadButton"; + .keyword-header__summary-award-amounts, + .keyword-header__summary-award-counts { + margin-right: rem(20); - @include span-columns(8); - @include media($medium-screen) { - @include span-columns(4); + .keyword-header__summary-amount_bold { + font-weight: $font-semibold; + } + } } - .download-wrap { - .download-hover-spacer { - left: rem(-140); - width: rem(290); - } - .download-button { - float: right; + .keyword-header__options { + @include flex(0 0 auto); + @import "../../../layouts/tabbedSearch/header/downloadButton"; + + @include span-columns(8); + @include media($medium-screen) { + @include span-columns(4); } - } - &.no-hover { .download-wrap { .download-hover-spacer { - display: none; + left: rem(-140); + width: rem(290); + } + .download-button { + float: right; + } + } + + &.no-hover { + .download-wrap { + .download-hover-spacer { + display: none; + } } } } diff --git a/src/_scss/pages/keyword/keywordPage.scss b/src/_scss/pages/keyword/keywordPage.scss index c0846985ed..63b7157099 100644 --- a/src/_scss/pages/keyword/keywordPage.scss +++ b/src/_scss/pages/keyword/keywordPage.scss @@ -1,6 +1,7 @@ .usa-da-keyword-page { @import "all"; @import "layouts/default/default"; + @import "layouts/default/stickyHeader/header"; @import "./header/header"; .keyword-content { diff --git a/src/js/components/keyword/KeywordPage.jsx b/src/js/components/keyword/KeywordPage.jsx index fd5d9762b6..14828209d6 100644 --- a/src/js/components/keyword/KeywordPage.jsx +++ b/src/js/components/keyword/KeywordPage.jsx @@ -124,13 +124,13 @@ export default class KeywordPage extends React.Component { Search Summary
    -
    - Total Prime Award Amount: {formattedPrimeAmount} +
    + Total Prime Award Amount: {formattedPrimeAmount}
    -
    - Prime Award Transaction Count: {formattedPrimeCount} +
    + Prime Award Transaction Count: {formattedPrimeCount}
    @@ -156,14 +156,16 @@ export default class KeywordPage extends React.Component {
    -
    -

    Keyword Search

    -
    - {searchSummary} -
    - +
    +
    +

    Keyword Search

    +
    + {searchSummary} +
    + +
    From 1b0c7d89273dac46575e0155bb3337697f79101e Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Mon, 5 Feb 2018 14:52:53 -0500 Subject: [PATCH 28/89] Search page filter analytic events --- .../awardAmount/SpecificAwardAmountItem.jsx | 2 +- .../filters/timePeriod/AllFiscalYears.jsx | 2 +- .../search/filters/timePeriod/TimePeriod.jsx | 2 +- .../checkbox/PrimaryCheckboxType.jsx | 4 +- .../checkbox/SecondaryCheckboxType.jsx | 4 +- .../checkbox/SingleCheckboxType.jsx | 4 +- .../search/SearchSidebarSubmitContainer.jsx | 6 +- .../search/filters/AgencyContainer.jsx | 2 +- .../search/filters/KeywordContainer.jsx | 2 +- .../awardID/AwardIDSearchContainer.jsx | 2 +- .../filters/cfda/CFDASearchContainer.jsx | 2 +- .../filters/location/POPFilterContainer.jsx | 2 +- .../location/RecipientFilterContainer.jsx | 2 +- .../filters/naics/NAICSSearchContainer.jsx | 2 +- .../search/filters/psc/PSCSearchContainer.jsx | 2 +- .../recipient/RecipientSearchContainer.jsx | 2 +- .../recipient/RecipientTypeContainer.jsx | 2 +- .../search/helpers/searchAnalytics.js | 210 ++++++++++++++++++ 18 files changed, 234 insertions(+), 20 deletions(-) create mode 100644 src/js/containers/search/helpers/searchAnalytics.js diff --git a/src/js/components/search/filters/awardAmount/SpecificAwardAmountItem.jsx b/src/js/components/search/filters/awardAmount/SpecificAwardAmountItem.jsx index 017eaa5095..3ee32ce252 100644 --- a/src/js/components/search/filters/awardAmount/SpecificAwardAmountItem.jsx +++ b/src/js/components/search/filters/awardAmount/SpecificAwardAmountItem.jsx @@ -19,7 +19,7 @@ const propTypes = { export default class SpecificAwardAmountItem extends React.Component { static logAmountRangeEvent(range) { Analytics.event({ - category: 'Search Page Filter Applied', + category: 'Search Filter Interaction', action: 'Applied Award Amount Range Filter', label: range }); diff --git a/src/js/components/search/filters/timePeriod/AllFiscalYears.jsx b/src/js/components/search/filters/timePeriod/AllFiscalYears.jsx index 2750ae061d..6791942d87 100644 --- a/src/js/components/search/filters/timePeriod/AllFiscalYears.jsx +++ b/src/js/components/search/filters/timePeriod/AllFiscalYears.jsx @@ -20,7 +20,7 @@ const propTypes = { export default class AllFiscalYears extends React.Component { static logFYEvent(year) { Analytics.event({ - category: 'Search Page Filter Applied', + category: 'Search Filter Interaction', action: 'Applied Fiscal Year Filter', label: year }); diff --git a/src/js/components/search/filters/timePeriod/TimePeriod.jsx b/src/js/components/search/filters/timePeriod/TimePeriod.jsx index 1bf2ad0cfe..b7773f30bf 100644 --- a/src/js/components/search/filters/timePeriod/TimePeriod.jsx +++ b/src/js/components/search/filters/timePeriod/TimePeriod.jsx @@ -47,7 +47,7 @@ export default class TimePeriod extends React.Component { Analytics.event({ label, - category: 'Search Page Filter Applied', + category: 'Search Filter Interaction', action: 'Applied Date Range Filter' }); } diff --git a/src/js/components/sharedComponents/checkbox/PrimaryCheckboxType.jsx b/src/js/components/sharedComponents/checkbox/PrimaryCheckboxType.jsx index 7c428adb17..e290a11b70 100644 --- a/src/js/components/sharedComponents/checkbox/PrimaryCheckboxType.jsx +++ b/src/js/components/sharedComponents/checkbox/PrimaryCheckboxType.jsx @@ -39,7 +39,7 @@ const defaultProps = { export default class PrimaryCheckboxType extends React.Component { static logPrimaryTypeFilterEvent(type, filter) { Analytics.event({ - category: 'Search Page Filter Applied', + category: 'Search Filter Interaction', action: `Selected ${filter} Type`, label: type }); @@ -47,7 +47,7 @@ export default class PrimaryCheckboxType extends React.Component { static logDeselectFilterEvent(type, filter) { Analytics.event({ - category: 'Search Page Filter Applied', + category: 'Search Filter Interaction', action: `Deselected ${filter} Type Children`, label: type }); diff --git a/src/js/components/sharedComponents/checkbox/SecondaryCheckboxType.jsx b/src/js/components/sharedComponents/checkbox/SecondaryCheckboxType.jsx index 54ad5d3860..94edd1b348 100644 --- a/src/js/components/sharedComponents/checkbox/SecondaryCheckboxType.jsx +++ b/src/js/components/sharedComponents/checkbox/SecondaryCheckboxType.jsx @@ -29,7 +29,7 @@ const defaultProps = { export default class SecondaryCheckboxType extends React.Component { static logSecondaryTypeFilterEvent(type, filter) { Analytics.event({ - category: 'Search Page Filter Applied', + category: 'Search Filter Interaction', action: `Selected Secondary ${filter} Type`, label: type }); @@ -37,7 +37,7 @@ export default class SecondaryCheckboxType extends React.Component { static logDeselectFilterEvent(type, filter) { Analytics.event({ - category: 'Search Page Filter Applied', + category: 'Search Filter Interaction', action: `Deselected Secondary ${filter} Type`, label: type }); diff --git a/src/js/components/sharedComponents/checkbox/SingleCheckboxType.jsx b/src/js/components/sharedComponents/checkbox/SingleCheckboxType.jsx index 174c53728b..eda11c240e 100644 --- a/src/js/components/sharedComponents/checkbox/SingleCheckboxType.jsx +++ b/src/js/components/sharedComponents/checkbox/SingleCheckboxType.jsx @@ -28,7 +28,7 @@ const defaultProps = { export default class SingleCheckboxType extends React.Component { static logSingleTypeFilterEvent(type, filter) { Analytics.event({ - category: 'Search Page Filter Applied', + category: 'Search Filter Interaction', action: `Selected ${filter} Type`, label: type }); @@ -36,7 +36,7 @@ export default class SingleCheckboxType extends React.Component { static logDeselectSingleTypeFilterEvent(type, filter) { Analytics.event({ - category: 'Search Page Filter Applied', + category: 'Search Filter Interaction', action: `Deselected ${filter} Type`, label: type }); diff --git a/src/js/containers/search/SearchSidebarSubmitContainer.jsx b/src/js/containers/search/SearchSidebarSubmitContainer.jsx index 043809f706..717d056c59 100644 --- a/src/js/containers/search/SearchSidebarSubmitContainer.jsx +++ b/src/js/containers/search/SearchSidebarSubmitContainer.jsx @@ -7,7 +7,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; - import { is } from 'immutable'; import * as appliedFilterActions from 'redux/actions/search/appliedFilterActions'; @@ -15,6 +14,8 @@ import { clearAllFilters as clearStagedFilters } from 'redux/actions/search/sear import SearchSidebarSubmit from 'components/search/SearchSidebarSubmit'; +import { convertFiltersToAnalyticEvents, sendAnalyticEvents } from './helpers/searchAnalytics'; + const combinedActions = Object.assign({}, appliedFilterActions, { clearStagedFilters }); @@ -86,6 +87,9 @@ export class SearchSidebarSubmitContainer extends React.Component { this.setState({ filtersChanged: false }); + + const events = convertFiltersToAnalyticEvents(this.props.stagedFilters); + sendAnalyticEvents(events); } resetFilters() { diff --git a/src/js/containers/search/filters/AgencyContainer.jsx b/src/js/containers/search/filters/AgencyContainer.jsx index 07c7e2c016..ea5a5040f9 100644 --- a/src/js/containers/search/filters/AgencyContainer.jsx +++ b/src/js/containers/search/filters/AgencyContainer.jsx @@ -27,7 +27,7 @@ const propTypes = { export class AgencyContainer extends React.Component { static logAgencyFilterEvent(agencyType, agency) { Analytics.event({ - category: 'Search Page Filter Applied', + category: 'Search Filter Interaction', action: `Applied ${agencyType} Agency Filter`, label: agency.toLowerCase() }); diff --git a/src/js/containers/search/filters/KeywordContainer.jsx b/src/js/containers/search/filters/KeywordContainer.jsx index 4f5b22036c..35cba18c36 100644 --- a/src/js/containers/search/filters/KeywordContainer.jsx +++ b/src/js/containers/search/filters/KeywordContainer.jsx @@ -24,7 +24,7 @@ const propTypes = { export class KeywordContainer extends React.Component { static logSelectedKeywordEvent(keyword) { Analytics.event({ - category: 'Search Page Filter Applied', + category: 'Search Filter Interaction', action: 'Applied Keyword Filter', label: keyword }); diff --git a/src/js/containers/search/filters/awardID/AwardIDSearchContainer.jsx b/src/js/containers/search/filters/awardID/AwardIDSearchContainer.jsx index bb487f2c23..ecf8aa036e 100644 --- a/src/js/containers/search/filters/awardID/AwardIDSearchContainer.jsx +++ b/src/js/containers/search/filters/awardID/AwardIDSearchContainer.jsx @@ -24,7 +24,7 @@ const propTypes = { export class AwardIDSearchContainer extends React.Component { static logIdEvent(id, type) { Analytics.event({ - category: 'Search Page Filter Applied', + category: 'Search Filter Interaction', action: `Toggled Award ${type} Filter`, label: id }); diff --git a/src/js/containers/search/filters/cfda/CFDASearchContainer.jsx b/src/js/containers/search/filters/cfda/CFDASearchContainer.jsx index 7bed9150aa..517406c3e5 100644 --- a/src/js/containers/search/filters/cfda/CFDASearchContainer.jsx +++ b/src/js/containers/search/filters/cfda/CFDASearchContainer.jsx @@ -24,7 +24,7 @@ const propTypes = { export class CFDASearchContainer extends React.Component { static logCFDAFilterEvent(place) { Analytics.event({ - category: 'Search Page Filter Applied', + category: 'Search Filter Interaction', action: `Applied CFDA Filter`, label: place.toLowerCase() }); diff --git a/src/js/containers/search/filters/location/POPFilterContainer.jsx b/src/js/containers/search/filters/location/POPFilterContainer.jsx index 4c4778ec2f..70ebcb5051 100644 --- a/src/js/containers/search/filters/location/POPFilterContainer.jsx +++ b/src/js/containers/search/filters/location/POPFilterContainer.jsx @@ -25,7 +25,7 @@ export class POPFilterContainer extends React.Component { static logLocationFilterEvent(label, event) { Analytics.event({ label, - category: 'Search Page Filter Applied', + category: 'Search Filter Interaction', action: `${event} Place of Performance Location Filter` }); } diff --git a/src/js/containers/search/filters/location/RecipientFilterContainer.jsx b/src/js/containers/search/filters/location/RecipientFilterContainer.jsx index f5632c8bfe..b26e77b096 100644 --- a/src/js/containers/search/filters/location/RecipientFilterContainer.jsx +++ b/src/js/containers/search/filters/location/RecipientFilterContainer.jsx @@ -25,7 +25,7 @@ export class RecipientFilterContainer extends React.Component { static logLocationFilterEvent(label, event) { Analytics.event({ label, - category: 'Search Page Filter Applied', + category: 'Search Filter Interaction', action: `${event} Recipient Location Filter` }); } diff --git a/src/js/containers/search/filters/naics/NAICSSearchContainer.jsx b/src/js/containers/search/filters/naics/NAICSSearchContainer.jsx index bbba49ba1c..55dfa11263 100644 --- a/src/js/containers/search/filters/naics/NAICSSearchContainer.jsx +++ b/src/js/containers/search/filters/naics/NAICSSearchContainer.jsx @@ -24,7 +24,7 @@ const propTypes = { export class NAICSSearchContainer extends React.Component { static logPlaceFilterEvent(naics) { Analytics.event({ - category: 'Search Page Filter Applied', + category: 'Search Filter Interaction', action: `Applied NAICS Filter`, label: naics.toLowerCase() }); diff --git a/src/js/containers/search/filters/psc/PSCSearchContainer.jsx b/src/js/containers/search/filters/psc/PSCSearchContainer.jsx index 9cb647dbe9..89cebee4d7 100644 --- a/src/js/containers/search/filters/psc/PSCSearchContainer.jsx +++ b/src/js/containers/search/filters/psc/PSCSearchContainer.jsx @@ -24,7 +24,7 @@ const propTypes = { export class PSCSearchContainer extends React.Component { static logPSCFilterEvent(psc) { Analytics.event({ - category: 'Search Page Filter Applied', + category: 'Search Filter Interaction', action: `Applied PSC Filter`, label: psc }); diff --git a/src/js/containers/search/filters/recipient/RecipientSearchContainer.jsx b/src/js/containers/search/filters/recipient/RecipientSearchContainer.jsx index 08226d4dc5..f7f3c386c0 100644 --- a/src/js/containers/search/filters/recipient/RecipientSearchContainer.jsx +++ b/src/js/containers/search/filters/recipient/RecipientSearchContainer.jsx @@ -24,7 +24,7 @@ const propTypes = { export class RecipientSearchContainer extends React.Component { static logRecipientFilterEvent(name) { Analytics.event({ - category: 'Search Page Filter Applied', + category: 'Search Filter Interaction', action: 'Applied Recipient Name/DUNS Filter', label: name.toLowerCase() }); diff --git a/src/js/containers/search/filters/recipient/RecipientTypeContainer.jsx b/src/js/containers/search/filters/recipient/RecipientTypeContainer.jsx index 018e864b37..b7ad0eb129 100644 --- a/src/js/containers/search/filters/recipient/RecipientTypeContainer.jsx +++ b/src/js/containers/search/filters/recipient/RecipientTypeContainer.jsx @@ -28,7 +28,7 @@ const propTypes = { export class RecipientTypeContainer extends React.Component { static logLocationFilterEvent(placeType, place, event) { Analytics.event({ - category: 'Search Page Filter Applied', + category: 'Search Filter Interaction', action: `${event} Recipient ${placeType.toLowerCase()} Filter`, label: place.toLowerCase() }); diff --git a/src/js/containers/search/helpers/searchAnalytics.js b/src/js/containers/search/helpers/searchAnalytics.js new file mode 100644 index 0000000000..276dff72d5 --- /dev/null +++ b/src/js/containers/search/helpers/searchAnalytics.js @@ -0,0 +1,210 @@ +/** + * searchAnalytics.js + * Created by Kevin Li 2/2/18 + */ + +import { Set } from 'immutable'; +import { awardTypeCodes } from 'dataMapping/search/awardType'; +import { recipientTypes, groupLabels } from 'dataMapping/search/recipientType'; +import { + pricingTypeDefinitions, + setAsideDefinitions, + extentCompetedDefinitions +} from 'dataMapping/search/contractFields'; + +import Analytics from 'helpers/analytics/Analytics'; + +const eventCategory = 'Applied Search Filter'; + +const convertDateRange = (range) => { + if (range.length !== 2) { + // this must be an array of length 2 + return null; + } + else if (!range[0] && !range[1]) { + // no start or end dates are set + return null; + } + + const startDate = range[0] || '...'; + const endDate = range[1] || 'present'; + + return [{ + action: 'Time Period - Date Range', + label: `${startDate} to ${endDate}` + }]; +}; + +const parseAgency = (agency) => { + const toptier = agency.toptier_agency; + const subtier = agency.subtier_agency; + const office = agency.office_agency; + if (agency.agencyType === 'toptier') { + if (toptier.abbreviation) { + return `${toptier.name} (${toptier.abbreviation})/${toptier.cgac_code}`; + } + return `${toptier.name}/${toptier.cgac_code}`; + } + else if (agency.agencyType === 'subtier') { + if (subtier.abbreviation) { + return `${subtier.name} (${subtier.abbreviation})/${subtier.subtier_code} - ${toptier.name}/${toptier.cgac_code}`; + } + return `${subtier.name}/${subtier.subtier_code} - ${toptier.name}/${toptier.cgac_code}`; + } + else if (agency.agencyType === 'office') { + return `${office.name}/${office.aac_code} - ${toptier.name}/${toptier.cgac_code}`; + } + return null; +}; + + +const convertReducibleValue = (value, type, parser) => ( + value.reduce((events, item) => { + events.push({ + action: type, + label: (parser && parser(item)) || item + }); + return events; + }, []) +); + +const convertTimePeriod = (value) => { + if (Set.isSet(value)) { + return convertReducibleValue( + value, + 'Time Period - Fiscal Year' + ); + } + else if (Array.isArray(value)) { + return convertDateRange(value); + } + return null; +}; + +const convertAgency = (agencies, type) => ( + convertReducibleValue( + agencies, + type, + parseAgency + ) +); + + +const convertLocation = (locations, type) => ( + convertReducibleValue( + locations, + type, + (location) => `${location.display.entity} - ${location.display.standalone}` + ) +); + +const convertFilter = (type, value) => { + switch (type) { + case 'timePeriod': + return convertTimePeriod(value); + case 'awardType': + return convertReducibleValue( + value, + 'Award Type', + (item) => awardTypeCodes[item] || item + ); + case 'selectedAwardingAgencies': + return convertAgency(value, 'Awarding Agency'); + case 'selectedFundingAgencies': + return convertAgency(value, 'Funding Agency'); + case 'selectedLocations': + return convertLocation(value, 'Place of Performance'); + case 'selectedRecipientLocations': + return convertLocation(value, 'Recipient Location'); + case 'selectedRecipients': + return convertReducibleValue(value, 'Recipient'); + case 'recipientType': + return convertReducibleValue( + value, + 'Recipient Type', + (item) => recipientTypes[item] || groupLabels[item] || item + ); + case 'awardAmounts': + return convertReducibleValue( + value, + 'Award Amount', + (amount) => `${amount[0]} - ${amount[1]}` + ); + case 'selectedAwardIDs': + return convertReducibleValue(value, 'Award ID'); + case 'selectedCFDA': + return convertReducibleValue( + value, + 'CFDA Program', + (cfda) => `${cfda.program_number} - ${cfda.program_title}` + ); + case 'selectedNAICS': + return convertReducibleValue( + value, + 'NAICS Code', + (naics) => `${naics.naics} - ${naics.naics_description}` + ); + case 'selectedPSC': + return convertReducibleValue( + value, + 'Product/Service Code (PSC)', + (psc) => `${psc.product_or_service_code} - ${psc.psc_description}` + ); + case 'pricingType': + return convertReducibleValue( + value, + 'Type of Contract Pricing', + (pricing) => pricingTypeDefinitions[pricing] + ); + case 'setAside': + return convertReducibleValue( + value, + 'Type of Set Aside', + (sa) => setAsideDefinitions[sa] + ); + case 'extentCompeted': + return convertReducibleValue( + value, + 'Extent Competed', + (extent) => extentCompetedDefinitions[extent] + ); + default: + return null; + } +}; + +const unifyDateFields = (redux) => { + // clone the filter reducer so we don't accidentally modify it + const clonedRedux = Object.assign({}, redux); + // unify the data fields into a single field + if (clonedRedux.timePeriodType === 'fy') { + clonedRedux.timePeriod = clonedRedux.timePeriodFY; + } + else { + clonedRedux.timePeriod = [clonedRedux.timePeriodStart, clonedRedux.timePeriodEnd]; + } + return clonedRedux; +}; + +export const convertFiltersToAnalyticEvents = (redux) => { + const filters = unifyDateFields(redux); + return Object.keys(filters).reduce((converted, type) => { + const value = filters[type]; + const analyticEvent = convertFilter(type, value); + if (analyticEvent) { + return converted.concat(analyticEvent); + } + return converted; + }, []); +}; + +export const sendAnalyticEvents = (events) => { + console.time('analytic'); + events.forEach((event) => { + Analytics.event(Object.assign({}, event, { + category: eventCategory + })); + }); + console.timeEnd('analytic'); +}; + From 5c6fa1195384289a3c96a8595ff17398df78b4a4 Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Mon, 5 Feb 2018 14:58:12 -0500 Subject: [PATCH 29/89] Added more space between the search bar and results section, converted the search bar to BEM --- src/_scss/pages/keyword/_searchBar.scss | 75 +++++++++---------- src/_scss/pages/keyword/keywordPage.scss | 3 + src/js/components/keyword/KeywordPage.jsx | 8 +- .../components/keyword/KeywordSearchBar.jsx | 42 +++++------ .../keyword/table/ResultsTableSection.jsx | 4 +- 5 files changed, 68 insertions(+), 64 deletions(-) diff --git a/src/_scss/pages/keyword/_searchBar.scss b/src/_scss/pages/keyword/_searchBar.scss index 1ccb620408..b70cc56f5e 100644 --- a/src/_scss/pages/keyword/_searchBar.scss +++ b/src/_scss/pages/keyword/_searchBar.scss @@ -1,4 +1,4 @@ -.search-bar-section { +.keyword-search-bar { background-color: $color-primary-alt-lightest; padding: rem(15); border: 1px solid $color-vis-lightest; @@ -8,54 +8,53 @@ @include align-items(center); @include justify-content(space-evenly); } - .keyword-search-bar { + + .keyword-search-bar__form { + width: 100%; + border: 1px solid $color-gray-light; + margin-right: rem(15); + background-color: $color-white; @include media($medium-screen) { padding-right: rem(10); width: 50%; } - form { - width: 100%; - border: 1px solid $color-gray-light; - margin-right: rem(15); - background-color: $color-white; - input.keyword-input { - margin-right: 0; - height: rem(66); - width: 85%; - border: 0; - padding: rem(15); - font-size: rem(28); - font-weight: 300; - line-height: rem(36); + .keyword-search-bar__input { + margin-right: 0; + height: rem(66); + width: 85%; + border: 0; + padding: rem(15); + font-size: rem(28); + font-weight: 300; + line-height: rem(36); + } + .keyword-search-bar__button { + @include button-unstyled; + height: rem(66); + float: right; + width: 15%; + .keyword-search-bar__button-icon { + height: rem(30); + width: rem(30); + margin: auto; + svg { + fill: $color-gray; + } } - button.keyword-submit { - @include button-unstyled; - height: rem(66); - float: right; - width: 15%; - .icon { - height: rem(30); - width: rem(30); - margin: auto; + &.keyword-search-bar__button_disabled { + .keyword-search-bar__button-icon { svg { - fill: $color-gray; + fill: $color-gray-lighter; } } - &.disabled { - .icon { - svg { - fill: $color-gray-lighter; - } - } - &:hover { - cursor: not-allowed; - } + &:hover { + cursor: not-allowed; } } } } - .info-text { + .keyword-search-bar__info { font-size: $small-font-size; line-height: rem(26); padding-left: rem(10); @@ -64,11 +63,11 @@ width: 50%; padding-top: 0; } - .info-wrap { + .keyword-search-bar__icon-wrapper { display: inline-block; margin: 0 rem(5); @import "keywordTooltip"; - .icon { + .keyword-search-bar__icon { @include button-unstyled; height: rem(15); width: rem(15); diff --git a/src/_scss/pages/keyword/keywordPage.scss b/src/_scss/pages/keyword/keywordPage.scss index c0846985ed..d5ce823548 100644 --- a/src/_scss/pages/keyword/keywordPage.scss +++ b/src/_scss/pages/keyword/keywordPage.scss @@ -14,6 +14,9 @@ border-right: 1px solid $color-gray-border; border-bottom: 1px solid $color-gray-border; + .keyword-search-bar { + margin-bottom: rem(22); + } @import "searchBar"; @import "table/resultsTable"; } diff --git a/src/js/components/keyword/KeywordPage.jsx b/src/js/components/keyword/KeywordPage.jsx index 3dcfa84374..2216319958 100644 --- a/src/js/components/keyword/KeywordPage.jsx +++ b/src/js/components/keyword/KeywordPage.jsx @@ -104,18 +104,18 @@ export default class KeywordPage extends React.Component { clickedDownload={this.clickedDownload} keyword={this.props.keyword} />
    -
    +
    -
    +
    Use the Keyword Search to get a broad picture of award data on a given theme. You can search through only award descriptions, or award descriptions plus other attributes. -
    +
    {hover}
    diff --git a/src/js/components/keyword/KeywordSearchBar.jsx b/src/js/components/keyword/KeywordSearchBar.jsx index 8ed6cd00e0..ea83b4ff99 100644 --- a/src/js/components/keyword/KeywordSearchBar.jsx +++ b/src/js/components/keyword/KeywordSearchBar.jsx @@ -38,33 +38,33 @@ export default class KeywordSearchBar extends React.Component { } render() { - let disabledClass = 'disabled'; + let disabledClass = 'keyword-search-bar__button_disabled'; let submitButtonText = 'Enter at least three characters to search'; if (this.state.searchString.length > 2) { disabledClass = ''; submitButtonText = 'Search by Keyword'; } return ( -
    -
    - - - -
    +
    + + + ); } } diff --git a/src/js/components/keyword/table/ResultsTableSection.jsx b/src/js/components/keyword/table/ResultsTableSection.jsx index ca4057b671..b45dd6b5be 100644 --- a/src/js/components/keyword/table/ResultsTableSection.jsx +++ b/src/js/components/keyword/table/ResultsTableSection.jsx @@ -100,7 +100,9 @@ export default class ResultsTableSection extends React.Component { } return ( -
    +
    Date: Mon, 5 Feb 2018 15:41:13 -0500 Subject: [PATCH 30/89] Updating Federal Account model --- src/js/models/account/FederalAccount.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/js/models/account/FederalAccount.js b/src/js/models/account/FederalAccount.js index 781e894426..50c93d54fa 100644 --- a/src/js/models/account/FederalAccount.js +++ b/src/js/models/account/FederalAccount.js @@ -23,10 +23,13 @@ const defaultValues = [ '', 'Not available', { - obligated: {}, - unobligated: {}, - budgetAuthority: {}, - outlay: {} + obligated: 0, + unobligated: 0, + budgetAuthority: 0, + outlay: 0, + balanceBroughtForward: 0, + otherBudgetaryResources: 0, + appropriations: 0 } ]; From b8f7abf00d589562e265c2b910c880eb008df352 Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Mon, 5 Feb 2018 15:56:12 -0500 Subject: [PATCH 31/89] Changed Award Data header to Custom Award Data, removed the word "download" from the header, added a "last 15 days" option per design review --- .../bulkDownload/awards/AwardDataContent.jsx | 2 +- .../dataMapping/bulkDownload/bulkDownloadOptions.js | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/js/components/bulkDownload/awards/AwardDataContent.jsx b/src/js/components/bulkDownload/awards/AwardDataContent.jsx index 406ed5ea87..b85673360f 100644 --- a/src/js/components/bulkDownload/awards/AwardDataContent.jsx +++ b/src/js/components/bulkDownload/awards/AwardDataContent.jsx @@ -102,7 +102,7 @@ export default class AwardDataContent extends React.Component { return (
    -

    Award Data Download

    +

    Custom Award Data

    diff --git a/src/js/dataMapping/bulkDownload/bulkDownloadOptions.js b/src/js/dataMapping/bulkDownload/bulkDownloadOptions.js index b9232b58bc..9a1358c32c 100644 --- a/src/js/dataMapping/bulkDownload/bulkDownloadOptions.js +++ b/src/js/dataMapping/bulkDownload/bulkDownloadOptions.js @@ -92,17 +92,22 @@ export const awardDownloadOptions = { endDate: moment().format('YYYY-MM-DD') }, { - label: 'last 30 days', - startDate: moment().subtract(30, 'day').format('YYYY-MM-DD'), + label: 'last 15 days', + startDate: moment().subtract(15, 'day').format('YYYY-MM-DD'), endDate: moment().format('YYYY-MM-DD') }, { - label: 'this month', - startDate: moment().startOf('month').format('YYYY-MM-DD'), + label: 'last 30 days', + startDate: moment().subtract(30, 'day').format('YYYY-MM-DD'), endDate: moment().format('YYYY-MM-DD') } ], column4: [ + { + label: 'this month', + startDate: moment().startOf('month').format('YYYY-MM-DD'), + endDate: moment().format('YYYY-MM-DD') + }, { label: 'last 3 months', startDate: moment().subtract(3, 'month').format('YYYY-MM-DD'), From c94dc2fd481811a00cf162bc2c8563b4d7fbfa41 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Mon, 5 Feb 2018 16:11:40 -0500 Subject: [PATCH 32/89] Remove old analytic events --- .../filters/objectClass/ObjectClassFilter.jsx | 3 +-- .../programActivity/ProgramActivityFilter.jsx | 3 +-- .../filters/awardAmount/AwardAmountSearch.jsx | 3 +-- .../awardAmount/SpecificAwardAmountItem.jsx | 13 ----------- .../search/filters/awardType/AwardType.jsx | 3 +-- .../contractFilters/ContractFilter.jsx | 3 +-- .../filters/recipient/RecipientType.jsx | 3 +-- .../filters/timePeriod/AllFiscalYears.jsx | 14 ------------ .../search/filters/timePeriod/TimePeriod.jsx | 22 ------------------- .../filterSidebar/FilterOption.jsx | 11 ---------- .../search/SearchSidebarSubmitContainer.jsx | 3 ++- .../search/filters/AgencyContainer.jsx | 19 ---------------- .../search/filters/KeywordContainer.jsx | 15 ------------- .../awardID/AwardIDSearchContainer.jsx | 18 +-------------- .../filters/cfda/CFDASearchContainer.jsx | 13 ----------- .../filters/location/POPFilterContainer.jsx | 13 ----------- .../location/RecipientFilterContainer.jsx | 13 ----------- .../filters/naics/NAICSSearchContainer.jsx | 13 ----------- .../search/filters/psc/PSCSearchContainer.jsx | 13 ----------- .../recipient/RecipientSearchContainer.jsx | 13 ----------- .../recipient/RecipientTypeContainer.jsx | 9 -------- .../search/helpers/searchAnalytics.js | 18 +++++++++++++-- 22 files changed, 25 insertions(+), 213 deletions(-) diff --git a/src/js/components/account/filters/objectClass/ObjectClassFilter.jsx b/src/js/components/account/filters/objectClass/ObjectClassFilter.jsx index 74ed7c171b..1b274f8e87 100644 --- a/src/js/components/account/filters/objectClass/ObjectClassFilter.jsx +++ b/src/js/components/account/filters/objectClass/ObjectClassFilter.jsx @@ -48,8 +48,7 @@ export default class ObjectClassFilter extends React.Component { filterType="Major Object Class" selectedCheckboxes={this.props.selectedCodes} toggleCheckboxType={this.toggleValue} - bulkTypeChange={this.props.updateMajorFilter} - enableAnalytics />); + bulkTypeChange={this.props.updateMajorFilter} />); }); return ( diff --git a/src/js/components/account/filters/programActivity/ProgramActivityFilter.jsx b/src/js/components/account/filters/programActivity/ProgramActivityFilter.jsx index 0bed35fc07..38d76ea360 100644 --- a/src/js/components/account/filters/programActivity/ProgramActivityFilter.jsx +++ b/src/js/components/account/filters/programActivity/ProgramActivityFilter.jsx @@ -79,8 +79,7 @@ export default class ProgramActivityFilter extends React.Component { types={keyBy(this.props.availableProgramActivities, 'id')} filterType="Object Class" selectedCheckboxes={this.props.selectedProgramActivities} - toggleCheckboxType={this.toggleValue} - enableAnalytics />); + toggleCheckboxType={this.toggleValue} />); } } }); diff --git a/src/js/components/search/filters/awardAmount/AwardAmountSearch.jsx b/src/js/components/search/filters/awardAmount/AwardAmountSearch.jsx index 38c81e6b35..9508aa9ba1 100644 --- a/src/js/components/search/filters/awardAmount/AwardAmountSearch.jsx +++ b/src/js/components/search/filters/awardAmount/AwardAmountSearch.jsx @@ -65,8 +65,7 @@ export default class AwardAmountSearch extends React.Component { code={key} filterType="Award Amount" selectedCheckboxes={this.props.awardAmounts} - toggleCheckboxType={this.toggleSelection} - enableAnalytics />); + toggleCheckboxType={this.toggleSelection} />); }); return ( diff --git a/src/js/components/search/filters/awardAmount/SpecificAwardAmountItem.jsx b/src/js/components/search/filters/awardAmount/SpecificAwardAmountItem.jsx index 3ee32ce252..03107c25d4 100644 --- a/src/js/components/search/filters/awardAmount/SpecificAwardAmountItem.jsx +++ b/src/js/components/search/filters/awardAmount/SpecificAwardAmountItem.jsx @@ -5,7 +5,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Analytics from 'helpers/analytics/Analytics'; import IndividualSubmit from 'components/search/filters/IndividualSubmit'; @@ -17,14 +16,6 @@ const propTypes = { }; export default class SpecificAwardAmountItem extends React.Component { - static logAmountRangeEvent(range) { - Analytics.event({ - category: 'Search Filter Interaction', - action: 'Applied Award Amount Range Filter', - label: range - }); - } - constructor(props) { super(props); @@ -69,10 +60,6 @@ export default class SpecificAwardAmountItem extends React.Component { }); this.props.searchSpecificRange([min, max]); - - // Analytics - const formattedRange = AwardAmountHelper.formatAwardAmountRange([min, max]); - SpecificAwardAmountItem.logAmountRangeEvent(formattedRange); } render() { diff --git a/src/js/components/search/filters/awardType/AwardType.jsx b/src/js/components/search/filters/awardType/AwardType.jsx index 2dd8d3f260..6f13e32fab 100644 --- a/src/js/components/search/filters/awardType/AwardType.jsx +++ b/src/js/components/search/filters/awardType/AwardType.jsx @@ -65,8 +65,7 @@ export default class AwardType extends React.Component { types={awardTypeCodes} filterType="Award" selectedCheckboxes={this.props.awardType} - bulkTypeChange={this.props.bulkTypeChange} - enableAnalytics />) + bulkTypeChange={this.props.bulkTypeChange} />) )); return ( diff --git a/src/js/components/search/filters/contractFilters/ContractFilter.jsx b/src/js/components/search/filters/contractFilters/ContractFilter.jsx index 14c291842b..c64d6556ff 100644 --- a/src/js/components/search/filters/contractFilters/ContractFilter.jsx +++ b/src/js/components/search/filters/contractFilters/ContractFilter.jsx @@ -88,8 +88,7 @@ export default class ContractFilter extends React.Component { code={invertedFilters[key]} filterType={this.props.contractFilterType} selectedCheckboxes={this.props[this.props.contractFilterState]} - toggleCheckboxType={this.toggleValue} - enableAnalytics />); + toggleCheckboxType={this.toggleValue} />); } }); } diff --git a/src/js/components/search/filters/recipient/RecipientType.jsx b/src/js/components/search/filters/recipient/RecipientType.jsx index 41bd51861d..65ce9772a1 100644 --- a/src/js/components/search/filters/recipient/RecipientType.jsx +++ b/src/js/components/search/filters/recipient/RecipientType.jsx @@ -85,8 +85,7 @@ export default class RecipientType extends React.Component { key={index} types={recipientTypes} filterType="Recipient" - selectedCheckboxes={this.props.selectedTypes} - enableAnalytics /> + selectedCheckboxes={this.props.selectedTypes} /> ) ); diff --git a/src/js/components/search/filters/timePeriod/AllFiscalYears.jsx b/src/js/components/search/filters/timePeriod/AllFiscalYears.jsx index 6791942d87..00e66d460b 100644 --- a/src/js/components/search/filters/timePeriod/AllFiscalYears.jsx +++ b/src/js/components/search/filters/timePeriod/AllFiscalYears.jsx @@ -7,8 +7,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Set } from 'immutable'; -import Analytics from 'helpers/analytics/Analytics'; - import FiscalYear from './FiscalYear'; const propTypes = { @@ -18,14 +16,6 @@ const propTypes = { }; export default class AllFiscalYears extends React.Component { - static logFYEvent(year) { - Analytics.event({ - category: 'Search Filter Interaction', - action: 'Applied Fiscal Year Filter', - label: year - }); - } - constructor(props) { super(props); // bind functions @@ -43,8 +33,6 @@ export default class AllFiscalYears extends React.Component { else { // the year does not yet exist in the set so we are adding newYears = this.props.selectedFY.add(year); - // Analytics - AllFiscalYears.logFYEvent(year); } this.props.updateFilter({ @@ -64,8 +52,6 @@ export default class AllFiscalYears extends React.Component { else { // we need to select all the years newYears = new Set(this.props.timePeriods); - // Analytics - AllFiscalYears.logFYEvent('all'); } this.props.updateFilter({ diff --git a/src/js/components/search/filters/timePeriod/TimePeriod.jsx b/src/js/components/search/filters/timePeriod/TimePeriod.jsx index b7773f30bf..085aac4bf5 100644 --- a/src/js/components/search/filters/timePeriod/TimePeriod.jsx +++ b/src/js/components/search/filters/timePeriod/TimePeriod.jsx @@ -8,8 +8,6 @@ import PropTypes from 'prop-types'; import moment from 'moment'; import { Set } from 'immutable'; -import Analytics from 'helpers/analytics/Analytics'; - import SubmitHint from 'components/sharedComponents/filterSidebar/SubmitHint'; import DateRange from './DateRange'; @@ -36,22 +34,6 @@ const propTypes = { }; export default class TimePeriod extends React.Component { - static logDateRangeEvent(start, end) { - let label = `${start} to ${end}`; - if (!start) { - label = `Through ${end}`; - } - else if (!end) { - label = `On or after ${start}`; - } - - Analytics.event({ - label, - category: 'Search Filter Interaction', - action: 'Applied Date Range Filter' - }); - } - constructor(props) { super(props); @@ -192,10 +174,6 @@ export default class TimePeriod extends React.Component { startDate: start.format('YYYY-MM-DD'), endDate: end.format('YYYY-MM-DD') }); - // Analytics - const startDate = start.format('YYYY-MM-DD'); - const endDate = end.format('YYYY-MM-DD'); - TimePeriod.logDateRangeEvent(startDate, endDate); } } else if (start || end) { diff --git a/src/js/components/sharedComponents/filterSidebar/FilterOption.jsx b/src/js/components/sharedComponents/filterSidebar/FilterOption.jsx index dd2b293236..1b2caa3adc 100644 --- a/src/js/components/sharedComponents/filterSidebar/FilterOption.jsx +++ b/src/js/components/sharedComponents/filterSidebar/FilterOption.jsx @@ -6,7 +6,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Analytics from 'helpers/analytics/Analytics'; import ComingSoonLabel from 'components/sharedComponents/ComingSoonLabel'; import FilterExpandButton from './FilterExpandButton'; @@ -24,14 +23,6 @@ const defaultProps = { }; export default class FilterOption extends React.Component { - static logFilterEvent(name) { - Analytics.event({ - category: 'Search Filters', - action: 'Expanded Filter', - label: name - }); - } - constructor(props) { super(props); @@ -86,8 +77,6 @@ export default class FilterOption extends React.Component { let newArrowState = 'collapsed'; if (newShowState) { newArrowState = 'expanded'; - const filterName = this.props.name; - FilterOption.logFilterEvent(filterName); } this.setState({ isDirty: true, showFilter: newShowState, arrowState: newArrowState diff --git a/src/js/containers/search/SearchSidebarSubmitContainer.jsx b/src/js/containers/search/SearchSidebarSubmitContainer.jsx index 717d056c59..018ed58908 100644 --- a/src/js/containers/search/SearchSidebarSubmitContainer.jsx +++ b/src/js/containers/search/SearchSidebarSubmitContainer.jsx @@ -14,7 +14,7 @@ import { clearAllFilters as clearStagedFilters } from 'redux/actions/search/sear import SearchSidebarSubmit from 'components/search/SearchSidebarSubmit'; -import { convertFiltersToAnalyticEvents, sendAnalyticEvents } from './helpers/searchAnalytics'; +import { convertFiltersToAnalyticEvents, sendAnalyticEvents, sendFieldCombinations } from './helpers/searchAnalytics'; const combinedActions = Object.assign({}, appliedFilterActions, { clearStagedFilters @@ -90,6 +90,7 @@ export class SearchSidebarSubmitContainer extends React.Component { const events = convertFiltersToAnalyticEvents(this.props.stagedFilters); sendAnalyticEvents(events); + sendFieldCombinations(events); } resetFilters() { diff --git a/src/js/containers/search/filters/AgencyContainer.jsx b/src/js/containers/search/filters/AgencyContainer.jsx index ea5a5040f9..0bced4da32 100644 --- a/src/js/containers/search/filters/AgencyContainer.jsx +++ b/src/js/containers/search/filters/AgencyContainer.jsx @@ -9,8 +9,6 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { is } from 'immutable'; -import Analytics from 'helpers/analytics/Analytics'; - import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; import Agency from 'components/search/filters/agency/Agency'; @@ -25,14 +23,6 @@ const propTypes = { }; export class AgencyContainer extends React.Component { - static logAgencyFilterEvent(agencyType, agency) { - Analytics.event({ - category: 'Search Filter Interaction', - action: `Applied ${agencyType} Agency Filter`, - label: agency.toLowerCase() - }); - } - constructor(props) { super(props); @@ -52,15 +42,6 @@ export class AgencyContainer extends React.Component { else { this.props.updateSelectedAwardingAgencies(updateParams); } - - // Analytics - - if (agency.agencyType === 'subtier') { - AgencyContainer.logAgencyFilterEvent(agencyType, agency.subtier_agency.name); - } - else { - AgencyContainer.logAgencyFilterEvent(agencyType, agency.toptier_agency.name); - } } } diff --git a/src/js/containers/search/filters/KeywordContainer.jsx b/src/js/containers/search/filters/KeywordContainer.jsx index 35cba18c36..e1e6f23bce 100644 --- a/src/js/containers/search/filters/KeywordContainer.jsx +++ b/src/js/containers/search/filters/KeywordContainer.jsx @@ -9,8 +9,6 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { is } from 'immutable'; -import Analytics from 'helpers/analytics/Analytics'; - import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; import Keyword from 'components/search/filters/keyword/Keyword'; @@ -22,14 +20,6 @@ const propTypes = { }; export class KeywordContainer extends React.Component { - static logSelectedKeywordEvent(keyword) { - Analytics.event({ - category: 'Search Filter Interaction', - action: 'Applied Keyword Filter', - label: keyword - }); - } - constructor(props) { super(props); @@ -69,11 +59,6 @@ export class KeywordContainer extends React.Component { submitText() { // take in keywords and pass to redux this.props.updateTextSearchInput(this.state.value); - - // Analytics - if (this.state.value) { - KeywordContainer.logSelectedKeywordEvent(this.state.value); - } } removeKeyword() { diff --git a/src/js/containers/search/filters/awardID/AwardIDSearchContainer.jsx b/src/js/containers/search/filters/awardID/AwardIDSearchContainer.jsx index ecf8aa036e..bbe96b0fc5 100644 --- a/src/js/containers/search/filters/awardID/AwardIDSearchContainer.jsx +++ b/src/js/containers/search/filters/awardID/AwardIDSearchContainer.jsx @@ -9,8 +9,6 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { OrderedMap, is } from 'immutable'; -import Analytics from 'helpers/analytics/Analytics'; - import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; import AwardIDSearch from 'components/search/filters/awardID/AwardIDSearch'; @@ -22,14 +20,6 @@ const propTypes = { }; export class AwardIDSearchContainer extends React.Component { - static logIdEvent(id, type) { - Analytics.event({ - category: 'Search Filter Interaction', - action: `Toggled Award ${type} Filter`, - label: id - }); - } - constructor(props) { super(props); @@ -53,19 +43,13 @@ export class AwardIDSearchContainer extends React.Component { [id]: id }) }); - - // Analytics - AwardIDSearchContainer.logIdEvent(id, 'Apply Award ID'); } - removeAwardID(id) { + removeAwardID() { this.props.updateGenericFilter({ type: 'selectedAwardIDs', value: new OrderedMap() }); - - // Analytics - AwardIDSearchContainer.logIdEvent(id, 'Remove Award ID'); } dirtyFilters() { diff --git a/src/js/containers/search/filters/cfda/CFDASearchContainer.jsx b/src/js/containers/search/filters/cfda/CFDASearchContainer.jsx index 517406c3e5..d57ac11e1f 100644 --- a/src/js/containers/search/filters/cfda/CFDASearchContainer.jsx +++ b/src/js/containers/search/filters/cfda/CFDASearchContainer.jsx @@ -9,8 +9,6 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { is } from 'immutable'; -import Analytics from 'helpers/analytics/Analytics'; - import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; import CFDASearch from 'components/search/filters/cfda/CFDASearch'; @@ -22,14 +20,6 @@ const propTypes = { }; export class CFDASearchContainer extends React.Component { - static logCFDAFilterEvent(place) { - Analytics.event({ - category: 'Search Filter Interaction', - action: `Applied CFDA Filter`, - label: place.toLowerCase() - }); - } - constructor(props) { super(props); @@ -44,9 +34,6 @@ export class CFDASearchContainer extends React.Component { const updateParams = {}; updateParams.cfda = cfda; this.props.updateSelectedCFDA(updateParams); - - // Analytics - CFDASearchContainer.logCFDAFilterEvent(cfda.program_number); } } diff --git a/src/js/containers/search/filters/location/POPFilterContainer.jsx b/src/js/containers/search/filters/location/POPFilterContainer.jsx index 70ebcb5051..6f2c3a0e5a 100644 --- a/src/js/containers/search/filters/location/POPFilterContainer.jsx +++ b/src/js/containers/search/filters/location/POPFilterContainer.jsx @@ -8,8 +8,6 @@ import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import Analytics from 'helpers/analytics/Analytics'; - import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; import SelectedLocations from 'components/search/filters/location/SelectedLocations'; @@ -22,14 +20,6 @@ const propTypes = { }; export class POPFilterContainer extends React.Component { - static logLocationFilterEvent(label, event) { - Analytics.event({ - label, - category: 'Search Filter Interaction', - action: `${event} Place of Performance Location Filter` - }); - } - constructor(props) { super(props); @@ -39,7 +29,6 @@ export class POPFilterContainer extends React.Component { addLocation(location) { this.props.addPOPLocationObject(location); - POPFilterContainer.logLocationFilterEvent(location.identifier, 'Applied'); } removeLocation(locationId) { @@ -48,8 +37,6 @@ export class POPFilterContainer extends React.Component { type: 'selectedLocations', value: newValue }); - - POPFilterContainer.logLocationFilterEvent(locationId, 'Removed'); } render() { diff --git a/src/js/containers/search/filters/location/RecipientFilterContainer.jsx b/src/js/containers/search/filters/location/RecipientFilterContainer.jsx index b26e77b096..6c563b8fc8 100644 --- a/src/js/containers/search/filters/location/RecipientFilterContainer.jsx +++ b/src/js/containers/search/filters/location/RecipientFilterContainer.jsx @@ -8,8 +8,6 @@ import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import Analytics from 'helpers/analytics/Analytics'; - import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; import SelectedLocations from 'components/search/filters/location/SelectedLocations'; @@ -22,14 +20,6 @@ const propTypes = { }; export class RecipientFilterContainer extends React.Component { - static logLocationFilterEvent(label, event) { - Analytics.event({ - label, - category: 'Search Filter Interaction', - action: `${event} Recipient Location Filter` - }); - } - constructor(props) { super(props); @@ -39,7 +29,6 @@ export class RecipientFilterContainer extends React.Component { addLocation(location) { this.props.addRecipientLocationObject(location); - RecipientFilterContainer.logLocationFilterEvent(location.identifier, 'Applied'); } removeLocation(locationId) { @@ -48,8 +37,6 @@ export class RecipientFilterContainer extends React.Component { type: 'selectedRecipientLocations', value: newValue }); - - RecipientFilterContainer.logLocationFilterEvent(locationId, 'Removed'); } render() { diff --git a/src/js/containers/search/filters/naics/NAICSSearchContainer.jsx b/src/js/containers/search/filters/naics/NAICSSearchContainer.jsx index 55dfa11263..a0a276d7ec 100644 --- a/src/js/containers/search/filters/naics/NAICSSearchContainer.jsx +++ b/src/js/containers/search/filters/naics/NAICSSearchContainer.jsx @@ -9,8 +9,6 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { is } from 'immutable'; -import Analytics from 'helpers/analytics/Analytics'; - import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; import NAICSSearch from 'components/search/filters/naics/NAICSSearch'; @@ -22,14 +20,6 @@ const propTypes = { }; export class NAICSSearchContainer extends React.Component { - static logPlaceFilterEvent(naics) { - Analytics.event({ - category: 'Search Filter Interaction', - action: `Applied NAICS Filter`, - label: naics.toLowerCase() - }); - } - constructor(props) { super(props); @@ -44,9 +34,6 @@ export class NAICSSearchContainer extends React.Component { const updateParams = {}; updateParams.naics = naics; this.props.updateSelectedNAICS(updateParams); - - // Analytics - NAICSSearchContainer.logPlaceFilterEvent(naics.naics_description); } } diff --git a/src/js/containers/search/filters/psc/PSCSearchContainer.jsx b/src/js/containers/search/filters/psc/PSCSearchContainer.jsx index 89cebee4d7..861b836275 100644 --- a/src/js/containers/search/filters/psc/PSCSearchContainer.jsx +++ b/src/js/containers/search/filters/psc/PSCSearchContainer.jsx @@ -9,8 +9,6 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { is } from 'immutable'; -import Analytics from 'helpers/analytics/Analytics'; - import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; import PSCSearch from 'components/search/filters/psc/PSCSearch'; @@ -22,14 +20,6 @@ const propTypes = { }; export class PSCSearchContainer extends React.Component { - static logPSCFilterEvent(psc) { - Analytics.event({ - category: 'Search Filter Interaction', - action: `Applied PSC Filter`, - label: psc - }); - } - constructor(props) { super(props); @@ -44,9 +34,6 @@ export class PSCSearchContainer extends React.Component { const updateParams = {}; updateParams.psc = psc; this.props.updateSelectedPSC(updateParams); - - // Analytics - PSCSearchContainer.logPSCFilterEvent(psc); } } diff --git a/src/js/containers/search/filters/recipient/RecipientSearchContainer.jsx b/src/js/containers/search/filters/recipient/RecipientSearchContainer.jsx index f7f3c386c0..26783317ab 100644 --- a/src/js/containers/search/filters/recipient/RecipientSearchContainer.jsx +++ b/src/js/containers/search/filters/recipient/RecipientSearchContainer.jsx @@ -9,8 +9,6 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { is } from 'immutable'; -import Analytics from 'helpers/analytics/Analytics'; - import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; import RecipientSearch from 'components/search/filters/recipient/RecipientSearch'; @@ -22,14 +20,6 @@ const propTypes = { }; export class RecipientSearchContainer extends React.Component { - static logRecipientFilterEvent(name) { - Analytics.event({ - category: 'Search Filter Interaction', - action: 'Applied Recipient Name/DUNS Filter', - label: name.toLowerCase() - }); - } - constructor(props) { super(props); @@ -39,9 +29,6 @@ export class RecipientSearchContainer extends React.Component { toggleRecipient(recipient) { this.props.updateSelectedRecipients(recipient); - - // Analytics - RecipientSearchContainer.logRecipientFilterEvent(recipient); } dirtyFilters() { diff --git a/src/js/containers/search/filters/recipient/RecipientTypeContainer.jsx b/src/js/containers/search/filters/recipient/RecipientTypeContainer.jsx index b7ad0eb129..7e408d9792 100644 --- a/src/js/containers/search/filters/recipient/RecipientTypeContainer.jsx +++ b/src/js/containers/search/filters/recipient/RecipientTypeContainer.jsx @@ -12,7 +12,6 @@ import { connect } from 'react-redux'; import { keyBy } from 'lodash'; import { recipientTypeGroups } from 'dataMapping/search/recipientType'; -import Analytics from 'helpers/analytics/Analytics'; import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; @@ -26,14 +25,6 @@ const propTypes = { }; export class RecipientTypeContainer extends React.Component { - static logLocationFilterEvent(placeType, place, event) { - Analytics.event({ - category: 'Search Filter Interaction', - action: `${event} Recipient ${placeType.toLowerCase()} Filter`, - label: place.toLowerCase() - }); - } - constructor(props) { super(props); diff --git a/src/js/containers/search/helpers/searchAnalytics.js b/src/js/containers/search/helpers/searchAnalytics.js index 276dff72d5..11939b4d3d 100644 --- a/src/js/containers/search/helpers/searchAnalytics.js +++ b/src/js/containers/search/helpers/searchAnalytics.js @@ -4,6 +4,7 @@ */ import { Set } from 'immutable'; +import { uniq } from 'lodash'; import { awardTypeCodes } from 'dataMapping/search/awardType'; import { recipientTypes, groupLabels } from 'dataMapping/search/recipientType'; import { @@ -199,12 +200,25 @@ export const convertFiltersToAnalyticEvents = (redux) => { }; export const sendAnalyticEvents = (events) => { - console.time('analytic'); events.forEach((event) => { Analytics.event(Object.assign({}, event, { category: eventCategory })); }); - console.timeEnd('analytic'); }; +export const sendFieldCombinations = (events) => { + // record the filter field combinations that were selected + // extract the action label from each event and then eliminate duplicates + const fields = uniq(events.reduce((parsed, event) => { + if (event.action) { + parsed.push(event.action); + } + return parsed; + }, [])); + + Analytics.event({ + category: 'Applied Search Fields', + action: fields.sort().join('-') + }); +}; From 7da0c7734c5cd9421b0737c59d1b14080059d46d Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Mon, 5 Feb 2018 16:41:02 -0500 Subject: [PATCH 33/89] Improved magnifying glass icon alignment for larger screen sizes; added ellipsis to placeholder text --- src/_scss/pages/keyword/_searchBar.scss | 13 ++++++++++--- src/js/components/keyword/KeywordSearchBar.jsx | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/_scss/pages/keyword/_searchBar.scss b/src/_scss/pages/keyword/_searchBar.scss index b70cc56f5e..88a9741fe2 100644 --- a/src/_scss/pages/keyword/_searchBar.scss +++ b/src/_scss/pages/keyword/_searchBar.scss @@ -15,7 +15,6 @@ margin-right: rem(15); background-color: $color-white; @include media($medium-screen) { - padding-right: rem(10); width: 50%; } .keyword-search-bar__input { @@ -24,15 +23,23 @@ width: 85%; border: 0; padding: rem(15); - font-size: rem(28); + font-size: rem(20); font-weight: 300; - line-height: rem(36); + @include media($tablet-screen) { + width: 90%; + font-size: rem(28); + line-height: rem(36); + } } .keyword-search-bar__button { @include button-unstyled; height: rem(66); float: right; width: 15%; + padding-right: rem(15); + @include media($tablet-screen) { + width: 10%; + } .keyword-search-bar__button-icon { height: rem(30); width: rem(30); diff --git a/src/js/components/keyword/KeywordSearchBar.jsx b/src/js/components/keyword/KeywordSearchBar.jsx index ea83b4ff99..b2b3992c88 100644 --- a/src/js/components/keyword/KeywordSearchBar.jsx +++ b/src/js/components/keyword/KeywordSearchBar.jsx @@ -54,7 +54,7 @@ export default class KeywordSearchBar extends React.Component { className="keyword-search-bar__input" value={this.state.searchString} onChange={this.changedInput} - placeholder="Type keywords" /> + placeholder="Type keywords..." /> +
  • +
  • ); let lastButton = ( -
  • - +
  • +
  • ); if (totalPages < 5) { @@ -117,8 +125,14 @@ export default class Pagination extends React.Component { generatePageButtons(pages, totalPages) { return (pages.map((page, index) => ( -
  • - +
  • +
  • ) )); @@ -142,20 +156,26 @@ export default class Pagination extends React.Component { return (
    -
    +
    {resultsText}
      -
    • - +
    • +
    • {pager.firstButton} {pager.prevEllipses} {pageButtons} {pager.nextEllipses} {pager.lastButton} -
    • - +
    • +
    From 6e483773bd333624bfa5f3e087a259c9c31979da Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Tue, 6 Feb 2018 14:45:59 -0500 Subject: [PATCH 42/89] Switch from floats to flexbox --- src/_scss/pages/keyword/_searchBar.scss | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/_scss/pages/keyword/_searchBar.scss b/src/_scss/pages/keyword/_searchBar.scss index 9779a56463..fa290e499d 100644 --- a/src/_scss/pages/keyword/_searchBar.scss +++ b/src/_scss/pages/keyword/_searchBar.scss @@ -14,6 +14,8 @@ border: 1px solid $color-gray-light; margin-right: rem(15); background-color: $color-white; + display: flex; + align-items: center; @include media($medium-screen) { width: 50%; } @@ -35,9 +37,11 @@ .keyword-search-bar__button { @include button-unstyled; height: rem(66); - float: right; width: 20%; padding-right: rem(10); + display: flex; + align-items: center; + justify-content: flex-end; @include media($tablet-screen) { padding-right: rem(15); width: 15%; @@ -45,7 +49,6 @@ .keyword-search-bar__button-icon { height: rem(30); width: rem(30); - float: right; svg { fill: $color-gray; } From 3eb312d184e23045251c1cd966a4db519108412c Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Tue, 6 Feb 2018 17:26:40 -0500 Subject: [PATCH 43/89] Implement new events --- .../visualizations/VisualizationWrapper.jsx | 8 +++++++ .../containers/keyword/KeywordContainer.jsx | 7 +++++++ .../keyword/table/ResultsTableContainer.jsx | 6 ++++++ src/js/containers/search/SearchContainer.jsx | 10 +++++++++ .../search/SearchSidebarSubmitContainer.jsx | 6 +++++- .../search/helpers/searchAnalytics.js | 4 ++-- .../search/table/ResultsTableContainer.jsx | 21 ++++--------------- .../geo/GeoVisualizationSectionContainer.jsx | 21 +++++++++++++++++++ .../TimeVisualizationSectionContainer.jsx | 10 +++++++++ .../search/helpers/searchAnalytics-test.js | 4 ++-- 10 files changed, 75 insertions(+), 22 deletions(-) diff --git a/src/js/components/search/visualizations/VisualizationWrapper.jsx b/src/js/components/search/visualizations/VisualizationWrapper.jsx index 12872fedac..be13af867e 100644 --- a/src/js/components/search/visualizations/VisualizationWrapper.jsx +++ b/src/js/components/search/visualizations/VisualizationWrapper.jsx @@ -6,6 +6,8 @@ import React from 'react'; import PropTypes from 'prop-types'; +import Analytics from 'helpers/analytics/Analytics'; + import ResultsTableContainer from 'containers/search/table/ResultsTableContainer'; import TimeVisualizationSectionContainer from 'containers/search/visualizations/time/TimeVisualizationSectionContainer'; @@ -56,6 +58,12 @@ export default class VisualizationWrapper extends React.Component { clickedTab(tab) { this.setState({ active: tab + }, () => { + const activeLabel = tabOptions.find((el) => el.code === tab).label; + Analytics.event({ + category: 'Advanced Search - Visualization Type', + action: activeLabel + }); }); } diff --git a/src/js/containers/keyword/KeywordContainer.jsx b/src/js/containers/keyword/KeywordContainer.jsx index a77bc3c6ba..0ed58fc9dd 100644 --- a/src/js/containers/keyword/KeywordContainer.jsx +++ b/src/js/containers/keyword/KeywordContainer.jsx @@ -9,6 +9,8 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { isCancel } from 'axios'; +import Analytics from 'helpers/analytics/Analytics'; + import * as bulkDownloadActions from 'redux/actions/bulkDownload/bulkDownloadActions'; import * as BulkDownloadHelper from 'helpers/bulkDownloadHelper'; import * as KeywordHelper from 'helpers/keywordHelper'; @@ -141,6 +143,11 @@ export class KeywordContainer extends React.Component { updateKeyword(keyword) { this.setState({ keyword + }, () => { + Analytics.event({ + category: 'Keyword Search - Keyword', + action: keyword + }); }); } diff --git a/src/js/containers/keyword/table/ResultsTableContainer.jsx b/src/js/containers/keyword/table/ResultsTableContainer.jsx index 62467d7d86..82e5cad0c5 100644 --- a/src/js/containers/keyword/table/ResultsTableContainer.jsx +++ b/src/js/containers/keyword/table/ResultsTableContainer.jsx @@ -12,6 +12,8 @@ import { availableColumns, defaultSort } from 'dataMapping/keyword/resultsTableC import { awardTypeGroups } from 'dataMapping/search/awardType'; import { measureTableHeader } from 'helpers/textMeasurement'; +import Analytics from 'helpers/analytics/Analytics'; + import ResultsTableSection from 'components/keyword/table/ResultsTableSection'; const propTypes = { @@ -238,6 +240,10 @@ export default class ResultsTableContainer extends React.Component { // Don't perform a search yet if user switches tabs before entering a keyword if (this.props.keyword) { this.performSearch(true); + Analytics.event({ + category: 'Keyword Search - Table Tab', + action: tab + }); } }); } diff --git a/src/js/containers/search/SearchContainer.jsx b/src/js/containers/search/SearchContainer.jsx index e12e93df37..599fb228ca 100644 --- a/src/js/containers/search/SearchContainer.jsx +++ b/src/js/containers/search/SearchContainer.jsx @@ -12,6 +12,11 @@ import { is } from 'immutable'; import moment from 'moment'; import Router from 'containers/router/Router'; +import { + convertFiltersToAnalyticEvents, + sendAnalyticEvents, + sendFieldCombinations +} from './helpers/searchAnalytics'; import { filterStoreVersion, requiredTypes, initialState } from 'redux/reducers/search/searchFiltersReducer'; @@ -232,6 +237,11 @@ export class SearchContainer extends React.Component { // apply the filters to both the staged and applied stores this.props.restoreHashedFilters(reduxValues); + // send the prepopulated filters (received from the hash) to Google Analytics + const events = convertFiltersToAnalyticEvents(reduxValues); + sendAnalyticEvents(events); + sendFieldCombinations(events); + this.setState({ hashState: 'ready' }); diff --git a/src/js/containers/search/SearchSidebarSubmitContainer.jsx b/src/js/containers/search/SearchSidebarSubmitContainer.jsx index 018ed58908..d493e9f4e4 100644 --- a/src/js/containers/search/SearchSidebarSubmitContainer.jsx +++ b/src/js/containers/search/SearchSidebarSubmitContainer.jsx @@ -14,7 +14,11 @@ import { clearAllFilters as clearStagedFilters } from 'redux/actions/search/sear import SearchSidebarSubmit from 'components/search/SearchSidebarSubmit'; -import { convertFiltersToAnalyticEvents, sendAnalyticEvents, sendFieldCombinations } from './helpers/searchAnalytics'; +import { + convertFiltersToAnalyticEvents, + sendAnalyticEvents, + sendFieldCombinations +} from './helpers/searchAnalytics'; const combinedActions = Object.assign({}, appliedFilterActions, { clearStagedFilters diff --git a/src/js/containers/search/helpers/searchAnalytics.js b/src/js/containers/search/helpers/searchAnalytics.js index 92556706f0..7434b9f686 100644 --- a/src/js/containers/search/helpers/searchAnalytics.js +++ b/src/js/containers/search/helpers/searchAnalytics.js @@ -15,7 +15,7 @@ import { import Analytics from 'helpers/analytics/Analytics'; -const eventCategory = 'Applied Search Filter'; +const eventCategory = 'Advanced Search - Search Filter'; export const convertDateRange = (range) => { if (range.length !== 2) { @@ -218,7 +218,7 @@ export const sendFieldCombinations = (events) => { }, [])); Analytics.event({ - category: 'Applied Search Fields', + category: 'Advanced Search - Search Fields', action: fields.sort().join('-') }); }; diff --git a/src/js/containers/search/table/ResultsTableContainer.jsx b/src/js/containers/search/table/ResultsTableContainer.jsx index 80228692e8..69d464a871 100644 --- a/src/js/containers/search/table/ResultsTableContainer.jsx +++ b/src/js/containers/search/table/ResultsTableContainer.jsx @@ -65,20 +65,6 @@ const tableTypes = [ ]; export class ResultsTableContainer extends React.Component { - static logLoadNextPageEvent(page, tableType) { - // Get the display name for the current table type - const currentType = tableTypes.filter((type) => - type.internal === tableType - ); - const typeLabel = currentType[0].label; - - Analytics.event({ - category: 'Search Page Spending By Award', - action: `Scrolled to next page of ${typeLabel}`, - label: page - }); - } - constructor(props) { super(props); @@ -399,6 +385,10 @@ export class ResultsTableContainer extends React.Component { this.setState(newState, () => { this.performSearch(true); + Analytics.event({ + category: 'Advanced Search - Table Tab', + action: tab + }); }); } @@ -411,9 +401,6 @@ export class ResultsTableContainer extends React.Component { // check if more pages are available if (!this.state.lastPage) { - // Analytics - ResultsTableContainer.logLoadNextPageEvent(`${this.state.page + 1}`, this.state.tableType); - // more pages are available, load them this.setState({ page: this.state.page + 1 diff --git a/src/js/containers/search/visualizations/geo/GeoVisualizationSectionContainer.jsx b/src/js/containers/search/visualizations/geo/GeoVisualizationSectionContainer.jsx index b3172ff3f0..7295d36bb2 100644 --- a/src/js/containers/search/visualizations/geo/GeoVisualizationSectionContainer.jsx +++ b/src/js/containers/search/visualizations/geo/GeoVisualizationSectionContainer.jsx @@ -18,6 +18,7 @@ import { setAppliedFilterCompletion } from 'redux/actions/search/appliedFilterAc import * as SearchHelper from 'helpers/searchHelper'; import MapBroadcaster from 'helpers/mapBroadcaster'; +import Analytics from 'helpers/analytics/Analytics'; import SearchAwardsOperation from 'models/search/SearchAwardsOperation'; @@ -34,6 +35,20 @@ const apiScopes = { congressionalDistrict: 'district' }; +const logMapLayerEvent = (layer) => { + Analytics.event({ + category: 'Advanced Search - Map - Map Layer', + action: layer + }); +}; + +const logMapScopeEvent = (scope) => { + Analytics.event({ + category: 'Advanced Search - Map - Location Type', + action: scope + }); +}; + export class GeoVisualizationSectionContainer extends React.Component { constructor(props) { super(props); @@ -70,6 +85,10 @@ export class GeoVisualizationSectionContainer extends React.Component { this.mapListeners.push(measureListener); const movedListener = MapBroadcaster.on('mapMoved', this.prepareFetch); this.mapListeners.push(movedListener); + + // log the initial event + logMapScopeEvent(this.state.scope); + logMapLayerEvent(this.state.mapLayer); } componentDidUpdate(prevProps) { @@ -95,6 +114,7 @@ export class GeoVisualizationSectionContainer extends React.Component { scope }, () => { this.prepareFetch(true); + logMapScopeEvent(scope); }); } @@ -259,6 +279,7 @@ export class GeoVisualizationSectionContainer extends React.Component { loadingTiles: true }, () => { this.prepareFetch(true); + logMapLayerEvent(layer); }); } diff --git a/src/js/containers/search/visualizations/time/TimeVisualizationSectionContainer.jsx b/src/js/containers/search/visualizations/time/TimeVisualizationSectionContainer.jsx index f9daa3914e..9a82247e14 100644 --- a/src/js/containers/search/visualizations/time/TimeVisualizationSectionContainer.jsx +++ b/src/js/containers/search/visualizations/time/TimeVisualizationSectionContainer.jsx @@ -18,6 +18,7 @@ import { setAppliedFilterCompletion } from 'redux/actions/search/appliedFilterAc import * as SearchHelper from 'helpers/searchHelper'; import * as MonthHelper from 'helpers/monthHelper'; +import Analytics from 'helpers/analytics/Analytics'; import SearchAwardsOperation from 'models/search/SearchAwardsOperation'; @@ -31,6 +32,13 @@ const propTypes = { noApplied: PropTypes.bool }; +const logPeriodEvent = (period) => { + Analytics.event({ + category: 'Advanced Search - Time - Period', + action: period + }); +}; + export class TimeVisualizationSectionContainer extends React.Component { constructor(props) { super(props); @@ -50,6 +58,7 @@ export class TimeVisualizationSectionContainer extends React.Component { componentDidMount() { this.fetchData(); + logPeriodEvent(this.state.visualizationPeriod); } componentDidUpdate(prevProps) { @@ -63,6 +72,7 @@ export class TimeVisualizationSectionContainer extends React.Component { visualizationPeriod }, () => { this.fetchData(); + logPeriodEvent(visualizationPeriod); }); } diff --git a/tests/containers/search/helpers/searchAnalytics-test.js b/tests/containers/search/helpers/searchAnalytics-test.js index 481529bb2b..328b3ab633 100644 --- a/tests/containers/search/helpers/searchAnalytics-test.js +++ b/tests/containers/search/helpers/searchAnalytics-test.js @@ -238,7 +238,7 @@ describe('searchAnalytics', () => { searchAnalytics.sendAnalyticEvents(events); expect(Analytics.event).toHaveBeenCalledTimes(1); expect(Analytics.event).toHaveBeenCalledWith({ - category: 'Applied Search Filter', + category: 'Advanced Search - Search Filter', action: 'action', label: 'label' }); @@ -265,7 +265,7 @@ describe('searchAnalytics', () => { searchAnalytics.sendFieldCombinations(events); expect(Analytics.event).toHaveBeenCalledTimes(1); expect(Analytics.event).toHaveBeenCalledWith({ - category: 'Applied Search Fields', + category: 'Advanced Search - Search Fields', action: 'action-z' }); }); From 68a064ceb54e25ca777af52f083ce107567b3fea Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Wed, 7 Feb 2018 09:06:43 -0500 Subject: [PATCH 44/89] Don't log visualization tab unless user has stayed there for 15 seconds --- .../visualizations/VisualizationWrapper.jsx | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/src/js/components/search/visualizations/VisualizationWrapper.jsx b/src/js/components/search/visualizations/VisualizationWrapper.jsx index be13af867e..2aea4905ed 100644 --- a/src/js/components/search/visualizations/VisualizationWrapper.jsx +++ b/src/js/components/search/visualizations/VisualizationWrapper.jsx @@ -52,18 +52,49 @@ export default class VisualizationWrapper extends React.Component { active: 'table' }; + this._queuedAnalyticEvent = null; + this.clickedTab = this.clickedTab.bind(this); + this.logVisualizationTab = this.logVisualizationTab.bind(this); + } + + componentDidMount() { + this._mounted = true; + this.logVisualizationTab(this.state.active); + } + + componentWillUnmount() { + this._mounted = false; + } + + logVisualizationTab(tab) { + if (this.props.noFiltersApplied) { + // no filters are applied yet, don't log an analytic event + return; + } + + // discard any previously scheduled tab analytic events that haven't run yet + if (this._queuedAnalyticEvent) { + window.clearTimeout(this._queuedAnalyticEvent); + } + + // only log analytic event after 15 seconds + this._queuedAnalyticEvent = window.setTimeout(() => { + if (this._mounted) { + const activeLabel = tabOptions.find((el) => el.code === tab).label; + Analytics.event({ + category: 'Advanced Search - Visualization Type', + action: activeLabel + }); + } + }, 15 * 1000); } clickedTab(tab) { this.setState({ active: tab }, () => { - const activeLabel = tabOptions.find((el) => el.code === tab).label; - Analytics.event({ - category: 'Advanced Search - Visualization Type', - action: activeLabel - }); + this.logVisualizationTab(tab); }); } From f2386e33481c6c525934b24b102a6fa131b08c86 Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Wed, 7 Feb 2018 09:13:39 -0500 Subject: [PATCH 45/89] Use flexbox mixins --- src/_scss/pages/keyword/_searchBar.scss | 26 ++++++++++--------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/_scss/pages/keyword/_searchBar.scss b/src/_scss/pages/keyword/_searchBar.scss index fa290e499d..01831c2e69 100644 --- a/src/_scss/pages/keyword/_searchBar.scss +++ b/src/_scss/pages/keyword/_searchBar.scss @@ -10,42 +10,35 @@ } .keyword-search-bar__form { - width: 100%; border: 1px solid $color-gray-light; margin-right: rem(15); background-color: $color-white; - display: flex; - align-items: center; + @include display(flex); + @include flex-direction(row); + @include align-items(center); + @include justify-content(flex-start); @include media($medium-screen) { + @include flex(1 1 auto); width: 50%; } .keyword-search-bar__input { + @include flex(1 1 auto); margin-right: 0; height: rem(66); - width: 80%; border: 0; - padding: rem(10); + padding: rem(15); font-size: rem(20); font-weight: 300; @include media($tablet-screen) { - width: 85%; font-size: rem(28); line-height: rem(36); - padding: rem(15); } } .keyword-search-bar__button { + @include flex(0 0 auto); @include button-unstyled; height: rem(66); - width: 20%; - padding-right: rem(10); - display: flex; - align-items: center; - justify-content: flex-end; - @include media($tablet-screen) { - padding-right: rem(15); - width: 15%; - } + padding: 0 rem(15); .keyword-search-bar__button-icon { height: rem(30); width: rem(30); @@ -72,6 +65,7 @@ padding-top: rem(10); @include media($medium-screen) { width: 50%; + @include flex(1 1 auto); padding-top: 0; padding-left: rem(10); } From 9143f61880d01ce11e69a3455e0f757584c2be7a Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Wed, 7 Feb 2018 09:28:30 -0500 Subject: [PATCH 46/89] Update about page content --- src/js/components/about/AboutContent.jsx | 6 +++ src/js/components/about/DataQuality.jsx | 28 +------------- src/js/components/about/MoreInfo.jsx | 48 ++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 26 deletions(-) create mode 100644 src/js/components/about/MoreInfo.jsx diff --git a/src/js/components/about/AboutContent.jsx b/src/js/components/about/AboutContent.jsx index 5f52085454..e4e04ad0bb 100644 --- a/src/js/components/about/AboutContent.jsx +++ b/src/js/components/about/AboutContent.jsx @@ -13,6 +13,7 @@ import Mission from './Mission'; import Background from './Background'; import DataSources from './DataSources'; import DataQuality from './DataQuality'; +import MoreInfo from './MoreInfo'; import Contact from './Contact'; const aboutSections = [ @@ -32,6 +33,10 @@ const aboutSections = [ section: 'data-quality', label: 'Data Quality' }, + { + section: 'more-info', + label: 'More Information' + }, { section: 'contact', label: 'Contact' @@ -222,6 +227,7 @@ export default class AboutContent extends React.Component { +
    diff --git a/src/js/components/about/DataQuality.jsx b/src/js/components/about/DataQuality.jsx index 5034f6bcfe..296624f65a 100644 --- a/src/js/components/about/DataQuality.jsx +++ b/src/js/components/about/DataQuality.jsx @@ -59,7 +59,8 @@ export default class DataQuality extends React.Component { Federal Government Procurement Data Quality Summary  about data submitted by the agencies to the Federal Procurement - Data System (FPDS). + Data System (FPDS). In addition, the federal agencies' raw quarterly submission files, including Quarterly + Assurance Statements about the data, are available here.

    Additionally, the Inspector General of each agency must issue reports to @@ -73,31 +74,6 @@ export default class DataQuality extends React.Component {  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 Quarterly Assurance Statements about the data, are available  - - here - - . -

    ); diff --git a/src/js/components/about/MoreInfo.jsx b/src/js/components/about/MoreInfo.jsx new file mode 100644 index 0000000000..8449dc1619 --- /dev/null +++ b/src/js/components/about/MoreInfo.jsx @@ -0,0 +1,48 @@ +/** + * MoreInfo.jsx + * Created by Kevin Li 2/7/18 + */ + +import React from 'react'; + +const MoreInfo = () => ( +
    +
    +

    More Information

    +
    +
    +

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

    +

    + You can also see an interactive report on how frequently federal agencies use + competitive practices when issuing contracts for goods and services in our  + + Data Lab + + . +

    +
    +
    +); + +export default MoreInfo; From b59b5cc86666bd626b9f31e851f68e496df7d79f Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Wed, 7 Feb 2018 10:34:31 -0500 Subject: [PATCH 47/89] Use sticky headers; updated search bar styling --- .../pages/accountLanding/header/header.scss | 2 +- .../pages/accountLanding/searchSection.scss | 53 ++++++++----------- .../pages/agencyLanding/header/header.scss | 2 +- .../accountLanding/AccountLandingPage.jsx | 10 +++- .../AccountLandingSearchBar.jsx | 4 +- .../header/AccountLandingHeader.jsx | 26 --------- .../agencyLanding/AgencyLandingPage.jsx | 10 +++- .../agencyLanding/AgencyLandingSearchBar.jsx | 4 +- .../header/AgencyLandingHeader.jsx | 26 --------- 9 files changed, 47 insertions(+), 90 deletions(-) delete mode 100644 src/js/components/accountLanding/header/AccountLandingHeader.jsx delete mode 100644 src/js/components/agencyLanding/header/AgencyLandingHeader.jsx diff --git a/src/_scss/pages/accountLanding/header/header.scss b/src/_scss/pages/accountLanding/header/header.scss index 51ec9e11e4..06763c1ffb 100644 --- a/src/_scss/pages/accountLanding/header/header.scss +++ b/src/_scss/pages/accountLanding/header/header.scss @@ -1 +1 @@ -@import "layouts/tabbedSearch/header/header"; \ No newline at end of file +@import "layouts/default/stickyHeader/header"; \ No newline at end of file diff --git a/src/_scss/pages/accountLanding/searchSection.scss b/src/_scss/pages/accountLanding/searchSection.scss index a195bdf157..d43320e656 100644 --- a/src/_scss/pages/accountLanding/searchSection.scss +++ b/src/_scss/pages/accountLanding/searchSection.scss @@ -1,45 +1,38 @@ .search-section { background-color: $color-primary-alt-lightest; - padding: rem(5); + padding: rem(10); border: 1px solid $color-vis-lightest; text-align: center; + @include media($medium-screen) { + padding: rem(38); + } .search-section__form { - position: relative; + @include display(flex); + @include flex-direction(row); + @include align-items(center); + @include justify-content(flex-start); + background-color: $color-white; .search-section__input { + @include flex(1 1 auto); color: $color-gray-light; font-weight: 300; line-height: rem(28); - padding: 0 rem(30) 0 rem(4); - width: 100%; + padding: rem(10) rem(15); + border: 0; + @include media($medium-screen) { + font-size: rem(22); + } } .search-section__button { - position: absolute; - top: rem(5); - left: 90%; - + @include flex(0 0 auto); @include button-unstyled; - width: 25px; - height: 25px; - - svg { - fill: $color-gray-light; - } - } - } - @include media($medium-screen) { - padding: rem(30); - .search-section__form { - .search-section__input { - width: 75%; - font-size: rem(22); - padding: rem(15); - padding-right: rem(30); - } - .search-section__button { - left: 80%; - top: rem(15); - width: 36px; - height: 36px; + padding: 0 rem(15); + .search-section__button-icon { + height: rem(24); + width: rem(24); + svg { + fill: $color-gray-light; + } } } } diff --git a/src/_scss/pages/agencyLanding/header/header.scss b/src/_scss/pages/agencyLanding/header/header.scss index 51ec9e11e4..06763c1ffb 100644 --- a/src/_scss/pages/agencyLanding/header/header.scss +++ b/src/_scss/pages/agencyLanding/header/header.scss @@ -1 +1 @@ -@import "layouts/tabbedSearch/header/header"; \ No newline at end of file +@import "layouts/default/stickyHeader/header"; \ No newline at end of file diff --git a/src/js/components/accountLanding/AccountLandingPage.jsx b/src/js/components/accountLanding/AccountLandingPage.jsx index 03adb5b96b..8219746ff0 100644 --- a/src/js/components/accountLanding/AccountLandingPage.jsx +++ b/src/js/components/accountLanding/AccountLandingPage.jsx @@ -8,10 +8,10 @@ import React from 'react'; import { accountLandingPageMetaTags } from 'helpers/metaTagHelper'; import MetaTags from 'components/sharedComponents/metaTags/MetaTags'; +import StickyHeader from 'components/sharedComponents/stickyHeader/StickyHeader'; import Header from 'components/sharedComponents/header/Header'; import Footer from 'components/sharedComponents/Footer'; import AccountLandingContainer from 'containers/accountLanding/AccountLandingContainer'; -import AccountLandingHeader from './header/AccountLandingHeader'; export default class AccountLandingPage extends React.Component { render() { @@ -19,7 +19,13 @@ export default class AccountLandingPage extends React.Component {
    - + +
    +

    + Federal Account Profiles +

    +
    +
    diff --git a/src/js/components/accountLanding/AccountLandingSearchBar.jsx b/src/js/components/accountLanding/AccountLandingSearchBar.jsx index 2a9b384db3..238e3d473e 100644 --- a/src/js/components/accountLanding/AccountLandingSearchBar.jsx +++ b/src/js/components/accountLanding/AccountLandingSearchBar.jsx @@ -30,7 +30,9 @@ export default class AccountLandingSearchBar extends React.Component {
    diff --git a/src/js/components/accountLanding/header/AccountLandingHeader.jsx b/src/js/components/accountLanding/header/AccountLandingHeader.jsx deleted file mode 100644 index d88e26d474..0000000000 --- a/src/js/components/accountLanding/header/AccountLandingHeader.jsx +++ /dev/null @@ -1,26 +0,0 @@ -/** - * AccountLandingHeader.jsx - * Created by Lizzie Salita 8/4/17 - */ - -import React from 'react'; - -export default class AccountLandingHeader extends React.Component { - render() { - return ( -
    -
    -
    -
    -

    - Federal Account Profiles -

    -
    -
    -
    -
    - ); - } -} diff --git a/src/js/components/agencyLanding/AgencyLandingPage.jsx b/src/js/components/agencyLanding/AgencyLandingPage.jsx index 651d9c60d8..8187a67239 100644 --- a/src/js/components/agencyLanding/AgencyLandingPage.jsx +++ b/src/js/components/agencyLanding/AgencyLandingPage.jsx @@ -9,9 +9,9 @@ import { agencyLandingPageMetaTags } from 'helpers/metaTagHelper'; import MetaTags from 'components/sharedComponents/metaTags/MetaTags'; import Header from 'components/sharedComponents/header/Header'; +import StickyHeader from 'components/sharedComponents/stickyHeader/StickyHeader'; import Footer from 'components/sharedComponents/Footer'; import AgencyLandingContainer from 'containers/agencyLanding/AgencyLandingContainer'; -import AgencyLandingHeader from './header/AgencyLandingHeader'; require('pages/agencyLanding/agencyLandingPage.scss'); @@ -21,7 +21,13 @@ export default class AgencyLandingPage extends React.Component {
    - + +
    +

    + Agency Profiles +

    +
    +
    diff --git a/src/js/components/agencyLanding/AgencyLandingSearchBar.jsx b/src/js/components/agencyLanding/AgencyLandingSearchBar.jsx index 144843c733..dd9495ac5e 100644 --- a/src/js/components/agencyLanding/AgencyLandingSearchBar.jsx +++ b/src/js/components/agencyLanding/AgencyLandingSearchBar.jsx @@ -30,7 +30,9 @@ export default class AgencyLandingSearchBar extends React.Component {
    diff --git a/src/js/components/agencyLanding/header/AgencyLandingHeader.jsx b/src/js/components/agencyLanding/header/AgencyLandingHeader.jsx deleted file mode 100644 index 59427096e2..0000000000 --- a/src/js/components/agencyLanding/header/AgencyLandingHeader.jsx +++ /dev/null @@ -1,26 +0,0 @@ -/** - * AgencyLandingHeader.jsx - * Created by Lizzie Salita 7/7/17 - */ - -import React from 'react'; - -export default class AgencyLandingHeader extends React.Component { - render() { - return ( -
    -
    -
    -
    -

    - Agency Profiles -

    -
    -
    -
    -
    - ); - } -} From 95392fee4dbb61a4f17633a4989acd0076b0a101 Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Wed, 7 Feb 2018 12:32:25 -0500 Subject: [PATCH 48/89] Updated headers in the Spending Explorer, Agency Profile Landing Page, Agency Profile Page, Federal Account Profile Page, About Page, and Award Summary Pages with the Sticky Header, added functionality to the Sticky Sidebar to handle interactions with the Sticky Header, updated Award Profile pages (Contract, Grant, etc.) to display "Summary" instead of "Profile", removed unneeded CSS, removed unneeded Headers --- src/_scss/components/pageTitleBar/_title.scss | 13 --------- .../components/pageTitleBar/pageTitleBar.scss | 24 ----------------- src/_scss/layouts/summary/summary.scss | 3 ++- src/_scss/pages/about/_sidebar.scss | 2 +- src/_scss/pages/about/aboutPage.scss | 4 +-- src/_scss/pages/account/accountPage.scss | 3 ++- src/_scss/pages/account/header/header.scss | 5 ---- src/_scss/pages/agency/_sidebar.scss | 2 +- src/_scss/pages/agency/agencyPage.scss | 4 +-- src/_scss/pages/agency/header/header.scss | 1 - .../agencyLanding/agencyLandingPage.scss | 2 +- .../pages/agencyLanding/header/header.scss | 1 - src/_scss/pages/award/awardPage.scss | 4 +-- src/_scss/pages/award/summaryBar.scss | 5 ---- .../pages/bulkDownload/bulkDownloadPage.scss | 2 +- .../pages/bulkDownload/header/header.scss | 1 - src/_scss/pages/explorer/explorerPage.scss | 2 +- src/_scss/pages/search/searchPage.scss | 1 + src/js/components/about/About.jsx | 10 +++++-- src/js/components/about/AboutContent.jsx | 6 +++-- src/js/components/about/AboutHeader.jsx | 20 -------------- src/js/components/account/Account.jsx | 2 -- src/js/components/account/AccountHeader.jsx | 12 ++++++--- src/js/components/agency/AgencyContent.jsx | 6 +++-- src/js/components/agency/AgencyPage.jsx | 17 +++++++----- .../components/agency/header/AgencyHeader.jsx | 27 ------------------- .../agencyLanding/AgencyLandingPage.jsx | 10 +++++-- .../header/AgencyLandingHeader.jsx | 20 -------------- src/js/components/award/AwardInfo.jsx | 4 ++- src/js/components/award/SummaryBar.jsx | 15 +++++++---- .../bulkDownload/BulkDownloadPage.jsx | 14 +++++----- .../explorer/ExplorerWrapperPage.jsx | 9 ++++--- src/js/components/keyword/KeywordPage.jsx | 26 +++++++++--------- src/js/components/search/SearchPage.jsx | 24 ++++++++--------- .../sharedComponents/sidebar/Sidebar.jsx | 7 ++--- .../stickyHeader/StickyHeader.jsx | 2 ++ 36 files changed, 115 insertions(+), 195 deletions(-) delete mode 100644 src/_scss/components/pageTitleBar/_title.scss delete mode 100644 src/_scss/components/pageTitleBar/pageTitleBar.scss delete mode 100644 src/_scss/pages/account/header/header.scss delete mode 100644 src/_scss/pages/agency/header/header.scss delete mode 100644 src/_scss/pages/agencyLanding/header/header.scss delete mode 100644 src/_scss/pages/award/summaryBar.scss delete mode 100644 src/_scss/pages/bulkDownload/header/header.scss delete mode 100644 src/js/components/about/AboutHeader.jsx delete mode 100644 src/js/components/agency/header/AgencyHeader.jsx delete mode 100644 src/js/components/agencyLanding/header/AgencyLandingHeader.jsx diff --git a/src/_scss/components/pageTitleBar/_title.scss b/src/_scss/components/pageTitleBar/_title.scss deleted file mode 100644 index 0d4199b81f..0000000000 --- a/src/_scss/components/pageTitleBar/_title.scss +++ /dev/null @@ -1,13 +0,0 @@ -.page-title { - font-size: rem(47); - line-height: rem(50); - font-weight: $font-light; - margin: 0 rem(20) 0 0; - padding: 0; - text-align: center; - - @include media($tablet-screen) { - text-align: left; - @include align-self(center); - } -} \ No newline at end of file diff --git a/src/_scss/components/pageTitleBar/pageTitleBar.scss b/src/_scss/components/pageTitleBar/pageTitleBar.scss deleted file mode 100644 index 570de3c60f..0000000000 --- a/src/_scss/components/pageTitleBar/pageTitleBar.scss +++ /dev/null @@ -1,24 +0,0 @@ -.page-title-bar { - background-color: $color-gray; - color: $color-white; - .page-title-bar-wrap { - max-width: 100%; - @include pad(2rem 0rem); - margin: 0 $outer-gutter/2; - @include media($tablet-screen) { - @include display(flex); - @include justify-content(space-between); - @include flex-direction(row); - @include align-items(center); - min-height: rem(102); - } - @include media($large-screen) { - margin: 0 $outer-gutter; - } - @include media($x-large-screen) { - max-width: $site-max-width; - margin: 0 auto; - } - @import "./_title"; - } -} \ No newline at end of file diff --git a/src/_scss/layouts/summary/summary.scss b/src/_scss/layouts/summary/summary.scss index 63f45d3063..3a80cc89d9 100644 --- a/src/_scss/layouts/summary/summary.scss +++ b/src/_scss/layouts/summary/summary.scss @@ -1,5 +1,6 @@ @import "../default/default"; -#main-content { + +.main-content { @include outer-container(100%); padding: 0; background-color: $color-white; diff --git a/src/_scss/pages/about/_sidebar.scss b/src/_scss/pages/about/_sidebar.scss index 01a8f5acf0..87315d6b80 100644 --- a/src/_scss/pages/about/_sidebar.scss +++ b/src/_scss/pages/about/_sidebar.scss @@ -8,7 +8,7 @@ .about-sidebar-content { &.float-sidebar { position: fixed; - top: rem(30); + top: rem(96); } background-color: $color-white; diff --git a/src/_scss/pages/about/aboutPage.scss b/src/_scss/pages/about/aboutPage.scss index 39fe1e6c04..907852df1a 100644 --- a/src/_scss/pages/about/aboutPage.scss +++ b/src/_scss/pages/about/aboutPage.scss @@ -1,11 +1,11 @@ .usa-da-about-page { @import "all"; @import "layouts/default/default"; + @import "layouts/default/stickyHeader/header"; + $color-gray-border: #D8D8D8; $color-blue-background: #F5FBFC; - @import "components/pageTitleBar/pageTitleBar"; - .main-content { @import "../../mixins/fullSectionWrap"; @include fullSectionWrap(($global-mrg * 2), ($global-mrg * 2)); diff --git a/src/_scss/pages/account/accountPage.scss b/src/_scss/pages/account/accountPage.scss index f165351562..2993f1350a 100644 --- a/src/_scss/pages/account/accountPage.scss +++ b/src/_scss/pages/account/accountPage.scss @@ -1,10 +1,11 @@ .usa-da-account-page { @import "all"; @import "layouts/search/search"; + @import "layouts/default/stickyHeader/header"; + @import "./header/options"; $color-gray-border: #D8D8D8; $color-blue-background: #F5FBFC; - @import "./header/header"; .main-content { @import "../../mixins/fullSectionWrap"; @include fullSectionWrap(($global-mrg * 4), ($global-mrg * 4)); diff --git a/src/_scss/pages/account/header/header.scss b/src/_scss/pages/account/header/header.scss deleted file mode 100644 index 67a01ca196..0000000000 --- a/src/_scss/pages/account/header/header.scss +++ /dev/null @@ -1,5 +0,0 @@ -@import "../../../components/pageTitleBar/pageTitleBar"; - -.page-title-bar { - @import "./_options"; -} \ No newline at end of file diff --git a/src/_scss/pages/agency/_sidebar.scss b/src/_scss/pages/agency/_sidebar.scss index fe9c8447fd..3d94839619 100644 --- a/src/_scss/pages/agency/_sidebar.scss +++ b/src/_scss/pages/agency/_sidebar.scss @@ -8,7 +8,7 @@ .agency-sidebar-content { &.float-sidebar { position: fixed; - top: rem(30); + top: rem(96); } background-color: $color-white; diff --git a/src/_scss/pages/agency/agencyPage.scss b/src/_scss/pages/agency/agencyPage.scss index 8fd5dab3f0..26426ac429 100644 --- a/src/_scss/pages/agency/agencyPage.scss +++ b/src/_scss/pages/agency/agencyPage.scss @@ -1,11 +1,11 @@ .usa-da-agency-page { @import "all"; @import "layouts/default/default"; + @import "layouts/default/stickyHeader/header"; + $color-gray-border: #D8D8D8; $color-blue-background: #F5FBFC; - @import "./header/header"; - .main-content { @import "../../mixins/fullSectionWrap"; @include fullSectionWrap(($global-mrg * 2), ($global-mrg * 2)); diff --git a/src/_scss/pages/agency/header/header.scss b/src/_scss/pages/agency/header/header.scss deleted file mode 100644 index c5c40e50a8..0000000000 --- a/src/_scss/pages/agency/header/header.scss +++ /dev/null @@ -1 +0,0 @@ -@import "components/pageTitleBar/pageTitleBar"; diff --git a/src/_scss/pages/agencyLanding/agencyLandingPage.scss b/src/_scss/pages/agencyLanding/agencyLandingPage.scss index 239f7f027e..d9d2c59d90 100644 --- a/src/_scss/pages/agencyLanding/agencyLandingPage.scss +++ b/src/_scss/pages/agencyLanding/agencyLandingPage.scss @@ -1,7 +1,7 @@ .usa-da-agency-landing { @import "all"; @import "layouts/landingPage/landingPage"; - @import "./header/header"; + @import "layouts/default/stickyHeader/header"; @import './agencyLandingSearch'; @import './table/resultsSection'; diff --git a/src/_scss/pages/agencyLanding/header/header.scss b/src/_scss/pages/agencyLanding/header/header.scss deleted file mode 100644 index f0d2edb6a2..0000000000 --- a/src/_scss/pages/agencyLanding/header/header.scss +++ /dev/null @@ -1 +0,0 @@ -@import "components/pageTitleBar/pageTitleBar"; \ No newline at end of file diff --git a/src/_scss/pages/award/awardPage.scss b/src/_scss/pages/award/awardPage.scss index 08be203034..5d46ba5e77 100644 --- a/src/_scss/pages/award/awardPage.scss +++ b/src/_scss/pages/award/awardPage.scss @@ -1,12 +1,12 @@ .usa-da-award-page { @import "all"; @import "layouts/summary/summary"; + @import "layouts/default/stickyHeader/header"; + @import "./summaryStatus"; $color-gray-border: #D8D8D8; $color-blue-background: #F5FBFC; - @import "./summaryBar"; - .main-content { @import "./infoBar"; @import "./awardContract"; diff --git a/src/_scss/pages/award/summaryBar.scss b/src/_scss/pages/award/summaryBar.scss deleted file mode 100644 index d955f3966c..0000000000 --- a/src/_scss/pages/award/summaryBar.scss +++ /dev/null @@ -1,5 +0,0 @@ -@import "../../components/pageTitleBar/pageTitleBar"; - -.page-title-bar { - @import './_summaryStatus'; -} \ No newline at end of file diff --git a/src/_scss/pages/bulkDownload/bulkDownloadPage.scss b/src/_scss/pages/bulkDownload/bulkDownloadPage.scss index e7a3b21718..6fbbfaa2dc 100644 --- a/src/_scss/pages/bulkDownload/bulkDownloadPage.scss +++ b/src/_scss/pages/bulkDownload/bulkDownloadPage.scss @@ -1,7 +1,7 @@ .usa-da-bulk-download-page { @import "all"; @import "layouts/search/search"; - @import "../../layouts/default/stickyHeader/header"; + @import "layouts/default/stickyHeader/header"; .bulk-download-content { @import "../../mixins/fullSectionWrap"; diff --git a/src/_scss/pages/bulkDownload/header/header.scss b/src/_scss/pages/bulkDownload/header/header.scss deleted file mode 100644 index c5c40e50a8..0000000000 --- a/src/_scss/pages/bulkDownload/header/header.scss +++ /dev/null @@ -1 +0,0 @@ -@import "components/pageTitleBar/pageTitleBar"; diff --git a/src/_scss/pages/explorer/explorerPage.scss b/src/_scss/pages/explorer/explorerPage.scss index d6d435a18c..81c944fe90 100644 --- a/src/_scss/pages/explorer/explorerPage.scss +++ b/src/_scss/pages/explorer/explorerPage.scss @@ -1,7 +1,7 @@ .usa-da-explorer-page { @import "all"; @import "layouts/default/default"; - @import "components/pageTitleBar/pageTitleBar"; + @import "layouts/default/stickyHeader/header"; .main-content { @import "./landing/explorerLanding"; diff --git a/src/_scss/pages/search/searchPage.scss b/src/_scss/pages/search/searchPage.scss index 81485f59e7..4bceca3f83 100644 --- a/src/_scss/pages/search/searchPage.scss +++ b/src/_scss/pages/search/searchPage.scss @@ -1,6 +1,7 @@ .usa-da-search-page { @import "all"; @import "layouts/tabbedSearch/tabbedSearch"; + @import "layouts/default/stickyHeader/header"; @import "./searchSidebar"; @import "./_pagePositioning"; diff --git a/src/js/components/about/About.jsx b/src/js/components/about/About.jsx index af0d3195db..97a3ed2d69 100644 --- a/src/js/components/about/About.jsx +++ b/src/js/components/about/About.jsx @@ -9,9 +9,9 @@ import { aboutPageMetaTags } from 'helpers/metaTagHelper'; import MetaTags from '../sharedComponents/metaTags/MetaTags'; import Header from '../sharedComponents/header/Header'; +import StickyHeader from '../sharedComponents/stickyHeader/StickyHeader'; import Footer from '../sharedComponents/Footer'; -import AboutHeader from './AboutHeader'; import AboutContent from './AboutContent'; require('pages/about/aboutPage.scss'); @@ -22,7 +22,13 @@ export default class About extends React.Component {
    - + +
    +

    + About +

    +
    +
    diff --git a/src/js/components/about/AboutContent.jsx b/src/js/components/about/AboutContent.jsx index 5f52085454..ae2a8f4f87 100644 --- a/src/js/components/about/AboutContent.jsx +++ b/src/js/components/about/AboutContent.jsx @@ -6,6 +6,7 @@ import React from 'react'; import { find, throttle } from 'lodash'; import { scrollToY } from 'helpers/scrollToHelper'; +import * as StickyHeader from 'components/sharedComponents/stickyHeader/StickyHeader'; import Sidebar from '../sharedComponents/sidebar/Sidebar'; @@ -122,7 +123,7 @@ export default class AboutContent extends React.Component { return; } - const sectionTop = sectionDom.offsetTop - 10; + const sectionTop = sectionDom.offsetTop - 10 - StickyHeader.stickyHeaderHeight; scrollToY(sectionTop, 700); }); } @@ -214,7 +215,8 @@ export default class AboutContent extends React.Component { active={this.state.activeSection} pageName="about" sections={aboutSections} - jumpToSection={this.jumpToSection} /> + jumpToSection={this.jumpToSection} + stickyHeaderHeight={StickyHeader.stickyHeaderHeight} />
    diff --git a/src/js/components/about/AboutHeader.jsx b/src/js/components/about/AboutHeader.jsx deleted file mode 100644 index 9ebd33bd9f..0000000000 --- a/src/js/components/about/AboutHeader.jsx +++ /dev/null @@ -1,20 +0,0 @@ -/** - * AboutHeader.jsx - * Created by Mike Bray 11/20/2017 - */ - -import React from 'react'; - -export default class AboutHeader extends React.Component { - render() { - return ( -
    -
    -

    - About -

    -
    -
    - ); - } -} diff --git a/src/js/components/account/Account.jsx b/src/js/components/account/Account.jsx index ee7ce3fa9c..5da63156cb 100644 --- a/src/js/components/account/Account.jsx +++ b/src/js/components/account/Account.jsx @@ -31,9 +31,7 @@ export default class Account extends React.Component {
    - -
    diff --git a/src/js/components/account/AccountHeader.jsx b/src/js/components/account/AccountHeader.jsx index 298b227bbb..53228b2651 100644 --- a/src/js/components/account/AccountHeader.jsx +++ b/src/js/components/account/AccountHeader.jsx @@ -5,6 +5,8 @@ import React from 'react'; import PropTypes from 'prop-types'; + +import StickyHeader from 'components/sharedComponents/stickyHeader/StickyHeader'; import InfoSnippet from '../award/InfoSnippet'; const propTypes = { @@ -17,11 +19,13 @@ export default class AccountHeader extends React.Component { `${this.props.account.agency_identifier}-${this.props.account.main_account_code}`; return ( -
    -
    -

    + +
    +

    Federal Account Profile

    +
    +
    -

    + ); } } diff --git a/src/js/components/agency/AgencyContent.jsx b/src/js/components/agency/AgencyContent.jsx index 733a1be0a2..15a940df4e 100644 --- a/src/js/components/agency/AgencyContent.jsx +++ b/src/js/components/agency/AgencyContent.jsx @@ -9,6 +9,7 @@ import { find, throttle } from 'lodash'; import { scrollToY } from 'helpers/scrollToHelper'; import moment from 'moment'; import { convertQuarterToDate } from 'helpers/fiscalYearHelper'; +import * as StickyHeader from 'components/sharedComponents/stickyHeader/StickyHeader'; import GlossaryButtonWrapperContainer from 'containers/glossary/GlossaryButtonWrapperContainer'; @@ -130,7 +131,7 @@ export default class AgencyContent extends React.Component { return; } - const sectionTop = sectionDom.offsetTop - 10; + const sectionTop = sectionDom.offsetTop - 10 - StickyHeader.stickyHeaderHeight; scrollToY(sectionTop, 700); }); } @@ -231,7 +232,8 @@ export default class AgencyContent extends React.Component { active={this.state.activeSection} pageName="agency" sections={agencySections} - jumpToSection={this.jumpToSection} /> + jumpToSection={this.jumpToSection} + stickyHeaderHeight={StickyHeader.stickyHeaderHeight} />
    diff --git a/src/js/components/agency/AgencyPage.jsx b/src/js/components/agency/AgencyPage.jsx index 359aacd4a3..6ffe677f22 100644 --- a/src/js/components/agency/AgencyPage.jsx +++ b/src/js/components/agency/AgencyPage.jsx @@ -8,11 +8,10 @@ import PropTypes from 'prop-types'; import { agencyPageMetaTags } from 'helpers/metaTagHelper'; -import MetaTags from '../sharedComponents/metaTags/MetaTags'; -import Header from '../sharedComponents/header/Header'; -import Footer from '../sharedComponents/Footer'; - -import AgencyHeader from './header/AgencyHeader'; +import MetaTags from 'components/sharedComponents/metaTags/MetaTags'; +import Header from 'components/sharedComponents/header/Header'; +import StickyHeader from 'components/sharedComponents/stickyHeader/StickyHeader'; +import Footer from 'components/sharedComponents/Footer'; import AgencyLoading from './AgencyLoading'; import AgencyError from './AgencyError'; @@ -41,7 +40,13 @@ export default class AgencyPage extends React.Component {
    - + +
    +

    + Agency Profile +

    +
    +
    diff --git a/src/js/components/agency/header/AgencyHeader.jsx b/src/js/components/agency/header/AgencyHeader.jsx deleted file mode 100644 index dd4a25e84b..0000000000 --- a/src/js/components/agency/header/AgencyHeader.jsx +++ /dev/null @@ -1,27 +0,0 @@ -/** - * AgencyHeader.jsx - * Created by Kevin Li 6/8/17 - */ - -import React from 'react'; -import PropTypes from 'prop-types'; - -const propTypes = { - account: PropTypes.object -}; - -export default class AgencyHeader extends React.Component { - render() { - return ( -
    -
    -

    - Agency Profile -

    -
    -
    - ); - } -} - -AgencyHeader.propTypes = propTypes; diff --git a/src/js/components/agencyLanding/AgencyLandingPage.jsx b/src/js/components/agencyLanding/AgencyLandingPage.jsx index 651d9c60d8..16c50df62c 100644 --- a/src/js/components/agencyLanding/AgencyLandingPage.jsx +++ b/src/js/components/agencyLanding/AgencyLandingPage.jsx @@ -9,9 +9,9 @@ import { agencyLandingPageMetaTags } from 'helpers/metaTagHelper'; import MetaTags from 'components/sharedComponents/metaTags/MetaTags'; import Header from 'components/sharedComponents/header/Header'; +import StickyHeader from 'components/sharedComponents/stickyHeader/StickyHeader'; import Footer from 'components/sharedComponents/Footer'; import AgencyLandingContainer from 'containers/agencyLanding/AgencyLandingContainer'; -import AgencyLandingHeader from './header/AgencyLandingHeader'; require('pages/agencyLanding/agencyLandingPage.scss'); @@ -21,7 +21,13 @@ export default class AgencyLandingPage extends React.Component {
    - + +
    +

    + Agency Profile +

    +
    +
    diff --git a/src/js/components/agencyLanding/header/AgencyLandingHeader.jsx b/src/js/components/agencyLanding/header/AgencyLandingHeader.jsx deleted file mode 100644 index 52a9cf6f38..0000000000 --- a/src/js/components/agencyLanding/header/AgencyLandingHeader.jsx +++ /dev/null @@ -1,20 +0,0 @@ -/** - * AgencyLandingHeader.jsx - * Created by Lizzie Salita 7/7/17 - */ - -import React from 'react'; - -export default class AgencyLandingHeader extends React.Component { - render() { - return ( -
    -
    -

    - Agency Profiles -

    -
    -
    - ); - } -} diff --git a/src/js/components/award/AwardInfo.jsx b/src/js/components/award/AwardInfo.jsx index 8691c99956..a42b6f9aea 100644 --- a/src/js/components/award/AwardInfo.jsx +++ b/src/js/components/award/AwardInfo.jsx @@ -73,7 +73,9 @@ export default class AwardInfo extends React.Component { -
    +
    diff --git a/src/js/components/award/SummaryBar.jsx b/src/js/components/award/SummaryBar.jsx index 062a8bbabc..c0cb1eb777 100644 --- a/src/js/components/award/SummaryBar.jsx +++ b/src/js/components/award/SummaryBar.jsx @@ -7,6 +7,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import moment from 'moment'; import { startCase, toLower, includes } from 'lodash'; + +import StickyHeader from 'components/sharedComponents/stickyHeader/StickyHeader'; import * as SummaryPageHelper from 'helpers/summaryPageHelper'; import { awardTypeGroups } from 'dataMapping/search/awardType'; @@ -78,12 +80,15 @@ export default class SummaryBar extends React.Component { label="Parent Award ID" value={this.state.parent} />); } + return ( -
    -
    -

    - {this.state.type} Profile + +
    +

    + {this.state.type} Summary

    +
    +
    -

    + ); } } diff --git a/src/js/components/bulkDownload/BulkDownloadPage.jsx b/src/js/components/bulkDownload/BulkDownloadPage.jsx index 719ca8d7bb..44f4bfe956 100644 --- a/src/js/components/bulkDownload/BulkDownloadPage.jsx +++ b/src/js/components/bulkDownload/BulkDownloadPage.jsx @@ -86,15 +86,15 @@ export default class BulkDownloadPage extends React.Component {
    + +
    +

    + Download Center +

    +
    +
    - -
    -

    - Download Center -

    -
    -
    (
    -
    -
    -

    + +
    +

    Spending Explorer

    -

    +
    diff --git a/src/js/components/keyword/KeywordPage.jsx b/src/js/components/keyword/KeywordPage.jsx index c1554ccf30..94051fa6d5 100644 --- a/src/js/components/keyword/KeywordPage.jsx +++ b/src/js/components/keyword/KeywordPage.jsx @@ -154,20 +154,20 @@ export default class KeywordPage extends React.Component { className="usa-da-keyword-page">
    -
    - -
    -
    -

    Keyword Search

    -
    - {searchSummary} -
    - -
    + +
    +
    +

    Keyword Search

    - + {searchSummary} +
    + +
    +
    +
    +
    + +
    +

    + Advanced Search +

    +
    +
    + +
    +
    - -
    -

    - Advanced Search -

    -
    -
    - -
    -
    { fullSidebar } diff --git a/src/js/components/sharedComponents/sidebar/Sidebar.jsx b/src/js/components/sharedComponents/sidebar/Sidebar.jsx index 7dae90cb91..4b7c563285 100644 --- a/src/js/components/sharedComponents/sidebar/Sidebar.jsx +++ b/src/js/components/sharedComponents/sidebar/Sidebar.jsx @@ -13,7 +13,8 @@ const propTypes = { active: PropTypes.string, pageName: PropTypes.string, sections: PropTypes.array, - jumpToSection: PropTypes.func + jumpToSection: PropTypes.func, + stickyHeaderHeight: PropTypes.number }; export default class Sidebar extends React.Component { @@ -52,7 +53,8 @@ export default class Sidebar extends React.Component { const width = targetElement.offsetWidth; // also measure the Y position at which to float the sidebar - const floatPoint = targetElement.offsetTop - 30; + // Subtract the height of the absolutely-positioned sticky header + const floatPoint = targetElement.offsetTop - 30 - this.props.stickyHeaderHeight; this.setState({ floatPoint, @@ -73,7 +75,6 @@ export default class Sidebar extends React.Component { return; } - let shouldFloat = false; const yPos = window.scrollY || window.pageYOffset; if (yPos > this.state.floatPoint) { diff --git a/src/js/components/sharedComponents/stickyHeader/StickyHeader.jsx b/src/js/components/sharedComponents/stickyHeader/StickyHeader.jsx index 63406c8d49..885d34ab45 100644 --- a/src/js/components/sharedComponents/stickyHeader/StickyHeader.jsx +++ b/src/js/components/sharedComponents/stickyHeader/StickyHeader.jsx @@ -12,6 +12,8 @@ const propTypes = { children: PropTypes.node }; +export const stickyHeaderHeight = 66; + export default class StickyHeader extends React.Component { constructor(props) { super(props); From b247b2f90be3303a71b61f814bdf3af2caba6b1a Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Wed, 7 Feb 2018 13:13:30 -0500 Subject: [PATCH 49/89] Added Account Landing Container tests --- .../AccountLandingContainer-test.jsx | 117 ++++++++++++++++++ .../accountLanding/accountLandingHelper.js | 14 +++ .../accountLanding/mockFederalAccounts.js | 74 +++++++++++ 3 files changed, 205 insertions(+) create mode 100644 tests/containers/accountLanding/AccountLandingContainer-test.jsx create mode 100644 tests/containers/accountLanding/accountLandingHelper.js create mode 100644 tests/containers/accountLanding/mockFederalAccounts.js diff --git a/tests/containers/accountLanding/AccountLandingContainer-test.jsx b/tests/containers/accountLanding/AccountLandingContainer-test.jsx new file mode 100644 index 0000000000..d4f6280d9b --- /dev/null +++ b/tests/containers/accountLanding/AccountLandingContainer-test.jsx @@ -0,0 +1,117 @@ +/** + * AccountLandingContainer-test.jsx + * Created by Lizzie Salita 2/7/18 + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import * as FiscalYearHelper from 'helpers/fiscalYearHelper'; +import AccountLandingContainer from 'containers/accountLanding/AccountLandingContainer'; + +import { mockData, mockParsed } from './mockFederalAccounts'; + +jest.mock('helpers/accountLandingHelper', () => require('./accountLandingHelper')); + +// mock the child component by replacing it with a function that returns a null element +jest.mock('components/accountLanding/AccountLandingContent', () => + jest.fn(() => null)); + +describe('AccountLandingContainer', () => { + it('should make an API request on mount', async () => { + // mount the container + const container = mount(); + const parseAccounts = jest.fn(); + container.instance().parseAccounts = parseAccounts; + + await container.instance().accountsRequest.promise; + + expect(parseAccounts).toHaveBeenCalledTimes(1); + }); + + describe('showColumns', () => { + it('should build the table', async () => { + // mount the container + const container = mount(); + + await container.instance().accountsRequest.promise; + + // validate the state contains the correctly parsed values + const fy = FiscalYearHelper.defaultFiscalYear(); + const expectedState = [ + { + columnName: "account_number", + defaultDirection: "desc", + displayName: "Account Number" + }, + { + columnName: "account_name", + defaultDirection: "asc", + displayName: "Account Name" + }, + { + columnName: "managing_agency", + defaultDirection: "asc", + displayName: "Managing Agency" + }, + { + columnName: "budgetary_resources", + defaultDirection: "desc", + displayName: `${fy} Budgetary Resources` + } + ]; + + expect(container.state().columns).toEqual(expectedState); + }); + }); + + describe('parseAccounts', () => { + it('should parse the API response and update the container state', () => { + const container = mount(); + + container.instance().parseAccounts(mockData.data); + expect(container.state().results).toEqual(mockParsed); + }); + }); + + describe('updateSort', () => { + it('should update the state and make an API request', async () => { + // mount the container + const container = mount(); + const parseAccounts = jest.fn(); + container.instance().parseAccounts = parseAccounts; + + await container.instance().accountsRequest.promise; + expect(parseAccounts).toHaveBeenCalledTimes(1); + + // change the sort order + container.instance().updateSort('managing_agency', 'asc'); + + await container.instance().accountsRequest.promise; + expect(container.state().order).toEqual({ + field: 'managing_agency', + direction: 'asc' + }); + expect(parseAccounts).toHaveBeenCalledTimes(2); + }) + }); + + describe('onChangePage', () => { + it('should update the state and make an API request', async () => { + // mount the container + const container = mount(); + const parseAccounts = jest.fn(); + container.instance().parseAccounts = parseAccounts; + + await container.instance().accountsRequest.promise; + expect(parseAccounts).toHaveBeenCalledTimes(1); + + // change the page number + container.instance().onChangePage(2); + + await container.instance().accountsRequest.promise; + expect(container.state().pageNumber).toEqual(2); + expect(parseAccounts).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/tests/containers/accountLanding/accountLandingHelper.js b/tests/containers/accountLanding/accountLandingHelper.js new file mode 100644 index 0000000000..d72218c517 --- /dev/null +++ b/tests/containers/accountLanding/accountLandingHelper.js @@ -0,0 +1,14 @@ +import { mockData } from './mockFederalAccounts'; + +export const fetchAllAccounts = () => ( + { + promise: new Promise((resolve) => { + process.nextTick(() => { + resolve({ + data: mockData + }); + }); + }), + cancel: jest.fn() + } +); \ No newline at end of file diff --git a/tests/containers/accountLanding/mockFederalAccounts.js b/tests/containers/accountLanding/mockFederalAccounts.js new file mode 100644 index 0000000000..593f41f31a --- /dev/null +++ b/tests/containers/accountLanding/mockFederalAccounts.js @@ -0,0 +1,74 @@ +export const mockData = { + data: { + page: 1, + limit: 2, + count: 3, + fy: '1987', + results: [ + { + account_id: 1, + account_number: '123-4567', + account_name: 'Mock Account', + managing_agency: 'Mock Agency', + managing_agency_acronym: 'XYZ', + budgetary_resources: 5000000 + }, + { + account_id: 2, + account_number: '098-7654', + account_name: 'Mock Account 2', + managing_agency: 'Mock Agency 2', + managing_agency_acronym: 'ABC', + budgetary_resources: 6500000 + }, + { + account_id: 3, + account_number: '234-5678', + account_name: 'Test Account', + managing_agency: 'Mock Agency 3', + managing_agency_acronym: 'DEF', + budgetary_resources: 4500000 + } + ] + } +}; + +export const mockParsed = [ + { + "account_id": 1, + "account_name": "Mock Account", + "account_number": "123-4567", + "budgetary_resources": 5000000, + "display": { + "account_name": "Mock Account", + "account_number": "123-4567", + "budgetary_resources": "$5,000,000", + "managing_agency": "Mock Agency (XYZ)" + }, + "managing_agency": "Mock Agency (XYZ)" + }, { + "account_id": 2, + "account_name": "Mock Account 2", + "account_number": "098-7654", + "budgetary_resources": 6500000, + "display": { + "account_name": "Mock Account 2", + "account_number": "098-7654", + "budgetary_resources": "$6,500,000", + "managing_agency": "Mock Agency 2 (ABC)" + }, + "managing_agency": "Mock Agency 2 (ABC)" + }, { + "account_id": 3, + "account_name": "Test Account", + "account_number": "234-5678", + "budgetary_resources": 4500000, + "display": { + "account_name": "Test Account", + "account_number": "234-5678", + "budgetary_resources": "$4,500,000", + "managing_agency": "Mock Agency 3 (DEF)" + }, + "managing_agency": "Mock Agency 3 (DEF)" + } +]; \ No newline at end of file From 388aa5247000e367a5559ea6dd28602ca51b9b10 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Wed, 7 Feb 2018 13:37:27 -0500 Subject: [PATCH 50/89] Adding footer events --- src/js/components/sharedComponents/Footer.jsx | 27 ++++++++++++++++--- .../sharedComponents/FooterExternalLink.jsx | 12 ++++++++- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/js/components/sharedComponents/Footer.jsx b/src/js/components/sharedComponents/Footer.jsx index 3141c0b8fe..3714a0874a 100644 --- a/src/js/components/sharedComponents/Footer.jsx +++ b/src/js/components/sharedComponents/Footer.jsx @@ -5,6 +5,7 @@ import PropTypes from 'prop-types'; import React from 'react'; +import Analytics from 'helpers/analytics/Analytics'; import GlossaryButtonWrapperContainer from 'containers/glossary/GlossaryButtonWrapperContainer'; import DownloadBottomBarContainer from 'containers/search/modals/fullDownload/DownloadBottomBarContainer'; @@ -17,6 +18,13 @@ const propTypes = { filters: PropTypes.object }; +const clickedFooterLink = (route) => { + Analytics.event({ + category: 'Footer - Link', + action: route + }); +}; + export default class Footer extends React.Component { render() { const year = new Date().getFullYear(); @@ -32,7 +40,11 @@ export default class Footer extends React.Component { aria-label="Footer">
    @@ -43,7 +55,9 @@ export default class Footer extends React.Component {
    • - + About
    • @@ -65,7 +79,12 @@ export default class Footer extends React.Component { title="Community" />
    • - + Contact Us
    • @@ -113,7 +132,7 @@ export default class Footer extends React.Component { © {year} USAspending.gov
    - NOTE: You must click here for very important D&B information. + NOTE: You must click here for very important D&B information.
    diff --git a/src/js/components/sharedComponents/FooterExternalLink.jsx b/src/js/components/sharedComponents/FooterExternalLink.jsx index 61c0452071..ceeef0fa3e 100644 --- a/src/js/components/sharedComponents/FooterExternalLink.jsx +++ b/src/js/components/sharedComponents/FooterExternalLink.jsx @@ -6,18 +6,28 @@ import React from 'react'; import PropTypes from 'prop-types'; +import Analytics from 'helpers/analytics/Analytics'; + const propTypes = { link: PropTypes.string, title: PropTypes.string }; +const clickedFooterLink = (route) => { + Analytics.event({ + category: 'Footer - Link', + action: route + }); +}; + const FooterExternalLink = (props) => ( + aria-label={props.title} + onClick={clickedFooterLink.bind(null, props.link)}> {props.title} ); From 883a5a021fb045a92b52abad32277e1b3ac050be Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Wed, 7 Feb 2018 13:40:37 -0500 Subject: [PATCH 51/89] Restore D&B info page --- src/js/components/about/DBInfo.jsx | 52 +++++++++++++++++++++++ src/js/containers/router/RouterRoutes.jsx | 9 ++++ 2 files changed, 61 insertions(+) create mode 100644 src/js/components/about/DBInfo.jsx diff --git a/src/js/components/about/DBInfo.jsx b/src/js/components/about/DBInfo.jsx new file mode 100644 index 0000000000..2fd66c7256 --- /dev/null +++ b/src/js/components/about/DBInfo.jsx @@ -0,0 +1,52 @@ +/** + * DBInfo.jsx + * Created by Destin Frasier 04/20/2017 + **/ + +import React from 'react'; + +import * as MetaTagHelper from 'helpers/metaTagHelper'; + +import MetaTags from '../sharedComponents/metaTags/MetaTags'; +import Header from '../sharedComponents/header/Header'; +import Footer from '../sharedComponents/Footer'; + +require('pages/dbInfo/dbInfoPage.scss'); + +export default class DBInfo extends React.Component { + render() { + return ( +
    + +
    +
    +
    +

    Limitation on Permissible Use of Dun & Bradstreet, Inc. Data

    +

    This website contains data supplied by third party information suppliers, one of which is D&B. For the purposes of the + following limitation on permissible use of D&B data, which includes each entity's DUNS Number and its associated business + information, "D&B Open Data" is defined as the following data elements: Business Name, Street Address, City Name, State/Province Name, + Country Name, County Code, State/Province Code, State/Province Abbreviation, and ZIP/Postal Code. +

    +

    D&B hereby grants you, the user, a license for a limited, non-exclusive use of D&B data within the limitations set forth herein. + By using this website you agree that you shall not use D&B Open Data without giving written attribution to the source of such data + (i.e., D&B) and shall not access, use or disseminate D&B Open Data in bulk, (i.e., in amounts sufficient for use as an original source + or as a substitute for the product and/or service being licensed hereunder). +

    +

    Except for data elements identified above as D&B Open Data, under no circumstances are you authorized to use any other D&B data for + commercial, resale or marketing purposes (e.g., identifying, quantifying, segmenting and/or analyzing customers and prospective customers). + Systematic access (electronic harvesting) or extraction of content from the website, including the use of "bots" or "spiders", is prohibited. + Federal government entities are authorized to use the D&B data for purposes of acquisition as defined in FAR 2.101 and for the purpose of managing + Federal awards, including sub-awardees, or reporting Federal award information. +

    +

    The Federal Government and its agencies assume no liability for the use of the D&B data once it is downloaded or accessed. The D&B data is + provided "as is" without warranty of any kind. The D&B data is the intellectual property of D&B. In no event will D&B or any third party information + supplier be liable in any way with regard to the use of the D&B data. For more information about the scope of permissible use of D&B data licensed + hereunder, please contact D&B at datause_govt@dnb.com +

    +
    +
    +
    +
    + ); + } +} diff --git a/src/js/containers/router/RouterRoutes.jsx b/src/js/containers/router/RouterRoutes.jsx index 8a2f981a0b..9b9a1404c4 100644 --- a/src/js/containers/router/RouterRoutes.jsx +++ b/src/js/containers/router/RouterRoutes.jsx @@ -92,6 +92,15 @@ const routes = { }); } }, + { + path: '/db_info', + parent: '/db_info', + component: (cb) => { + require.ensure([], (require) => { + cb(require('components/about/DBInfo').default); + }); + } + }, { path: '/style', parent: '/style', From f5619256159dc262951193c7bcacc4d8b43da2bf Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Wed, 7 Feb 2018 13:51:28 -0500 Subject: [PATCH 52/89] API integration --- .../AccountLandingContainer.jsx | 3 +- src/js/helpers/accountLandingHelper.js | 132 ++---------------- 2 files changed, 16 insertions(+), 119 deletions(-) diff --git a/src/js/containers/accountLanding/AccountLandingContainer.jsx b/src/js/containers/accountLanding/AccountLandingContainer.jsx index a06d2863ab..7e9808a6e6 100644 --- a/src/js/containers/accountLanding/AccountLandingContainer.jsx +++ b/src/js/containers/accountLanding/AccountLandingContainer.jsx @@ -30,8 +30,7 @@ export default class AccountLandingContainer extends React.Component { searchString: '', results: [], totalItems: 0, - // TODO - Lizzie: update to 50 - pageSize: 10 + pageSize: 50 }; this.accountsRequest = null; diff --git a/src/js/helpers/accountLandingHelper.js b/src/js/helpers/accountLandingHelper.js index 7a5d3ff327..de163aff3b 100644 --- a/src/js/helpers/accountLandingHelper.js +++ b/src/js/helpers/accountLandingHelper.js @@ -3,124 +3,22 @@ * Created by Lizzie Salita 8/4/17 **/ -// import Axios, { CancelToken } from 'axios'; +import Axios, { CancelToken } from 'axios'; +import kGlobalConstants from 'GlobalConstants'; -// import kGlobalConstants from 'GlobalConstants'; - -// TODO - Lizzie: update when API is ready -// export const fetchAllAccounts = (params) => { -// const source = CancelToken.source(); -// return { -// promise: Axios.request({ -// url: 'v2/references/federal_account/', -// baseURL: kGlobalConstants.API, -// method: 'post', -// params, -// cancelToken: source.token -// }), -// cancel() { -// source.cancel(); -// } -// }; -// }; - -const mockResponse = { - page: 1, - limit: 10, - count: 20, - results: [ - { - account_id: 1, - account_number: '123-4567', - account_name: 'Mock Account', - managing_agency: 'Mock Agency', - managing_agency_acronym: 'XYZ', - budgetary_resources: 5000000 - }, - { - account_id: 2, - account_number: '098-7654', - account_name: 'Mock Account 2', - managing_agency: 'Mock Agency 2', - managing_agency_acronym: 'ABC', - budgetary_resources: 6500000 - }, - { - account_id: 3, - account_number: '234-5678', - account_name: 'Test Account', - managing_agency: 'Mock Agency 3', - managing_agency_acronym: 'DEF', - budgetary_resources: 4500000 - }, - { - account_id: 4, - account_number: '123-4567', - account_name: 'Mock Account 4', - managing_agency: 'Mock Agency 4', - managing_agency_acronym: 'XYZ', - budgetary_resources: 5500000 - }, - { - account_id: 5, - account_number: '098-7654', - account_name: 'Mock Account 5', - managing_agency: 'Mock Agency 5', - managing_agency_acronym: 'ABC', - budgetary_resources: 6000000 - }, - { - account_id: 6, - account_number: '234-5678', - account_name: 'Test Account 2', - managing_agency: 'Mock Agency 6', - managing_agency_acronym: 'DEF', - budgetary_resources: 4000000 - }, - { - account_id: 7, - account_number: '123-4567', - account_name: 'Mock Account 7', - managing_agency: 'Mock Agency 7', - managing_agency_acronym: 'XYZ', - budgetary_resources: 5000000 - }, - { - account_id: 8, - account_number: '098-7654', - account_name: 'Mock Account 8', - managing_agency: 'Mock Agency 8', - managing_agency_acronym: 'ABC', - budgetary_resources: 6500000 - }, - { - account_id: 9, - account_number: '234-5678', - account_name: 'Test Account 3', - managing_agency: 'Mock Agency 9', - managing_agency_acronym: 'DEF', - budgetary_resources: 4500000 - }, - { - account_id: 10, - account_number: '123-4567', - account_name: 'Mock Account 10', - managing_agency: 'Mock Agency 6', - managing_agency_acronym: 'XYZ', - budgetary_resources: 5500000 +export const fetchAllAccounts = (data) => { + const source = CancelToken.source(); + return { + promise: Axios.request({ + url: 'v2/references/federal_account/', + baseURL: kGlobalConstants.API, + method: 'post', + data, + cancelToken: source.token + }), + cancel() { + source.cancel(); } - ] + }; }; -export const fetchAllAccounts = () => ( - { - promise: new Promise((resolve) => { - setTimeout(() => { - resolve({ - data: mockResponse - }); - }, 1000); - }) - } -); - From 21aeace21053fdb5ee7d457c8733ac2284f2cfef Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Wed, 7 Feb 2018 13:58:19 -0500 Subject: [PATCH 53/89] Fixed header styling file structure --- .../accountLanding/accountLandingPage.scss | 18 +++++++++--------- .../pages/accountLanding/header/header.scss | 1 - 2 files changed, 9 insertions(+), 10 deletions(-) delete mode 100644 src/_scss/pages/accountLanding/header/header.scss diff --git a/src/_scss/pages/accountLanding/accountLandingPage.scss b/src/_scss/pages/accountLanding/accountLandingPage.scss index 4c35980cc0..6ef3122118 100644 --- a/src/_scss/pages/accountLanding/accountLandingPage.scss +++ b/src/_scss/pages/accountLanding/accountLandingPage.scss @@ -1,12 +1,12 @@ .usa-da-account-landing { - @import "all"; - @import "layouts/landingPage/landingPage"; - @import "components/pagination"; - @import "./header/header"; - @import './searchSection'; - @import './table/resultsSection'; + @import "all"; + @import "layouts/landingPage/landingPage"; + @import "components/pagination"; + @import "layouts/default/stickyHeader/header"; + @import './searchSection'; + @import './table/resultsSection'; - .search-section { - display: none; - } + .search-section { + display: none; + } } \ No newline at end of file diff --git a/src/_scss/pages/accountLanding/header/header.scss b/src/_scss/pages/accountLanding/header/header.scss deleted file mode 100644 index 06763c1ffb..0000000000 --- a/src/_scss/pages/accountLanding/header/header.scss +++ /dev/null @@ -1 +0,0 @@ -@import "layouts/default/stickyHeader/header"; \ No newline at end of file From faa86750158072f77de0236fec9dc3d36ebe2ab4 Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Wed, 7 Feb 2018 14:03:33 -0500 Subject: [PATCH 54/89] Fix variable name --- src/js/dataMapping/accountLanding/accountsTableFields.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/dataMapping/accountLanding/accountsTableFields.js b/src/js/dataMapping/accountLanding/accountsTableFields.js index c92ce989d9..870f4f4688 100644 --- a/src/js/dataMapping/accountLanding/accountsTableFields.js +++ b/src/js/dataMapping/accountLanding/accountsTableFields.js @@ -1,4 +1,4 @@ -const agenciesTableFields = { +const accountsTableFields = { defaultSortDirection: { account_number: 'desc', account_name: 'asc', @@ -17,4 +17,4 @@ const agenciesTableFields = { budgetary_resources: 'Budgetary Resources' }; -export default agenciesTableFields; +export default accountsTableFields; From c68be83e5d4951951b906eba65d69ab2c2c68b92 Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Wed, 7 Feb 2018 14:16:52 -0500 Subject: [PATCH 55/89] Enabled the navigation link --- src/js/dataMapping/navigation/menuOptions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/dataMapping/navigation/menuOptions.js b/src/js/dataMapping/navigation/menuOptions.js index e40e3924f4..7aa77e5921 100644 --- a/src/js/dataMapping/navigation/menuOptions.js +++ b/src/js/dataMapping/navigation/menuOptions.js @@ -27,7 +27,7 @@ export const profileOptions = [ { label: 'Federal Accounts', url: '#/federal_account', - enabled: false + enabled: true }, { label: 'Recipients', From 92d6d0380cc275c94abb49b0afef961c9763a99f Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Wed, 7 Feb 2018 15:22:20 -0500 Subject: [PATCH 56/89] Spending explorer and keyword download analytics --- .../explorer/detail/header/DetailHeader.jsx | 20 ++++++++++++++-- .../visualization/ExplorerVisualization.jsx | 12 ++++++++++ src/js/components/keyword/KeywordPage.jsx | 6 +++++ .../detail/DetailContentContainer.jsx | 23 +++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/js/components/explorer/detail/header/DetailHeader.jsx b/src/js/components/explorer/detail/header/DetailHeader.jsx index 068c888df4..48d2ecad45 100644 --- a/src/js/components/explorer/detail/header/DetailHeader.jsx +++ b/src/js/components/explorer/detail/header/DetailHeader.jsx @@ -7,6 +7,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import moment from 'moment'; +import Analytics from 'helpers/analytics/Analytics'; import { sidebarTypes } from 'dataMapping/explorer/sidebarStrings'; import { formatTreemapValues } from 'helpers/moneyFormatter'; @@ -22,6 +23,13 @@ const propTypes = { parent: PropTypes.string }; +const exitExplorer = (target) => { + Analytics.event({ + category: 'Spending Explorer - Exit', + action: target + }); +}; + const dataType = (type, parent) => { if (!type) { return null; @@ -54,14 +62,22 @@ const heading = (type, title, id) => { if (type === 'Federal Account') { return (

    - {title} + + {title} +

    ); } else if (type === 'Agency') { return (

    - {title} + + {title} +

    ); } diff --git a/src/js/components/explorer/detail/visualization/ExplorerVisualization.jsx b/src/js/components/explorer/detail/visualization/ExplorerVisualization.jsx index 3747ca8f2f..75a0cfbc47 100644 --- a/src/js/components/explorer/detail/visualization/ExplorerVisualization.jsx +++ b/src/js/components/explorer/detail/visualization/ExplorerVisualization.jsx @@ -6,6 +6,8 @@ import React from 'react'; import PropTypes from 'prop-types'; +import Analytics from 'helpers/analytics/Analytics'; + import ExplorerTableContainer from 'containers/explorer/detail/table/ExplorerTableContainer'; import BreakdownDropdown from './toolbar/BreakdownDropdown'; import ExplorerTreemap from './treemap/ExplorerTreemap'; @@ -40,6 +42,11 @@ export default class ExplorerVisualization extends React.Component { componentDidMount() { this.measureWidth(); window.addEventListener('resize', this.measureWidth); + + Analytics.event({ + category: 'Spending Explorer - Visualization Type', + action: this.state.viewType + }); } componentWillUnmount() { @@ -58,6 +65,11 @@ export default class ExplorerVisualization extends React.Component { this.setState({ viewType }); + + Analytics.event({ + category: 'Spending Explorer - Visualization Type', + action: viewType + }); } render() { diff --git a/src/js/components/keyword/KeywordPage.jsx b/src/js/components/keyword/KeywordPage.jsx index 519e136ac2..b6dc4fcf2e 100644 --- a/src/js/components/keyword/KeywordPage.jsx +++ b/src/js/components/keyword/KeywordPage.jsx @@ -6,6 +6,8 @@ import React from 'react'; import PropTypes from 'prop-types'; +import Analytics from 'helpers/analytics/Analytics'; + import * as MetaTagHelper from 'helpers/metaTagHelper'; import * as MoneyFormatter from 'helpers/moneyFormatter'; import { InfoCircle } from 'components/sharedComponents/icons/Icons'; @@ -73,6 +75,10 @@ export default class KeywordPage extends React.Component { clickedDownload() { this.props.startDownload(); this.showModal(); + Analytics.event({ + category: 'Keyword Search - Download', + action: this.props.keyword + }); } showTooltip() { diff --git a/src/js/containers/explorer/detail/DetailContentContainer.jsx b/src/js/containers/explorer/detail/DetailContentContainer.jsx index 988a40ff15..9b92ae76d1 100644 --- a/src/js/containers/explorer/detail/DetailContentContainer.jsx +++ b/src/js/containers/explorer/detail/DetailContentContainer.jsx @@ -10,6 +10,7 @@ import { connect } from 'react-redux'; import { isCancel } from 'axios'; import { List } from 'immutable'; +import Analytics from 'helpers/analytics/Analytics'; import Router from 'containers/router/Router'; import { dropdownScopes } from 'dataMapping/explorer/dropdownScopes'; @@ -85,6 +86,12 @@ export class DetailContentContainer extends React.Component { }, () => { this.loadData(request, true); }); + + // log the analytics event for a Spending Explorer starting point + Analytics.event({ + category: 'Spending Explorer - Starting Point', + action: rootType + }); } loadData(request, isRoot = false, isRewind = false) { @@ -239,6 +246,11 @@ export class DetailContentContainer extends React.Component { transition: '' }); } + + Analytics.event({ + category: 'Spending Explorer - Data Type', + action: request.subdivision + }); } goDeeper(id, data) { @@ -254,6 +266,11 @@ export class DetailContentContainer extends React.Component { if (filterBy === 'award') { // we are at the bottom of the path, go to the award page Router.history.push(`/award/${id}`); + + Analytics.event({ + category: 'Spending Explorer - Exit', + action: `/award/${id}` + }); return; } @@ -312,6 +329,12 @@ export class DetailContentContainer extends React.Component { }, () => { this.loadData(request, false); }); + + Analytics.event({ + category: 'Spending Explorer - Drilldown', + action: filterBy, + label: `${data.name} - ${data.id}` + }); } changeSubdivisionType(type) { From 61503d5899c25f3a0741a80adbd7f66b0ca84d85 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Wed, 7 Feb 2018 15:41:53 -0500 Subject: [PATCH 57/89] Fix ugly download filter bar --- src/_scss/pages/modals/fullDownload/_filterBar.scss | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/_scss/pages/modals/fullDownload/_filterBar.scss b/src/_scss/pages/modals/fullDownload/_filterBar.scss index fe4fdc1603..b1ed31849c 100644 --- a/src/_scss/pages/modals/fullDownload/_filterBar.scss +++ b/src/_scss/pages/modals/fullDownload/_filterBar.scss @@ -5,6 +5,15 @@ .search-top-filter-header { margin-bottom: rem(10); + @include clearfix; + .header-title { + float: left; + font-weight: 600; + font-size: rem(14); + line-height: rem(18); + color: #525252; + margin: 0; + } } .search-top-filters-content { From a94f029ab9e06be6477fcfe27473c8e07d5832ee Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Wed, 7 Feb 2018 16:31:39 -0500 Subject: [PATCH 58/89] Advanced search download --- .../modals/fullDownload/screens/DownloadProgress.jsx | 5 +++-- src/js/containers/search/helpers/searchAnalytics.js | 11 +++++++++++ .../fullDownload/DownloadBottomBarContainer.jsx | 11 +++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/js/components/search/modals/fullDownload/screens/DownloadProgress.jsx b/src/js/components/search/modals/fullDownload/screens/DownloadProgress.jsx index 9733b197d5..28a1eee80d 100644 --- a/src/js/components/search/modals/fullDownload/screens/DownloadProgress.jsx +++ b/src/js/components/search/modals/fullDownload/screens/DownloadProgress.jsx @@ -5,15 +5,16 @@ import React from 'react'; import PropTypes from 'prop-types'; - import { CopyToClipboard } from 'react-copy-to-clipboard'; + import { CheckCircle } from 'components/sharedComponents/icons/Icons'; const propTypes = { hideModal: PropTypes.func, setDownloadCollapsed: PropTypes.func, expectedFile: PropTypes.string, - expectedUrl: PropTypes.string + expectedUrl: PropTypes.string, + download: PropTypes.object }; export default class DownloadProgress extends React.Component { diff --git a/src/js/containers/search/helpers/searchAnalytics.js b/src/js/containers/search/helpers/searchAnalytics.js index 7434b9f686..db6f1d4c5b 100644 --- a/src/js/containers/search/helpers/searchAnalytics.js +++ b/src/js/containers/search/helpers/searchAnalytics.js @@ -222,3 +222,14 @@ export const sendFieldCombinations = (events) => { action: fields.sort().join('-') }); }; + +export const uniqueFilterFields = (redux) => { + const events = convertFiltersToAnalyticEvents(redux); + const fields = uniq(events.reduce((parsed, event) => { + if (event.action) { + parsed.push(event.action); + } + return parsed; + }, [])); + return fields.sort().join('-'); +}; diff --git a/src/js/containers/search/modals/fullDownload/DownloadBottomBarContainer.jsx b/src/js/containers/search/modals/fullDownload/DownloadBottomBarContainer.jsx index 8485ffe14e..4295ec470d 100644 --- a/src/js/containers/search/modals/fullDownload/DownloadBottomBarContainer.jsx +++ b/src/js/containers/search/modals/fullDownload/DownloadBottomBarContainer.jsx @@ -10,6 +10,9 @@ import { connect } from 'react-redux'; import { isCancel } from 'axios'; import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'; +import Analytics from 'helpers/analytics/Analytics'; +import { uniqueFilterFields } from 'containers/search/helpers/searchAnalytics'; + import * as downloadActions from 'redux/actions/search/downloadActions'; import SearchAwardsOperation from 'models/search/SearchAwardsOperation'; @@ -125,6 +128,14 @@ export class DownloadBottomBarContainer extends React.Component { } } }); + + // get the selected filters as a single string + + Analytics.event({ + category: 'Advanced Search - Download', + action: this.props.download.type, + label: uniqueFilterFields(filters) + }); } checkStatus() { From 4d399755c2e9a448fd41f56d10315c3336b8cf1d Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Thu, 8 Feb 2018 11:38:29 -0500 Subject: [PATCH 59/89] Update test coverage --- .../fullDownload/DownloadBottomBarContainer.jsx | 4 ++-- .../search/helpers/searchAnalytics-test.js | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/js/containers/search/modals/fullDownload/DownloadBottomBarContainer.jsx b/src/js/containers/search/modals/fullDownload/DownloadBottomBarContainer.jsx index 4295ec470d..fb59303e4a 100644 --- a/src/js/containers/search/modals/fullDownload/DownloadBottomBarContainer.jsx +++ b/src/js/containers/search/modals/fullDownload/DownloadBottomBarContainer.jsx @@ -129,8 +129,8 @@ export class DownloadBottomBarContainer extends React.Component { } }); - // get the selected filters as a single string - + // send an analytic event of action download type and label value with all the filter + // field names Analytics.event({ category: 'Advanced Search - Download', action: this.props.download.type, diff --git a/tests/containers/search/helpers/searchAnalytics-test.js b/tests/containers/search/helpers/searchAnalytics-test.js index 328b3ab633..cb304677f6 100644 --- a/tests/containers/search/helpers/searchAnalytics-test.js +++ b/tests/containers/search/helpers/searchAnalytics-test.js @@ -248,7 +248,7 @@ describe('searchAnalytics', () => { }); describe('sendFieldCombinations', () => { - it('should send an Analytic event with a non-repeating - separated string of filter names', () => { + it('should send an Analytic event with a non-repeating `-` separated string of filter names', () => { const events = [{ action: 'action', label: 'label' @@ -270,4 +270,19 @@ describe('searchAnalytics', () => { }); }); }); + + describe('uniqueFilterFields', () => { + it('should return a string of non-repeating `-` separated string of filter names', () => { + const filters = { + timePeriodType: 'fy', + timePeriodFY: new Set(['1900']), + selectedAwardIDs: new OrderedMap({ + abc: 'abc' + }) + }; + + const fields = searchAnalytics.uniqueFilterFields(filters); + expect(fields).toEqual('Award ID-Time Period - Fiscal Year'); + }); + }); }); \ No newline at end of file From 488c9dc428471ceb39dfa9f8f8289d336e077b7d Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Thu, 8 Feb 2018 11:59:20 -0500 Subject: [PATCH 60/89] Removed keyword filter from advanced search page --- src/js/components/search/SearchSidebar.jsx | 6 - .../search/filters/keyword/Keyword.jsx | 109 ----------------- .../search/filters/keyword/KeywordHover.jsx | 38 ------ .../filterGroups/KeywordFilterGroup.jsx | 58 --------- .../filterGroups/TopFilterGroupGenerator.jsx | 3 - .../search/filters/KeywordContainer.jsx | 115 ------------------ .../topFilterBar/TopFilterBarContainer.jsx | 30 ----- .../dataMapping/search/awardsOperationKeys.js | 1 - src/js/dataMapping/search/filterFields.js | 3 - src/js/helpers/sidebarHelper.js | 5 - src/js/models/search/SearchAwardsOperation.js | 9 -- src/js/models/search/SearchOperation.js | 8 -- .../search/queryBuilders/KeywordQuery.js | 24 ---- .../actions/search/searchFilterActions.js | 6 - .../reducers/search/searchFiltersReducer.js | 8 -- .../search/SearchContainer-test.jsx | 6 +- .../filters/keyword/KeywordContainer-test.jsx | 95 --------------- tests/containers/search/mockSearchHashes.js | 1 - .../table/ResultsTableContainer-test.jsx | 2 +- .../TopFilterBarContainer-test.jsx | 29 ----- .../search/searchFiltersReducer-test.js | 22 +--- 21 files changed, 9 insertions(+), 569 deletions(-) delete mode 100644 src/js/components/search/filters/keyword/Keyword.jsx delete mode 100644 src/js/components/search/filters/keyword/KeywordHover.jsx delete mode 100644 src/js/components/search/topFilterBar/filterGroups/KeywordFilterGroup.jsx delete mode 100644 src/js/containers/search/filters/KeywordContainer.jsx delete mode 100644 src/js/models/search/queryBuilders/KeywordQuery.js delete mode 100644 tests/containers/search/filters/keyword/KeywordContainer-test.jsx diff --git a/src/js/components/search/SearchSidebar.jsx b/src/js/components/search/SearchSidebar.jsx index 6d67b12333..338630fdba 100644 --- a/src/js/components/search/SearchSidebar.jsx +++ b/src/js/components/search/SearchSidebar.jsx @@ -14,7 +14,6 @@ import AgencyContainer from 'containers/search/filters/AgencyContainer'; import LocationSectionContainer from 'containers/search/filters/location/LocationSectionContainer'; import RecipientSearchContainer from 'containers/search/filters/recipient/RecipientSearchContainer'; import RecipientTypeContainer from 'containers/search/filters/recipient/RecipientTypeContainer'; -import KeywordContainer from 'containers/search/filters/KeywordContainer'; import AwardIDSearchContainer from 'containers/search/filters/awardID/AwardIDSearchContainer'; import AwardAmountSearchContainer from 'containers/search/filters/awardAmount/AwardAmountSearchContainer'; @@ -25,15 +24,12 @@ import PricingTypeContainer from 'containers/search/filters/PricingTypeContainer import SetAsideContainer from 'containers/search/filters/SetAsideContainer'; import ExtentCompetedContainer from 'containers/search/filters/ExtentCompetedContainer'; -import KeywordHover from 'components/search/filters/keyword/KeywordHover'; - import { Filter as FilterIcon } from 'components/sharedComponents/icons/Icons'; import FilterSidebar from 'components/sharedComponents/filterSidebar/FilterSidebar'; import * as SidebarHelper from 'helpers/sidebarHelper'; const filters = { options: [ - 'Keyword', 'Time Period', 'Award Type', 'Agency', @@ -50,7 +46,6 @@ const filters = { 'Extent Competed' ], components: [ - KeywordContainer, TimePeriodContainer, AwardTypeContainer, AgencyContainer, @@ -67,7 +62,6 @@ const filters = { ExtentCompetedContainer ], accessories: [ - KeywordHover, null, null, null, diff --git a/src/js/components/search/filters/keyword/Keyword.jsx b/src/js/components/search/filters/keyword/Keyword.jsx deleted file mode 100644 index ae23ac4f84..0000000000 --- a/src/js/components/search/filters/keyword/Keyword.jsx +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Keyword.jsx - * Created by Emily Gullo 10/18/2016 - **/ - -import React from 'react'; -import PropTypes from 'prop-types'; - -import { Close } from 'components/sharedComponents/icons/Icons'; -import SubmitHint from 'components/sharedComponents/filterSidebar/SubmitHint'; -import IndividualSubmit from 'components/search/filters/IndividualSubmit'; - -const propTypes = { - selectedKeyword: PropTypes.string, - submitText: PropTypes.func, - changedInput: PropTypes.func, - removeKeyword: PropTypes.func, - value: PropTypes.string, - dirtyFilter: PropTypes.string -}; - -export default class Keyword extends React.Component { - constructor(props) { - super(props); - - this.searchKeyword = this.searchKeyword.bind(this); - this.removeKeyword = this.removeKeyword.bind(this); - } - - componentDidUpdate(prevProps) { - if (this.props.dirtyFilter && prevProps.dirtyFilter !== this.props.dirtyFilter) { - if (this.hint) { - this.hint.showHint(); - } - } - } - - searchKeyword(e) { - e.preventDefault(); - this.props.submitText(); - } - - removeKeyword() { - if (this.searchInput) { - // focus on the input field for accessibility users - this.searchInput.focus(); - } - this.props.removeKeyword(); - } - - render() { - let hideTags = 'hide'; - if (this.props.selectedKeyword !== '') { - hideTags = ''; - } - - const accessibility = { - 'aria-controls': 'selected-keyword-tags' - }; - - return ( -
    -
    -
    -
    - { - this.searchInput = input; - }} /> - -
    -
    - -
    - { - this.hint = component; - }} /> -
    - -
    - ); - } -} -Keyword.propTypes = propTypes; diff --git a/src/js/components/search/filters/keyword/KeywordHover.jsx b/src/js/components/search/filters/keyword/KeywordHover.jsx deleted file mode 100644 index 31dc1ca008..0000000000 --- a/src/js/components/search/filters/keyword/KeywordHover.jsx +++ /dev/null @@ -1,38 +0,0 @@ -/** - * KeywordHover.jsx - * Created by Kevin Li 12/4/17 - */ - -import React from 'react'; -import { InfoCircle } from 'components/sharedComponents/icons/Icons'; - -export default class KeywordHover extends React.Component { - render() { - return ( -
    -
    - -
    -
    - The Keyword field currently matches against - the following attributes: -
      -
    • Recipient Name
    • -
    • Recipient DUNS
    • -
    • Recipient Parent DUNS
    • -
    • NAICS Code
    • -
    • PSC Code
    • -
    • PIID (prime award only)
    • -
    • FAIN (prime award only)
    • -
    -
    - Note: -
    - Award Descriptions are not currently included - in Keyword matching. -
    -
    - ); - } -} - diff --git a/src/js/components/search/topFilterBar/filterGroups/KeywordFilterGroup.jsx b/src/js/components/search/topFilterBar/filterGroups/KeywordFilterGroup.jsx deleted file mode 100644 index 3aa60c2a94..0000000000 --- a/src/js/components/search/topFilterBar/filterGroups/KeywordFilterGroup.jsx +++ /dev/null @@ -1,58 +0,0 @@ -/** - * KeywordFilterGroup.jsx - * Created by Emily Gullo 03/09/2017 - */ - -import React from 'react'; -import PropTypes from 'prop-types'; - -import BaseTopFilterGroup from './BaseTopFilterGroup'; - -const propTypes = { - filter: PropTypes.object, - redux: PropTypes.object, - compressed: PropTypes.bool -}; - -export default class KeywordFilterGroup extends React.Component { - constructor(props) { - super(props); - - this.removeFilter = this.removeFilter.bind(this); - } - - removeFilter() { - // remove a single filter item - this.props.redux.clearFilterType('keyword'); - } - - generateTags() { - const tags = []; - - // check to see if a keyword is provided - const keyword = this.props.filter.values; - - const tag = { - value: keyword, - title: `Keyword | ${keyword}`, - isSpecial: false, - removeFilter: this.removeFilter - }; - - tags.push(tag); - - return tags; - } - - render() { - const tags = this.generateTags(); - - return (); - } -} - -KeywordFilterGroup.propTypes = propTypes; diff --git a/src/js/components/search/topFilterBar/filterGroups/TopFilterGroupGenerator.jsx b/src/js/components/search/topFilterBar/filterGroups/TopFilterGroupGenerator.jsx index cbb492a650..a72f75d6a4 100644 --- a/src/js/components/search/topFilterBar/filterGroups/TopFilterGroupGenerator.jsx +++ b/src/js/components/search/topFilterBar/filterGroups/TopFilterGroupGenerator.jsx @@ -12,7 +12,6 @@ import LocationFilterGroup from './LocationFilterGroup'; import AgencyFilterGroup from './AgencyFilterGroup'; import RecipientFilterGroup from './RecipientFilterGroup'; import RecipientTypeFilterGroup from './RecipientTypeFilterGroup'; -import KeywordFilterGroup from './KeywordFilterGroup'; import AwardIDFilterGroup from './AwardIDFilterGroup'; import AwardAmountFilterGroup from './AwardAmountFilterGroup'; import CFDAFilterGroup from './CFDAFilterGroup'; @@ -31,8 +30,6 @@ export const topFilterGroupGenerator = (config = { const groupKey = `top-filter-group-${config.filter.code}`; switch (config.filter.code) { - case 'keyword': - return ; case 'timePeriodFY': return ; case 'timePeriodDR': diff --git a/src/js/containers/search/filters/KeywordContainer.jsx b/src/js/containers/search/filters/KeywordContainer.jsx deleted file mode 100644 index b708f502bd..0000000000 --- a/src/js/containers/search/filters/KeywordContainer.jsx +++ /dev/null @@ -1,115 +0,0 @@ -/** - * KeywordContainer.jsx - * Created by Emily Gullo 11/30/16 - **/ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; -import { is } from 'immutable'; - -import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; - -import Keyword from 'components/search/filters/keyword/Keyword'; - -const propTypes = { - keyword: PropTypes.string, - appliedFilter: PropTypes.string, - updateTextSearchInput: PropTypes.func -}; - -const ga = require('react-ga'); - -export class KeywordContainer extends React.Component { - static logSelectedKeywordEvent(keyword) { - ga.event({ - category: 'Search Page Filter Applied', - action: 'Applied Keyword Filter', - label: keyword - }); - } - - constructor(props) { - super(props); - - this.state = { - value: '' - }; - - this.submitText = this.submitText.bind(this); - this.changedInput = this.changedInput.bind(this); - this.removeKeyword = this.removeKeyword.bind(this); - } - - componentWillMount() { - if (this.props.keyword !== '') { - this.populateInput(this.props.keyword); - } - } - - componentWillReceiveProps(nextProps) { - if (nextProps.keyword !== this.state.defaultValue) { - this.populateInput(nextProps.keyword); - } - } - - populateInput(value) { - this.setState({ - value - }); - } - - changedInput(e) { - this.setState({ - value: e.target.value - }); - } - - submitText() { - // take in keywords and pass to redux - this.props.updateTextSearchInput(this.state.value); - - // Analytics - if (this.state.value) { - KeywordContainer.logSelectedKeywordEvent(this.state.value); - } - } - - removeKeyword() { - this.setState({ - value: '' - }, () => { - this.submitText(); - }); - } - - dirtyFilter() { - if (is(this.props.appliedFilter, this.props.keyword)) { - return null; - } - return this.props.keyword; - } - - render() { - return ( - - ); - } -} - -KeywordContainer.propTypes = propTypes; - -export default connect( - (state) => ({ - keyword: state.filters.keyword, - appliedFilter: state.appliedFilters.filters.keyword - }), - (dispatch) => bindActionCreators(searchFilterActions, dispatch) -)(KeywordContainer); diff --git a/src/js/containers/search/topFilterBar/TopFilterBarContainer.jsx b/src/js/containers/search/topFilterBar/TopFilterBarContainer.jsx index a8378499d0..fa1926ed43 100644 --- a/src/js/containers/search/topFilterBar/TopFilterBarContainer.jsx +++ b/src/js/containers/search/topFilterBar/TopFilterBarContainer.jsx @@ -63,12 +63,6 @@ export class TopFilterBarContainer extends React.Component { filters.push(timeFilters); } - // prepare the keyword filters - const keywordFilters = this.prepareKeywords(props); - if (keywordFilters) { - filters.push(keywordFilters); - } - // prepare the award filters const awardFilters = this.prepareAwardTypes(props); if (awardFilters) { @@ -215,30 +209,6 @@ export class TopFilterBarContainer extends React.Component { return null; } - - /** - * Logic for parsing the current Redux keyword filter into a JS object that can be parsed by the - * top filter bar - */ - prepareKeywords(props) { - let selected = false; - const filter = {}; - - if (props.keyword) { - // keyword exists - selected = true; - filter.code = 'keyword'; - filter.name = 'Keyword'; - - filter.values = props.keyword; - } - - if (selected) { - return filter; - } - return null; - } - /** * Logic for parsing the current Redux award type filter into a JS object that can be parsed by * the top filter bar diff --git a/src/js/dataMapping/search/awardsOperationKeys.js b/src/js/dataMapping/search/awardsOperationKeys.js index 014393b827..def818c4e1 100644 --- a/src/js/dataMapping/search/awardsOperationKeys.js +++ b/src/js/dataMapping/search/awardsOperationKeys.js @@ -4,7 +4,6 @@ */ export const rootKeys = { - keyword: 'keyword', timePeriod: 'time_period', awardType: 'award_type_codes', agencies: 'agencies', diff --git a/src/js/dataMapping/search/filterFields.js b/src/js/dataMapping/search/filterFields.js index a9e3768b8d..aaff16a131 100644 --- a/src/js/dataMapping/search/filterFields.js +++ b/src/js/dataMapping/search/filterFields.js @@ -1,7 +1,6 @@ export const awardFields = { startDate: 'period_of_performance_start_date', endDate: 'period_of_performance_current_end_date', - keyword: 'description', locationScope: 'place_of_performance__location_country_code', location: 'place_of_performance__location_id', awardType: 'type', @@ -48,7 +47,6 @@ export const tasCategoriesFields = { export const transactionFields = { date: 'action_date', - keyword: 'description', locationScope: 'place_of_performance__location_country_code', location: 'place_of_performance__location_id', fundingAgencyCGAC: 'award__financial_set__treasury_account__agency_id', @@ -83,7 +81,6 @@ export const transactionFields = { export const accountAwardsFields = { startDate: 'award__period_of_performance_start_date', endDate: 'award__period_of_performance_current_end_date', - keyword: 'award__description', locationScope: 'award__place_of_performance__location_country_code', location: 'award__place_of_performance__location_id', awardType: 'award__type', diff --git a/src/js/helpers/sidebarHelper.js b/src/js/helpers/sidebarHelper.js index b5cffafaa6..15aca9ef71 100644 --- a/src/js/helpers/sidebarHelper.js +++ b/src/js/helpers/sidebarHelper.js @@ -5,11 +5,6 @@ /* eslint-disable default-export */ export const filterHasSelections = (reduxFilters, filter) => { switch (filter) { - case 'Keyword': - if (reduxFilters.keyword !== '') { - return true; - } - return false; case 'Time Period': if (reduxFilters.timePeriodFY.toArray().length > 0 || (reduxFilters.timePeriodRange diff --git a/src/js/models/search/SearchAwardsOperation.js b/src/js/models/search/SearchAwardsOperation.js index 305105987d..1116deae29 100644 --- a/src/js/models/search/SearchAwardsOperation.js +++ b/src/js/models/search/SearchAwardsOperation.js @@ -9,8 +9,6 @@ import * as FiscalYearHelper from 'helpers/fiscalYearHelper'; class SearchAwardsOperation { constructor() { - this.keyword = ''; - this.timePeriodType = 'fy'; this.timePeriodFY = []; this.timePeriodRange = []; @@ -42,8 +40,6 @@ class SearchAwardsOperation { } fromState(state) { - this.keyword = state.keyword; - this.timePeriodFY = state.timePeriodFY.toArray(); this.timePeriodRange = []; this.timePeriodType = state.timePeriodType; @@ -82,11 +78,6 @@ class SearchAwardsOperation { // Convert the search operation into JS objects const filters = {}; - // Add keyword - if (this.keyword !== '') { - filters[rootKeys.keyword] = this.keyword; - } - // Add Time Period if (this.timePeriodFY.length > 0 || this.timePeriodRange.length === 2) { if (this.timePeriodType === 'fy' && this.timePeriodFY.length > 0) { diff --git a/src/js/models/search/SearchOperation.js b/src/js/models/search/SearchOperation.js index d07d35e758..bfaa1d8248 100644 --- a/src/js/models/search/SearchOperation.js +++ b/src/js/models/search/SearchOperation.js @@ -11,14 +11,12 @@ import * as LocationQuery from './queryBuilders/LocationQuery'; import * as BudgetCategoryQuery from './queryBuilders/BudgetCategoryQuery'; import * as AgencyQuery from './queryBuilders/AgencyQuery'; import * as RecipientQuery from './queryBuilders/RecipientQuery'; -import * as KeywordQuery from './queryBuilders/KeywordQuery'; import * as AwardIDQuery from './queryBuilders/AwardIDQuery'; import * as AwardAmountQuery from './queryBuilders/AwardAmountQuery'; import * as OtherFiltersQuery from './queryBuilders/OtherFiltersQuery'; class SearchOperation { constructor() { - this.keyword = ''; this.awardType = []; this.timePeriodType = 'fy'; this.timePeriodFY = []; @@ -59,7 +57,6 @@ class SearchOperation { } fromState(state) { - this.keyword = state.keyword; this.awardType = state.awardType.toArray(); this.timePeriodFY = state.timePeriodFY.toArray(); this.timePeriodRange = []; @@ -102,11 +99,6 @@ class SearchOperation { // data structures between Awards and Transactions const filters = []; - // add keyword query - if (this.keyword !== '') { - filters.push(KeywordQuery.buildKeywordQuery(this.keyword, this.searchContext)); - } - // Add award types if (this.awardType.length > 0) { filters.push(AwardTypeQuery.buildQuery(this.awardType, this.searchContext)); diff --git a/src/js/models/search/queryBuilders/KeywordQuery.js b/src/js/models/search/queryBuilders/KeywordQuery.js deleted file mode 100644 index 770f9a96c7..0000000000 --- a/src/js/models/search/queryBuilders/KeywordQuery.js +++ /dev/null @@ -1,24 +0,0 @@ -/** -* KeywordQuery.js -* Created by Emily Gullo -**/ - -/* eslint-disable import/prefer-default-export */ -// We only have one export but want to maintain consistency with other query modules - -import * as FilterFields from 'dataMapping/search/filterFields'; - -export const buildKeywordQuery = (value, searchContext = 'award') => { - const keyword = value; - - const keywordField = FilterFields[`${searchContext}Fields`].keyword; - - const filter = { - field: keywordField, - operation: "search", - value: keyword - }; - - return filter; -}; -/* eslint-enable import/prefer-default-export */ diff --git a/src/js/redux/actions/search/searchFilterActions.js b/src/js/redux/actions/search/searchFilterActions.js index bff77e6b97..317bec9da0 100644 --- a/src/js/redux/actions/search/searchFilterActions.js +++ b/src/js/redux/actions/search/searchFilterActions.js @@ -3,12 +3,6 @@ * Created by Kevin Li 11/1/16 **/ -// Keyword Filter -export const updateTextSearchInput = (state) => ({ - type: 'UPDATE_TEXT_SEARCH', - textInput: state -}); - // Time Period Filter export const updateTimePeriod = (state) => ({ type: 'UPDATE_SEARCH_FILTER_TIME_PERIOD', diff --git a/src/js/redux/reducers/search/searchFiltersReducer.js b/src/js/redux/reducers/search/searchFiltersReducer.js index 524e2457df..33f3f0f5c8 100644 --- a/src/js/redux/reducers/search/searchFiltersReducer.js +++ b/src/js/redux/reducers/search/searchFiltersReducer.js @@ -38,7 +38,6 @@ export const requiredTypes = { }; export const initialState = { - keyword: '', timePeriodType: 'fy', timePeriodFY: new Set(), timePeriodStart: null, @@ -64,13 +63,6 @@ export const initialState = { const searchFiltersReducer = (state = initialState, action) => { switch (action.type) { - // Free Text Search - case 'UPDATE_TEXT_SEARCH': { - return Object.assign({}, state, { - keyword: action.textInput - }); - } - // Time Period Filter case 'UPDATE_SEARCH_FILTER_TIME_PERIOD': { // FY time period is stored as an ImmutableJS set diff --git a/tests/containers/search/SearchContainer-test.jsx b/tests/containers/search/SearchContainer-test.jsx index 2b87a3c40f..8103f9cb02 100644 --- a/tests/containers/search/SearchContainer-test.jsx +++ b/tests/containers/search/SearchContainer-test.jsx @@ -97,7 +97,7 @@ describe('SearchContainer', () => { }); const nextFilters = Object.assign({}, initialState, { - keyword: 'blerg' + timePeriodFY: new Set(['1987']) }); const nextProps = Object.assign({}, mockRedux, mockActions, { @@ -158,7 +158,7 @@ describe('SearchContainer', () => { it('should generate a hash if there are filters applied', () => { const filters = Object.assign({}, initialState, { - keyword: 'blerg' + timePeriodFY: new Set(['1987']) }); const redux = Object.assign({}, mockRedux, { @@ -282,7 +282,7 @@ describe('SearchContainer', () => { {...mockRedux} />); const modifiedState = Object.assign({}, initialState, { - keyword: 'blerg' + timePeriodFY: new Set(['1987']) }); const unfiltered = container.instance().determineIfUnfiltered(modifiedState); diff --git a/tests/containers/search/filters/keyword/KeywordContainer-test.jsx b/tests/containers/search/filters/keyword/KeywordContainer-test.jsx deleted file mode 100644 index 3480ed5344..0000000000 --- a/tests/containers/search/filters/keyword/KeywordContainer-test.jsx +++ /dev/null @@ -1,95 +0,0 @@ -/** - * KeywordContainer-test.jsx - * Created by Emily Gullo 03/13/2017 - */ - -import React from 'react'; -import { shallow } from 'enzyme'; -import sinon from 'sinon'; - -import { KeywordContainer } from 'containers/search/filters/KeywordContainer'; - -const initialFilters = { - keyword: '', - appliedFilter: '' -}; - -describe('KeywordContainer', () => { - describe('submitText', () => { - it('should submit given keyword text to redux state', () => { - const mockReduxActionKeyword = jest.fn((args) => { - expect(args).toEqual('Education'); - }); - const keywordContainer = shallow( - ); - - const submitTextSpy = sinon.spy(keywordContainer.instance(), - 'submitText'); - - // Add keyword to redux - keywordContainer.setState({ - value: 'Education' - }); - keywordContainer.instance().submitText(); - - // everything should be updated now - expect(submitTextSpy.callCount).toEqual(1); - expect(mockReduxActionKeyword).toHaveBeenCalled(); - - // reset the spies - submitTextSpy.reset(); - }); - it('should overwrite a previous keyword with a new keyword', () => { - const existingFilters = Object.assign({}, initialFilters, { - keyword: 'Education' - }); - const mockReduxActionKeyword = jest.fn((args) => { - expect(args).toEqual('Financial'); - }); - const keywordContainer = shallow( - ); - - const submitTextSpy = sinon.spy(keywordContainer.instance(), - 'submitText'); - - // Add keyword to redux - keywordContainer.instance().populateInput('Financial'); - keywordContainer.instance().submitText(); - - // everything should be updated now - expect(submitTextSpy.callCount).toEqual(1); - expect(mockReduxActionKeyword).toHaveBeenCalled(); - - // reset the spies - submitTextSpy.reset(); - }); - }); - describe('dirtyFilter', () => { - it('should return the keyword string when the staged filters do not match with the applied filters', () => { - const container = shallow( - ); - - container.setProps({ - keyword: 'blerg' - }); - - const changed = container.instance().dirtyFilter(); - expect(changed).toEqual('blerg'); - }); - it('should return null when the staged filters match with the applied filters', () => { - const container = shallow( - ); - - const changed = container.instance().dirtyFilter(); - expect(changed).toBeFalsy(); - }); - }); -}); diff --git a/tests/containers/search/mockSearchHashes.js b/tests/containers/search/mockSearchHashes.js index 9b4b2518c3..d95a4ca33c 100644 --- a/tests/containers/search/mockSearchHashes.js +++ b/tests/containers/search/mockSearchHashes.js @@ -17,7 +17,6 @@ export const mockFilters = { selectedLocations: {}, recipientType: [], timePeriodFY: [`${FiscalYearHelper.currentFiscalYear()}`], - keyword: "", timePeriodType: "fy", timePeriodStart: null, selectedAwardingAgencies: {}, diff --git a/tests/containers/search/table/ResultsTableContainer-test.jsx b/tests/containers/search/table/ResultsTableContainer-test.jsx index 47326b8185..122db8230d 100644 --- a/tests/containers/search/table/ResultsTableContainer-test.jsx +++ b/tests/containers/search/table/ResultsTableContainer-test.jsx @@ -38,7 +38,7 @@ describe('ResultsTableContainer', () => { // update the filters const newFilters = Object.assign({}, mockRedux.filters, { - keyword: 'blah blah' + timePeriodFY: new Set(['1987']) }); container.setProps({ filters: newFilters diff --git a/tests/containers/search/topFilterBar/TopFilterBarContainer-test.jsx b/tests/containers/search/topFilterBar/TopFilterBarContainer-test.jsx index 9c5cc3e0d1..33f55e1f87 100644 --- a/tests/containers/search/topFilterBar/TopFilterBarContainer-test.jsx +++ b/tests/containers/search/topFilterBar/TopFilterBarContainer-test.jsx @@ -91,35 +91,6 @@ describe('TopFilterBarContainer', () => { expect(topBarContainer.instance().prepareFilters).toHaveBeenCalledTimes(1); }); - it('should update component state with Redux keyword filter when available', () => { - // mount the container with default props - const topBarContainer = setup({ - reduxFilters: Object.assign({}, stateWithoutDefault), - updateFilterCount: jest.fn() - }); - - expect(topBarContainer.state().filters).toHaveLength(0); - - const keywordFilter = Object.assign({}, stateWithoutDefault, { - keyword: 'Education' - }); - - topBarContainer.setProps({ - reduxFilters: keywordFilter - }); - - expect(topBarContainer.state().filters).toHaveLength(1); - - const filterItem = topBarContainer.state().filters[0]; - const expectedFilterState = { - code: 'keyword', - name: 'Keyword', - values: 'Education' - }; - - expect(filterItem).toEqual(expectedFilterState); - }); - it('should update component state with Redux time filters when available', () => { // mount the container with default props const topBarContainer = setup(defaultProps); diff --git a/tests/redux/reducers/search/searchFiltersReducer-test.js b/tests/redux/reducers/search/searchFiltersReducer-test.js index 0794fa3a2a..ec75984b70 100644 --- a/tests/redux/reducers/search/searchFiltersReducer-test.js +++ b/tests/redux/reducers/search/searchFiltersReducer-test.js @@ -122,18 +122,6 @@ describe('searchFiltersReducer', () => { }); }); - describe('UPDATE_TEXT_SEARCH', () => { - it('should set the keyword filter option to the input string', () => { - const action = { - type: 'UPDATE_TEXT_SEARCH', - textInput: 'business' - }; - - const updatedState = searchFiltersReducer(undefined, action); - expect(updatedState.keyword).toEqual('business'); - }); - }); - describe('UPDATE_SELECTED_LOCATIONS', () => { const action = { type: 'UPDATE_SELECTED_LOCATIONS', @@ -922,24 +910,24 @@ describe('searchFiltersReducer', () => { describe('RESTORE_HASHED_FILTERS', () => { it('should create a brand new state based on the initial state with the provided inputs', () => { const originalState = Object.assign({}, initialState); - originalState.keyword = 'hello'; originalState.recipientDomesticForeign = 'foreign'; originalState.awardType = new Set(['A', 'B']); + originalState.timePeriodFY = new Set(['1987']); let state = searchFiltersReducer(originalState, {}); - expect(state.keyword).toEqual('hello'); expect(state.recipientDomesticForeign).toEqual('foreign'); expect(state.awardType).toEqual(new Set(['A', 'B'])); + expect(state.timePeriodFY).toEqual(new Set(['1987'])); const action = { type: 'RESTORE_HASHED_FILTERS', filters: { - keyword: 'bye', - recipientDomesticForeign: 'domestic' + recipientDomesticForeign: 'domestic', + timePeriodFY: new Set (['1999']) } }; state = searchFiltersReducer(state, action); - expect(state.keyword).toEqual('bye'); + expect(state.timePeriodFY).toEqual(new Set(['1999'])); expect(state.recipientDomesticForeign).toEqual('domestic'); expect(state.awardType).toEqual(new Set([])); }); From e373d915a5983dcb719523393296d9c9bf4c106a Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Thu, 8 Feb 2018 12:02:39 -0500 Subject: [PATCH 61/89] updated endpoint url --- src/js/helpers/accountLandingHelper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/helpers/accountLandingHelper.js b/src/js/helpers/accountLandingHelper.js index de163aff3b..de249409be 100644 --- a/src/js/helpers/accountLandingHelper.js +++ b/src/js/helpers/accountLandingHelper.js @@ -10,7 +10,7 @@ export const fetchAllAccounts = (data) => { const source = CancelToken.source(); return { promise: Axios.request({ - url: 'v2/references/federal_account/', + url: 'v2/federal_accounts/', baseURL: kGlobalConstants.API, method: 'post', data, From 135956faaa33a860864b8124853e14d317a81899 Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Thu, 8 Feb 2018 14:48:57 -0500 Subject: [PATCH 62/89] Hide parens if no agency acronym; use new loading/ error messages --- .../accountLanding/table/resultsSection.scss | 24 ++------- .../AccountLandingResultsSection.jsx | 50 +++++++++---------- .../AccountLandingContainer.jsx | 9 +++- 3 files changed, 36 insertions(+), 47 deletions(-) diff --git a/src/_scss/pages/accountLanding/table/resultsSection.scss b/src/_scss/pages/accountLanding/table/resultsSection.scss index da9b1f0028..22588e921e 100644 --- a/src/_scss/pages/accountLanding/table/resultsSection.scss +++ b/src/_scss/pages/accountLanding/table/resultsSection.scss @@ -1,22 +1,8 @@ +@import "pages/search/results/table/_tableMessages"; +@import "pages/search/results/screens/screens"; .results-table-section { - transition: opacity 0.25s ease; - + position: relative; + min-height: rem(200); + @include transition(opacity 0.25s ease-in); @import './table'; - - .results-table-section__loading-wrapper_loading { - opacity: 0.5; - @include transition(opacity 0.25s ease-in); - } - - .results-table-section__message { - margin: rem(90) auto; - font-size: rem(36); - font-weight: 300; - color: $color-base; - text-align: center; - .results-table-section__search-string { - color: $color-active; - font-weight: normal; - } - } } diff --git a/src/js/components/accountLanding/AccountLandingResultsSection.jsx b/src/js/components/accountLanding/AccountLandingResultsSection.jsx index eeaa259e1d..765ebdb458 100644 --- a/src/js/components/accountLanding/AccountLandingResultsSection.jsx +++ b/src/js/components/accountLanding/AccountLandingResultsSection.jsx @@ -6,6 +6,10 @@ import React from 'react'; import PropTypes from 'prop-types'; +import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'; + +import ResultsTableLoadingMessage from 'components/search/table/ResultsTableLoadingMessage'; +import ResultsTableErrorMessage from 'components/search/table/ResultsTableErrorMessage'; import AccountLandingTable from './table/AccountLandingTable'; const propTypes = { @@ -19,44 +23,38 @@ const propTypes = { export default class AccountLandingResultsSection extends React.Component { render() { - let loadingWrapper = 'results-table-section__loading-wrapper'; let message = null; + let table = ( + + ); if (this.props.inFlight) { - loadingWrapper = 'results-table-section__loading-wrapper results-table-section__loading-wrapper_loading'; message = ( -
    - Loading data... +
    +
    ); } else if (this.props.results.length === 0) { // no results - if (this.props.accountSearchString) { - message = ( -
    - No results found for “ - - {this.props.accountSearchString} - ”. -
    - ); - } - else { - message = ( -
    - No results found. -
    - ); - } + table = null; + message = ( +
    + +
    + ); } return (
    -
    - -
    - {message} + + {message} + + {table}
    ); } diff --git a/src/js/containers/accountLanding/AccountLandingContainer.jsx b/src/js/containers/accountLanding/AccountLandingContainer.jsx index 7e9808a6e6..d5c8e64ac7 100644 --- a/src/js/containers/accountLanding/AccountLandingContainer.jsx +++ b/src/js/containers/accountLanding/AccountLandingContainer.jsx @@ -158,16 +158,21 @@ export default class AccountLandingContainer extends React.Component { const formattedCurrency = MoneyFormatter.formatMoneyWithPrecision(item.budgetary_resources, 0); + let formattedAgency = item.managing_agency; + if (item.managing_agency_acronym) { + formattedAgency = `${item.managing_agency} (${item.managing_agency_acronym})`; + } + const account = { account_id: item.account_id, account_number: item.account_number, - managing_agency: `${item.managing_agency} (${item.managing_agency_acronym})`, + managing_agency: formattedAgency, account_name: item.account_name, budgetary_resources: item.budgetary_resources, display: { account_number: `${item.account_number}`, account_name: `${item.account_name}`, - managing_agency: `${item.managing_agency} (${item.managing_agency_acronym})`, + managing_agency: formattedAgency, budgetary_resources: formattedCurrency } }; From 44beef750224753abe8c8b03617308793583c233 Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Thu, 8 Feb 2018 15:26:31 -0500 Subject: [PATCH 63/89] Pagination fixes; added error state --- src/_scss/components/_pagination.scss | 30 +++++++++---------- .../accountLanding/AccountLandingContent.jsx | 2 ++ .../AccountLandingResultsSection.jsx | 4 +-- .../sharedComponents/Pagination.jsx | 18 ++++++----- .../AccountLandingContainer.jsx | 8 +++-- 5 files changed, 34 insertions(+), 28 deletions(-) diff --git a/src/_scss/components/_pagination.scss b/src/_scss/components/_pagination.scss index 3d1bd18e75..58ffdf7355 100644 --- a/src/_scss/components/_pagination.scss +++ b/src/_scss/components/_pagination.scss @@ -1,17 +1,19 @@ .pagination { - margin: rem(15); + @include display(flex); + @include flex-direction(row); + @include align-items(center); .pagination__totals { - @include display(flex); - @include flex-direction(row); - float: left; + @include flex(0 0 auto); } .pager { - list-style-type: none; @include display(flex); @include flex-direction(row); - float: right; + @include justify-content(flex-end); + @include flex(1 1 auto); + list-style-type: none; margin: 0; .pager__item { + @include flex(0 0 auto); .pager__button { padding: rem(7) rem(10); background-color: $color-white; @@ -23,16 +25,7 @@ &:hover { background-color: $color-primary-alt-lightest; } - } - &.pager__item_active { - .pager__button { - background-color: $color-primary-alt-dark; - color: $color-white; - border-color: $color-primary-alt-dark; - } - } - &.pager__item_disabled { - .pager__button { + &.pager__button_disabled { border-color: $color-gray-lighter; color: $color-gray-light; &:hover { @@ -40,6 +33,11 @@ cursor: auto; } } + &.pager__button_active { + background-color: $color-primary-alt-dark; + color: $color-white; + border-color: $color-primary-alt-dark; + } } } .pager__ellipsis { diff --git a/src/js/components/accountLanding/AccountLandingContent.jsx b/src/js/components/accountLanding/AccountLandingContent.jsx index 81f37b324a..ae7d7e502f 100644 --- a/src/js/components/accountLanding/AccountLandingContent.jsx +++ b/src/js/components/accountLanding/AccountLandingContent.jsx @@ -14,6 +14,7 @@ const propTypes = { results: PropTypes.array, accountSearchString: PropTypes.string, inFlight: PropTypes.bool, + error: PropTypes.bool, columns: PropTypes.array, setAccountSearchString: PropTypes.func, onChangePage: PropTypes.func, @@ -48,6 +49,7 @@ export default class AccountLandingContent extends React.Component { columns={this.props.columns} results={this.props.results} inFlight={this.props.inFlight} + error={this.props.error} accountSearchString={this.props.accountSearchString} order={this.props.order} updateSort={this.props.updateSort} /> diff --git a/src/js/components/accountLanding/AccountLandingResultsSection.jsx b/src/js/components/accountLanding/AccountLandingResultsSection.jsx index 765ebdb458..8c92764c8d 100644 --- a/src/js/components/accountLanding/AccountLandingResultsSection.jsx +++ b/src/js/components/accountLanding/AccountLandingResultsSection.jsx @@ -14,6 +14,7 @@ import AccountLandingTable from './table/AccountLandingTable'; const propTypes = { inFlight: PropTypes.bool, + error: PropTypes.bool, results: PropTypes.array, columns: PropTypes.array, accountSearchString: PropTypes.string, @@ -35,8 +36,7 @@ export default class AccountLandingResultsSection extends React.Component {
    ); } - else if (this.props.results.length === 0) { - // no results + else if (this.props.error) { table = null; message = (
    diff --git a/src/js/components/sharedComponents/Pagination.jsx b/src/js/components/sharedComponents/Pagination.jsx index 59340d2f55..19dcac2799 100644 --- a/src/js/components/sharedComponents/Pagination.jsx +++ b/src/js/components/sharedComponents/Pagination.jsx @@ -76,7 +76,7 @@ export default class Pagination extends React.Component { } else if (currentPage === 3) { startPage = 1; - endPage = currentPage; + endPage = 4; } } else if (currentPage > (totalPages - 3)) { @@ -87,7 +87,7 @@ export default class Pagination extends React.Component { endPage = currentPage; } else if (currentPage === (totalPages - 2)) { - startPage = currentPage; + startPage = currentPage - 1; endPage = totalPages; } } @@ -127,9 +127,9 @@ export default class Pagination extends React.Component { (
  • + className="pager__item"> @@ -160,9 +160,10 @@ export default class Pagination extends React.Component { {resultsText}
    • -
    • +
    • @@ -171,9 +172,10 @@ export default class Pagination extends React.Component { {pageButtons} {pager.nextEllipses} {pager.lastButton} -
    • +
    • diff --git a/src/js/containers/accountLanding/AccountLandingContainer.jsx b/src/js/containers/accountLanding/AccountLandingContainer.jsx index d5c8e64ac7..c13ec6777d 100644 --- a/src/js/containers/accountLanding/AccountLandingContainer.jsx +++ b/src/js/containers/accountLanding/AccountLandingContainer.jsx @@ -27,6 +27,7 @@ export default class AccountLandingContainer extends React.Component { }, columns: [], inFlight: false, + error: false, searchString: '', results: [], totalItems: 0, @@ -114,7 +115,8 @@ export default class AccountLandingContainer extends React.Component { } this.setState({ - inFlight: true + inFlight: true, + error: false }); // generate the params @@ -143,7 +145,8 @@ export default class AccountLandingContainer extends React.Component { this.accountsRequest = null; if (!isCancel(err)) { this.setState({ - inFlight: false + inFlight: false, + error: true }); console.log(err); } @@ -190,6 +193,7 @@ export default class AccountLandingContainer extends React.Component { Date: Thu, 8 Feb 2018 15:33:32 -0500 Subject: [PATCH 64/89] Added pagination positioning in the spending explorer table --- .../pages/explorer/detail/visualization/table/_table.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/_scss/pages/explorer/detail/visualization/table/_table.scss b/src/_scss/pages/explorer/detail/visualization/table/_table.scss index ffbb6cdbc5..2f131b105f 100644 --- a/src/_scss/pages/explorer/detail/visualization/table/_table.scss +++ b/src/_scss/pages/explorer/detail/visualization/table/_table.scss @@ -5,6 +5,10 @@ display: block; overflow: auto; + .pagination { + margin: rem(15); + } + &.no-results { border: none; } From aea70cef6ab6444bcdcbed94d79a37ec32e568e8 Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Thu, 8 Feb 2018 15:41:25 -0500 Subject: [PATCH 65/89] Show the Award Type Description, instead of the Award Type Code, on the Federal Account page. Fixed ordering of Award Type in the Contracts view. --- .../search/accountTableSearchFields.js | 30 +++++++++---------- .../models/v2/BaseFederalAccountAwardRow.js | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/js/dataMapping/search/accountTableSearchFields.js b/src/js/dataMapping/search/accountTableSearchFields.js index 3e6bbf205d..0c49243c04 100644 --- a/src/js/dataMapping/search/accountTableSearchFields.js +++ b/src/js/dataMapping/search/accountTableSearchFields.js @@ -12,7 +12,7 @@ const accountTableSearchFields = { period_of_performance_start_date: 'desc', period_of_performance_current_end_date: 'desc', total_obligation: 'desc', - type: 'asc', + type_description: 'asc', awarding_agency_name: 'asc', awarding_subtier_name: 'asc', latest_transaction__action_date: 'desc', @@ -25,7 +25,7 @@ const accountTableSearchFields = { startDate: 'period_of_performance_start_date', endDate: 'period_of_performance_current_end_date', awardAmount: 'total_obligation', - awardType: 'type', + awardType: 'type_description', awardingToptierAgency: 'awarding_agency__toptier_agency__name', awardingSubtierAgency: 'awarding_agency__subtier_agency__name', issuedDate: 'latest_transaction__action_date', @@ -40,9 +40,9 @@ const accountTableSearchFields = { 'period_of_performance_start_date', 'period_of_performance_current_end_date', 'total_obligation', - 'type', 'awarding_agency_name', - 'awarding_subtier_name' + 'awarding_subtier_name', + 'type_description' ], _mapping: { award_id: 'awardId', @@ -50,7 +50,7 @@ const accountTableSearchFields = { period_of_performance_start_date: 'startDate', period_of_performance_current_end_date: 'endDate', total_obligation: 'awardAmount', - type: 'awardType', + type_description: 'awardType', awarding_agency_name: 'awardingToptierAgency', awarding_subtier_name: 'awardingSubtierAgency' }, @@ -59,7 +59,7 @@ const accountTableSearchFields = { period_of_performance_start_date: 'Start Date', period_of_performance_current_end_date: 'End Date', total_obligation: 'Award Amount', - type: 'Contract Award Type', + type_description: 'Contract Award Type', awarding_agency_name: 'Awarding Agency', awarding_subtier_name: 'Awarding Sub Agency' }, @@ -73,7 +73,7 @@ const accountTableSearchFields = { 'total_obligation', 'awarding_agency_name', 'awarding_subtier_name', - 'type' + 'type_description' ], _mapping: { award_id: 'awardId', @@ -81,7 +81,7 @@ const accountTableSearchFields = { period_of_performance_start_date: 'startDate', period_of_performance_current_end_date: 'endDate', total_obligation: 'awardAmount', - type: 'awardType', + type_description: 'awardType', awarding_agency_name: 'awardingToptierAgency', awarding_subtier_name: 'awardingSubtierAgency' }, @@ -92,7 +92,7 @@ const accountTableSearchFields = { total_obligation: 'Award Amount', awarding_agency_name: 'Awarding Agency', awarding_subtier_name: 'Awarding Sub Agency', - type: 'Award Type' + type_description: 'Award Type' }, direct_payments: { _defaultSortField: 'total_obligation', @@ -104,7 +104,7 @@ const accountTableSearchFields = { 'total_obligation', 'awarding_agency_name', 'awarding_subtier_name', - 'type' + 'type_description' ], _mapping: { award_id: 'awardId', @@ -112,7 +112,7 @@ const accountTableSearchFields = { period_of_performance_start_date: 'startDate', period_of_performance_current_end_date: 'endDate', total_obligation: 'awardAmount', - type: 'awardType', + type_description: 'awardType', awarding_agency_name: 'awardingToptierAgency', awarding_subtier_name: 'awardingSubtierAgency' }, @@ -123,7 +123,7 @@ const accountTableSearchFields = { total_obligation: 'Award Amount', awarding_agency_name: 'Awarding Agency', awarding_subtier_name: 'Awarding Sub Agency', - type: 'Award Type' + type_description: 'Award Type' }, loans: { _defaultSortField: 'latest_transaction__assistance_data__face_value_loan_guarantee', @@ -172,7 +172,7 @@ const accountTableSearchFields = { 'total_obligation', 'awarding_agency_name', 'awarding_subtier_name', - 'type' + 'type_description' ], _mapping: { award_id: 'awardId', @@ -180,7 +180,7 @@ const accountTableSearchFields = { period_of_performance_start_date: 'startDate', period_of_performance_current_end_date: 'endDate', total_obligation: 'awardAmount', - type: 'awardType', + type_description: 'awardType', awarding_agency_name: 'awardingToptierAgency', awarding_subtier_name: 'awardingSubtierAgency' }, @@ -191,7 +191,7 @@ const accountTableSearchFields = { total_obligation: 'Award Amount', awarding_agency_name: 'Awarding Agency', awarding_subtier_name: 'Awarding Sub Agency', - type: 'Award Type' + type_description: 'Award Type' } }; /* eslint-enable max-len */ diff --git a/src/js/models/v2/BaseFederalAccountAwardRow.js b/src/js/models/v2/BaseFederalAccountAwardRow.js index d06aec2522..38bd3c8b75 100644 --- a/src/js/models/v2/BaseFederalAccountAwardRow.js +++ b/src/js/models/v2/BaseFederalAccountAwardRow.js @@ -15,7 +15,7 @@ const BaseFederalAccountAwardRow = { this._startDate = parseDate(data.period_of_performance_start_date || null); this._endDate = parseDate(data.period_of_performance_current_end_date || null); this._awardAmount = data.total_obligation || 0; - this.awardType = data.type || ''; + this.awardType = data.type_description || ''; this.awardingToptierAgency = data.awarding_agency.toptier_agency.name || ''; this.awardingSubtierAgency = data.awarding_agency.subtier_agency.name || ''; this._issuedDate = parseDate((data.latest_transaction && data.latest_transaction.action_date)); From 159b932119bbb2d80d9f1301febbd59bd6d1fa29 Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Thu, 8 Feb 2018 15:52:34 -0500 Subject: [PATCH 66/89] Update hover text and link --- src/js/components/search/header/NoDownloadHover.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/js/components/search/header/NoDownloadHover.jsx b/src/js/components/search/header/NoDownloadHover.jsx index d63f84db2c..78112f2534 100644 --- a/src/js/components/search/header/NoDownloadHover.jsx +++ b/src/js/components/search/header/NoDownloadHover.jsx @@ -18,8 +18,9 @@ const NoDownloadHover = () => (
    - Please visit the Download Center page to export - more than 500,000 records or limit your results with additional filters. + Our Advanced Search limits downloads to 500,000 records. + Narrow your search using additional filters, or grab larger files from + our Award Data Archive.
    From 0f0b8bc0721c1b51035d14d9d7663a648760788a Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Thu, 8 Feb 2018 16:00:44 -0500 Subject: [PATCH 67/89] Bulk download analytics --- .../bulkDownload/archive/table/TableRow.jsx | 17 +++- .../bulkDownload/sidebar/SidebarButton.jsx | 12 ++- .../BulkDownloadPageContainer.jsx | 4 + .../bulkDownload/helpers/downloadAnalytics.js | 75 ++++++++++++++++ src/js/containers/search/SearchContainer.jsx | 12 +-- .../helpers/downloadAnalytics-test.js | 88 +++++++++++++++++++ .../search/helpers/searchAnalytics-test.js | 2 + 7 files changed, 203 insertions(+), 7 deletions(-) create mode 100644 src/js/containers/bulkDownload/helpers/downloadAnalytics.js create mode 100644 tests/containers/bulkDownload/helpers/downloadAnalytics-test.js diff --git a/src/js/components/bulkDownload/archive/table/TableRow.jsx b/src/js/components/bulkDownload/archive/table/TableRow.jsx index 5bfaeb1841..2d8e628703 100644 --- a/src/js/components/bulkDownload/archive/table/TableRow.jsx +++ b/src/js/components/bulkDownload/archive/table/TableRow.jsx @@ -5,6 +5,7 @@ import React from 'react'; import PropTypes from 'prop-types'; +import Analytics from 'helpers/analytics/Analytics'; const propTypes = { columns: PropTypes.array.isRequired, @@ -13,6 +14,19 @@ const propTypes = { }; export default class TableRow extends React.PureComponent { + constructor(props) { + super(props); + + this.logArchiveDownload = this.logArchiveDownload.bind(this); + } + + logArchiveDownload() { + Analytics.event({ + category: 'Download Center - Archive Download', + action: this.props.file.fileName + }); + } + render() { let rowClass = 'row-even'; if (this.props.rowIndex % 2 === 0) { @@ -28,7 +42,8 @@ export default class TableRow extends React.PureComponent { + rel="noopener noreferrer" + onClick={this.logArchiveDownload}> {this.props.file.fileName} diff --git a/src/js/components/bulkDownload/sidebar/SidebarButton.jsx b/src/js/components/bulkDownload/sidebar/SidebarButton.jsx index 2fb9ccc910..971d5fd0ff 100644 --- a/src/js/components/bulkDownload/sidebar/SidebarButton.jsx +++ b/src/js/components/bulkDownload/sidebar/SidebarButton.jsx @@ -5,6 +5,7 @@ import React from 'react'; import PropTypes from 'prop-types'; +import Analytics from 'helpers/analytics/Analytics'; const propTypes = { type: PropTypes.string, @@ -21,12 +22,20 @@ export default class SidebarButton extends React.Component { super(props); this.clickedButton = this.clickedButton.bind(this); + this.logExternalLink = this.logExternalLink.bind(this); } clickedButton() { this.props.changeDataType(this.props.type); } + logExternalLink() { + Analytics.event({ + category: 'Download Center - Link', + action: this.props.url + }); + } + render() { let active = ''; if (this.props.active === this.props.type) { @@ -56,7 +65,8 @@ export default class SidebarButton extends React.Component { + rel="noopener noreferrer" + onClick={this.logExternalLink}> {this.props.label} ); diff --git a/src/js/containers/bulkDownload/BulkDownloadPageContainer.jsx b/src/js/containers/bulkDownload/BulkDownloadPageContainer.jsx index 149d942b7b..8e23aacf20 100644 --- a/src/js/containers/bulkDownload/BulkDownloadPageContainer.jsx +++ b/src/js/containers/bulkDownload/BulkDownloadPageContainer.jsx @@ -16,6 +16,8 @@ import * as BulkDownloadHelper from 'helpers/bulkDownloadHelper'; import { awardDownloadOptions } from 'dataMapping/bulkDownload/bulkDownloadOptions'; import BulkDownloadPage from 'components/bulkDownload/BulkDownloadPage'; +import { logAwardDownload } from './helpers/downloadAnalytics'; + require('pages/bulkDownload/bulkDownloadPage.scss'); const propTypes = { @@ -107,6 +109,8 @@ export class BulkDownloadPageContainer extends React.Component { }; this.requestDownload(params, 'awards'); + + logAwardDownload(this.props.bulkDownload.awards); } requestDownload(params, type) { diff --git a/src/js/containers/bulkDownload/helpers/downloadAnalytics.js b/src/js/containers/bulkDownload/helpers/downloadAnalytics.js new file mode 100644 index 0000000000..b39465f42f --- /dev/null +++ b/src/js/containers/bulkDownload/helpers/downloadAnalytics.js @@ -0,0 +1,75 @@ +/** + * downloadAnalytics.js + * Created by Kevin Li 2/8/18 + */ + +import Analytics from 'helpers/analytics/Analytics'; + +const categoryPrefix = 'Download Center - Download'; + +export const logDownloadType = (type) => { + Analytics.event({ + category: `${categoryPrefix} Type`, + action: type + }); +}; + +// convert an object whose truthy keys are all selected field values +export const convertKeyedField = (field) => ( + Object.keys(field).reduce((values, key) => { + if (field[key]) { + values.push(key); + } + return values; + }, []) +); + +export const convertDateRange = (dates) => { + if (dates.startDate && dates.endDate) { + return `${dates.startDate} - ${dates.endDate}`; + } + else if (dates.startDate) { + return `${dates.startDate} - present`; + } + else if (dates.endDate) { + return `... - ${dates.endDate}`; + } + return null; +}; + +export const logSingleDownloadField = (type, name, value) => { + Analytics.event({ + category: `${categoryPrefix} - ${type}`, + action: name, + label: value + }); +}; + +export const logDownloadFields = (type, filters) => { + const keyedFields = ['awardLevels', 'awardTypes']; + const keyedLabels = ['Award Level', 'Award Type']; + keyedFields.forEach((field, i) => { + const values = convertKeyedField(filters[field]); + values.forEach((value) => { + logSingleDownloadField(type, keyedLabels[i], value); + }); + }); + + // log the agency fields + logSingleDownloadField(type, 'Agency', filters.agency.name); + if (filters.subAgency.id) { + logSingleDownloadField(type, 'Sub Agency', filters.subAgency.name); + } + + // log the date fields + const dates = convertDateRange(filters.dateRange); + if (dates) { + logSingleDownloadField(type, 'Date Type', filters.dateType); + logSingleDownloadField(type, 'Date Range', dates); + } +}; + +export const logAwardDownload = (redux) => { + logDownloadType('award'); + logDownloadFields('award', redux); +}; diff --git a/src/js/containers/search/SearchContainer.jsx b/src/js/containers/search/SearchContainer.jsx index 599fb228ca..4d54ca113b 100644 --- a/src/js/containers/search/SearchContainer.jsx +++ b/src/js/containers/search/SearchContainer.jsx @@ -12,11 +12,6 @@ import { is } from 'immutable'; import moment from 'moment'; import Router from 'containers/router/Router'; -import { - convertFiltersToAnalyticEvents, - sendAnalyticEvents, - sendFieldCombinations -} from './helpers/searchAnalytics'; import { filterStoreVersion, requiredTypes, initialState } from 'redux/reducers/search/searchFiltersReducer'; @@ -34,6 +29,13 @@ import SearchAwardsOperation from 'models/search/SearchAwardsOperation'; import SearchPage from 'components/search/SearchPage'; +import { + convertFiltersToAnalyticEvents, + sendAnalyticEvents, + sendFieldCombinations +} from './helpers/searchAnalytics'; + + require('pages/search/searchPage.scss'); const propTypes = { diff --git a/tests/containers/bulkDownload/helpers/downloadAnalytics-test.js b/tests/containers/bulkDownload/helpers/downloadAnalytics-test.js new file mode 100644 index 0000000000..ab0ecd829d --- /dev/null +++ b/tests/containers/bulkDownload/helpers/downloadAnalytics-test.js @@ -0,0 +1,88 @@ +/** + * downloadAnalytics-test.js + * Created by Kevin Li 2/8/18 + */ + +import * as downloadAnalytics from 'containers/bulkDownload/helpers/downloadAnalytics'; + +jest.mock('helpers/analytics/Analytics', () => ({ + event: jest.fn() +})); + +describe('downloadAnalytics', () => { + describe('logDownloadType', () => { + it('should send an Analytic event indicating the current donwload type', () => { + const Analytics = require('helpers/analytics/Analytics'); + + downloadAnalytics.logDownloadType('award'); + + expect(Analytics.event).toHaveBeenCalledTimes(1); + expect(Analytics.event).toHaveBeenCalledWith({ + category: 'Download Center - Download Type', + action: 'award' + }); + + Analytics.event.mockClear(); + }); + }); + + describe('convertKeyedField', () => { + it('should evaluate each property in the given object for truthiness and return an array of only truthy keys', () => { + const inbound = { + first: true, + second: '', + third: 1, + fourth: false + }; + + expect(downloadAnalytics.convertKeyedField(inbound)).toEqual(['first', 'third']); + }); + }); + + describe('convertDateRange', () => { + it('should return both the start and end date when they are available', () => { + const dates = { + startDate: '1900-01-01', + endDate: '1900-01-02' + }; + expect(downloadAnalytics.convertDateRange(dates)).toEqual('1900-01-01 - 1900-01-02'); + }); + it('should return only the start date when there is no end date', () => { + const dates = { + startDate: '1900-01-01', + endDate: '' + }; + expect(downloadAnalytics.convertDateRange(dates)).toEqual('1900-01-01 - present'); + }); + it('should return only the end date when there is no start date', () => { + const dates = { + startDate: '', + endDate: '1900-01-02' + }; + expect(downloadAnalytics.convertDateRange(dates)).toEqual('... - 1900-01-02'); + }); + it('should return null when no dates are available', () => { + const dates = { + startDate: '', + endDate: '' + }; + expect(downloadAnalytics.convertDateRange(dates)).toBeFalsy(); + }); + }); + + describe('logSingleDownloadField', () => { + it('should log an Analytic event using the download type as the `category`, the field name as the `action`, and the value as the `label`', () => { + const Analytics = require('helpers/analytics/Analytics'); + + downloadAnalytics.logSingleDownloadField('award', 'name', 'value'); + + expect(Analytics.event).toHaveBeenCalledTimes(1); + expect(Analytics.event).toHaveBeenCalledWith({ + category: 'Download Center - Download - award', + action: 'name', + label: 'value' + }); + Analytics.event.mockClear(); + }); + }); +}); \ No newline at end of file diff --git a/tests/containers/search/helpers/searchAnalytics-test.js b/tests/containers/search/helpers/searchAnalytics-test.js index cb304677f6..668fc595ac 100644 --- a/tests/containers/search/helpers/searchAnalytics-test.js +++ b/tests/containers/search/helpers/searchAnalytics-test.js @@ -268,6 +268,8 @@ describe('searchAnalytics', () => { category: 'Advanced Search - Search Fields', action: 'action-z' }); + + Analytics.event.mockClear(); }); }); From f1c8cb2e48cdbe545563787ccceff3a10b2b9472 Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Fri, 9 Feb 2018 09:02:18 -0500 Subject: [PATCH 68/89] Added BaseFederalAccountLandingRow object --- .../table/AccountLandingTable.jsx | 5 +- .../accountLanding/table/TableRow.jsx | 18 +++---- .../AccountLandingContainer.jsx | 29 ++--------- .../accountLanding/accountsTableFields.js | 30 ++++++----- .../BaseFederalAccountLandingRow.js | 30 +++++++++++ .../AccountLandingContainer-test.jsx | 8 +-- .../accountLanding/mockFederalAccounts.js | 51 +++++++------------ 7 files changed, 87 insertions(+), 84 deletions(-) create mode 100644 src/js/models/accountLanding/BaseFederalAccountLandingRow.js diff --git a/src/js/components/accountLanding/table/AccountLandingTable.jsx b/src/js/components/accountLanding/table/AccountLandingTable.jsx index 1e14e297d9..29b482afe1 100644 --- a/src/js/components/accountLanding/table/AccountLandingTable.jsx +++ b/src/js/components/accountLanding/table/AccountLandingTable.jsx @@ -6,6 +6,7 @@ import React from 'react'; import PropTypes from 'prop-types'; +import AccountsTableFields from 'dataMapping/accountLanding/accountsTableFields'; import LegacyTableHeaderCell from 'components/account/awards/LegacyTableHeaderCell'; import TableRow from './TableRow'; @@ -22,7 +23,7 @@ export default class AccountLandingTable extends React.PureComponent { const rows = this.props.results.map((account, index) => ( @@ -32,7 +33,7 @@ export default class AccountLandingTable extends React.PureComponent {
    ); } - else if (column.columnName === 'budget_authority_amount') { + else if (column.columnName === 'budgetaryResources') { return ( ); @@ -53,10 +53,10 @@ export default class TableRow extends React.PureComponent { return ( diff --git a/src/js/containers/accountLanding/AccountLandingContainer.jsx b/src/js/containers/accountLanding/AccountLandingContainer.jsx index c13ec6777d..9438a2c135 100644 --- a/src/js/containers/accountLanding/AccountLandingContainer.jsx +++ b/src/js/containers/accountLanding/AccountLandingContainer.jsx @@ -8,11 +8,12 @@ import { isCancel } from 'axios'; import AccountsTableFields from 'dataMapping/accountLanding/accountsTableFields'; import * as AccountLandingHelper from 'helpers/accountLandingHelper'; -import * as MoneyFormatter from 'helpers/moneyFormatter'; import * as FiscalYearHelper from 'helpers/fiscalYearHelper'; import AccountLandingContent from 'components/accountLanding/AccountLandingContent'; +import BaseFederalAccountLandingRow from 'models/accountLanding/BaseFederalAccountLandingRow'; + require('pages/accountLanding/accountLandingPage.scss'); export default class AccountLandingContainer extends React.Component { @@ -88,7 +89,7 @@ export default class AccountLandingContainer extends React.Component { AccountsTableFields.order.forEach((col) => { let displayName = AccountsTableFields[col]; - if (col === 'budgetary_resources') { + if (col === 'budgetaryResources') { // Add default fiscal year to Budgetary Resources column header const fy = FiscalYearHelper.defaultFiscalYear(); displayName = `${fy} ${displayName}`; @@ -157,28 +158,8 @@ export default class AccountLandingContainer extends React.Component { const accounts = []; data.results.forEach((item) => { - // Format budgetary resources - const formattedCurrency = - MoneyFormatter.formatMoneyWithPrecision(item.budgetary_resources, 0); - - let formattedAgency = item.managing_agency; - if (item.managing_agency_acronym) { - formattedAgency = `${item.managing_agency} (${item.managing_agency_acronym})`; - } - - const account = { - account_id: item.account_id, - account_number: item.account_number, - managing_agency: formattedAgency, - account_name: item.account_name, - budgetary_resources: item.budgetary_resources, - display: { - account_number: `${item.account_number}`, - account_name: `${item.account_name}`, - managing_agency: formattedAgency, - budgetary_resources: formattedCurrency - } - }; + const account = Object.create(BaseFederalAccountLandingRow); + account.parse(item); accounts.push(account); }); diff --git a/src/js/dataMapping/accountLanding/accountsTableFields.js b/src/js/dataMapping/accountLanding/accountsTableFields.js index 870f4f4688..d8a3c54a49 100644 --- a/src/js/dataMapping/accountLanding/accountsTableFields.js +++ b/src/js/dataMapping/accountLanding/accountsTableFields.js @@ -1,20 +1,26 @@ const accountsTableFields = { defaultSortDirection: { - account_number: 'desc', - account_name: 'asc', - managing_agency: 'asc', - budgetary_resources: 'desc' + accountNumber: 'desc', + accountName: 'asc', + managingAgency: 'asc', + budgetaryResources: 'desc' + }, + modelMapping: { + accountNumber: 'account_number', + accountName: 'account_name', + managingAgency: 'managing_agency', + budgetaryResources: 'budgetary_resources' }, order: [ - 'account_number', - 'account_name', - 'managing_agency', - 'budgetary_resources' + 'accountNumber', + 'accountName', + 'managingAgency', + 'budgetaryResources' ], - account_number: 'Account Number', - account_name: 'Account Name', - managing_agency: 'Managing Agency', - budgetary_resources: 'Budgetary Resources' + accountNumber: 'Account Number', + accountName: 'Account Name', + managingAgency: 'Managing Agency', + budgetaryResources: 'Budgetary Resources' }; export default accountsTableFields; diff --git a/src/js/models/accountLanding/BaseFederalAccountLandingRow.js b/src/js/models/accountLanding/BaseFederalAccountLandingRow.js new file mode 100644 index 0000000000..beb550c3e8 --- /dev/null +++ b/src/js/models/accountLanding/BaseFederalAccountLandingRow.js @@ -0,0 +1,30 @@ +/** + * BaseFederalAccountLandingRow.js + * Created by Lizzie Salita on 2/8/18 + */ + +import { formatMoney } from 'helpers/moneyFormatter'; + +/* eslint-disable object-shorthand */ +const BaseFederalAccountLandingRow = { + parse: function (data) { + this.accountId = data.account_id || ''; + this.accountNumber = data.account_number || ''; + this._managingAgency = data.managing_agency || ''; + this._managingAgencyAcronym = data.managing_agency_acronym || ''; + this.accountName = data.account_name || ''; + this._budgetaryResources = data.budgetary_resources || 0; + }, + get managingAgency() { + if (!this._managingAgencyAcronym) { + return this._managingAgency; + } + return `${this._managingAgency} (${this._managingAgencyAcronym})`; + }, + get budgetaryResources() { + return formatMoney(this._budgetaryResources); + } +}; +/* eslint-enable object-shorthand */ + +export default BaseFederalAccountLandingRow; diff --git a/tests/containers/accountLanding/AccountLandingContainer-test.jsx b/tests/containers/accountLanding/AccountLandingContainer-test.jsx index d4f6280d9b..6a0221faba 100644 --- a/tests/containers/accountLanding/AccountLandingContainer-test.jsx +++ b/tests/containers/accountLanding/AccountLandingContainer-test.jsx @@ -40,22 +40,22 @@ describe('AccountLandingContainer', () => { const fy = FiscalYearHelper.defaultFiscalYear(); const expectedState = [ { - columnName: "account_number", + columnName: "accountNumber", defaultDirection: "desc", displayName: "Account Number" }, { - columnName: "account_name", + columnName: "accountName", defaultDirection: "asc", displayName: "Account Name" }, { - columnName: "managing_agency", + columnName: "managingAgency", defaultDirection: "asc", displayName: "Managing Agency" }, { - columnName: "budgetary_resources", + columnName: "budgetaryResources", defaultDirection: "desc", displayName: `${fy} Budgetary Resources` } diff --git a/tests/containers/accountLanding/mockFederalAccounts.js b/tests/containers/accountLanding/mockFederalAccounts.js index 593f41f31a..c0555846e6 100644 --- a/tests/containers/accountLanding/mockFederalAccounts.js +++ b/tests/containers/accountLanding/mockFederalAccounts.js @@ -35,40 +35,25 @@ export const mockData = { export const mockParsed = [ { - "account_id": 1, - "account_name": "Mock Account", - "account_number": "123-4567", - "budgetary_resources": 5000000, - "display": { - "account_name": "Mock Account", - "account_number": "123-4567", - "budgetary_resources": "$5,000,000", - "managing_agency": "Mock Agency (XYZ)" - }, - "managing_agency": "Mock Agency (XYZ)" + _budgetaryResources: 5000000, + _managingAgency: "Mock Agency", + _managingAgencyAcronym: "XYZ", + accountId: 1, + accountName: "Mock Account", + accountNumber: "123-4567" }, { - "account_id": 2, - "account_name": "Mock Account 2", - "account_number": "098-7654", - "budgetary_resources": 6500000, - "display": { - "account_name": "Mock Account 2", - "account_number": "098-7654", - "budgetary_resources": "$6,500,000", - "managing_agency": "Mock Agency 2 (ABC)" - }, - "managing_agency": "Mock Agency 2 (ABC)" + _budgetaryResources: 6500000, + _managingAgency: "Mock Agency 2", + _managingAgencyAcronym: "ABC", + accountId: 2, + accountName: "Mock Account 2", + accountNumber: "098-7654" }, { - "account_id": 3, - "account_name": "Test Account", - "account_number": "234-5678", - "budgetary_resources": 4500000, - "display": { - "account_name": "Test Account", - "account_number": "234-5678", - "budgetary_resources": "$4,500,000", - "managing_agency": "Mock Agency 3 (DEF)" - }, - "managing_agency": "Mock Agency 3 (DEF)" + _budgetaryResources: 4500000, + _managingAgency: "Mock Agency 3", + _managingAgencyAcronym: "DEF", + accountId: 3, + accountName: "Test Account", + accountNumber: "234-5678" } ]; \ No newline at end of file From 25c0844922f9c513db73ca58d7b4ba4e8d9707a8 Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Fri, 9 Feb 2018 10:08:51 -0500 Subject: [PATCH 69/89] BEM and test fixes --- .../pages/accountLanding/table/table.scss | 12 ++--- .../table/cells/HighlightedCell.jsx | 6 ++- .../AccountLandingContainer.jsx | 16 +++--- .../AccountLandingContainer-test.jsx | 53 +++++++++---------- 4 files changed, 46 insertions(+), 41 deletions(-) diff --git a/src/_scss/pages/accountLanding/table/table.scss b/src/_scss/pages/accountLanding/table/table.scss index 6e2321f6e3..89410ebeb7 100644 --- a/src/_scss/pages/accountLanding/table/table.scss +++ b/src/_scss/pages/accountLanding/table/table.scss @@ -1,8 +1,6 @@ .results-table { display: block; overflow: scroll; - margin-top: rem(15); - .results-table__head { .results-table__row { border-bottom: solid 1px $color-gray-lighter; @@ -39,15 +37,17 @@ &:last-child { width: 20%; .results-table-cell__content, .cell-content { - float: right; + text-align: right; } } } .results-table-cell { .results-table-cell__content { - .results-table-cell__highlight { - text-decoration: underline; - font-weight: 600; + .results-table-cell__matched { + &.results-table-cell__matched_highlight { + text-decoration: underline; + font-weight: 600; + } } } } diff --git a/src/js/components/accountLanding/table/cells/HighlightedCell.jsx b/src/js/components/accountLanding/table/cells/HighlightedCell.jsx index 7981164efb..a1fc6ebb62 100644 --- a/src/js/components/accountLanding/table/cells/HighlightedCell.jsx +++ b/src/js/components/accountLanding/table/cells/HighlightedCell.jsx @@ -20,7 +20,11 @@ export default class HighlightedCell extends React.Component { // highlight the matched string if applicable if (this.props.searchString) { data = reactStringReplace(this.props.data, this.props.searchString, (match, i) => ( - {match} + + {match} + )); } return ( diff --git a/src/js/containers/accountLanding/AccountLandingContainer.jsx b/src/js/containers/accountLanding/AccountLandingContainer.jsx index 9438a2c135..3ec1b68550 100644 --- a/src/js/containers/accountLanding/AccountLandingContainer.jsx +++ b/src/js/containers/accountLanding/AccountLandingContainer.jsx @@ -5,6 +5,7 @@ import React from 'react'; import { isCancel } from 'axios'; +import { inRange } from 'lodash'; import AccountsTableFields from 'dataMapping/accountLanding/accountsTableFields'; import * as AccountLandingHelper from 'helpers/accountLandingHelper'; @@ -52,12 +53,15 @@ export default class AccountLandingContainer extends React.Component { } onChangePage(pageNumber) { - // Change page number in the state and make a new request - this.setState({ - pageNumber - }, () => { - this.fetchAccounts(); - }); + const totalPages = Math.ceil(this.state.totalItems / this.state.pageSize); + if (inRange(pageNumber, 1, totalPages + 1)) { + // Change page number in the state and make a new request + this.setState({ + pageNumber + }, () => { + this.fetchAccounts(); + }); + } } setAccountSearchString(searchString) { diff --git a/tests/containers/accountLanding/AccountLandingContainer-test.jsx b/tests/containers/accountLanding/AccountLandingContainer-test.jsx index 6a0221faba..299b87cc03 100644 --- a/tests/containers/accountLanding/AccountLandingContainer-test.jsx +++ b/tests/containers/accountLanding/AccountLandingContainer-test.jsx @@ -4,7 +4,7 @@ */ import React from 'react'; -import { mount } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import * as FiscalYearHelper from 'helpers/fiscalYearHelper'; import AccountLandingContainer from 'containers/accountLanding/AccountLandingContainer'; @@ -30,11 +30,10 @@ describe('AccountLandingContainer', () => { }); describe('showColumns', () => { - it('should build the table', async () => { - // mount the container - const container = mount(); + it('should build the table', () => { + const container = shallow(); - await container.instance().accountsRequest.promise; + container.instance().showColumns(); // validate the state contains the correctly parsed values const fy = FiscalYearHelper.defaultFiscalYear(); @@ -67,7 +66,7 @@ describe('AccountLandingContainer', () => { describe('parseAccounts', () => { it('should parse the API response and update the container state', () => { - const container = mount(); + const container = shallow(); container.instance().parseAccounts(mockData.data); expect(container.state().results).toEqual(mockParsed); @@ -75,43 +74,41 @@ describe('AccountLandingContainer', () => { }); describe('updateSort', () => { - it('should update the state and make an API request', async () => { - // mount the container - const container = mount(); - const parseAccounts = jest.fn(); - container.instance().parseAccounts = parseAccounts; - - await container.instance().accountsRequest.promise; - expect(parseAccounts).toHaveBeenCalledTimes(1); + it('should update the container state', () => { + const container = shallow(); // change the sort order container.instance().updateSort('managing_agency', 'asc'); - await container.instance().accountsRequest.promise; expect(container.state().order).toEqual({ field: 'managing_agency', direction: 'asc' }); - expect(parseAccounts).toHaveBeenCalledTimes(2); - }) + }); }); describe('onChangePage', () => { - it('should update the state and make an API request', async () => { - // mount the container - const container = mount(); - const parseAccounts = jest.fn(); - container.instance().parseAccounts = parseAccounts; - - await container.instance().accountsRequest.promise; - expect(parseAccounts).toHaveBeenCalledTimes(1); - + it('should update the page number when in range', () => { + const container = shallow(); + // Give the container enough items for two pages + container.setState({ + totalItems: 75 + }); // change the page number container.instance().onChangePage(2); - await container.instance().accountsRequest.promise; expect(container.state().pageNumber).toEqual(2); - expect(parseAccounts).toHaveBeenCalledTimes(2); + }); + it('should not update the page number when out of range', () => { + const container = shallow(); + // Give the container enough items for two pages + container.setState({ + totalItems: 75 + }); + // try to change the page number + container.instance().onChangePage(3); + + expect(container.state().pageNumber).toEqual(1); }); }); }); From 746e53a6fbc96b5e9dfa0bce42522f9e1870e6b1 Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Fri, 9 Feb 2018 10:11:58 -0500 Subject: [PATCH 70/89] Only shows the NoDownloadHover component if the user has applied filters and there are more than 500,000 results --- src/js/components/search/SearchPage.jsx | 3 ++- src/js/components/search/header/DownloadButton.jsx | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/js/components/search/SearchPage.jsx b/src/js/components/search/SearchPage.jsx index 143d24dc24..aeb672c9da 100644 --- a/src/js/components/search/SearchPage.jsx +++ b/src/js/components/search/SearchPage.jsx @@ -146,7 +146,8 @@ export default class SearchPage extends React.Component {
    + onClick={this.showModal} + filterCount={this.state.filterCount} />
    diff --git a/src/js/components/search/header/DownloadButton.jsx b/src/js/components/search/header/DownloadButton.jsx index e5b53184e7..5999b9dd42 100644 --- a/src/js/components/search/header/DownloadButton.jsx +++ b/src/js/components/search/header/DownloadButton.jsx @@ -10,7 +10,8 @@ import NoDownloadHover from './NoDownloadHover'; const propTypes = { onClick: PropTypes.func, - downloadAvailable: PropTypes.bool + downloadAvailable: PropTypes.bool, + filterCount: PropTypes.number }; export default class DownloadButton extends React.Component { @@ -49,7 +50,7 @@ export default class DownloadButton extends React.Component { render() { let hover = null; - if (this.state.showHover && !this.props.downloadAvailable) { + if (this.state.showHover && !this.props.downloadAvailable && this.props.filterCount > 0) { hover = (); } From e5492174719ae0c4332f5f77a00f7470ba21272c Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Fri, 9 Feb 2018 10:24:58 -0500 Subject: [PATCH 71/89] Adding a default prop to ensure this doesn't break the Keyword Search page --- src/js/components/search/header/DownloadButton.jsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/js/components/search/header/DownloadButton.jsx b/src/js/components/search/header/DownloadButton.jsx index 5999b9dd42..b9339b723e 100644 --- a/src/js/components/search/header/DownloadButton.jsx +++ b/src/js/components/search/header/DownloadButton.jsx @@ -14,6 +14,10 @@ const propTypes = { filterCount: PropTypes.number }; +const defaultProps = { + filterCount: 0 +}; + export default class DownloadButton extends React.Component { constructor(props) { super(props); @@ -84,3 +88,4 @@ export default class DownloadButton extends React.Component { } DownloadButton.propTypes = propTypes; +DownloadButton.defaultProps = defaultProps; From ece18c14c7491909df40914453f93f0bd8a5653d Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Fri, 9 Feb 2018 15:07:06 -0500 Subject: [PATCH 72/89] Updated link cell highlight span with BEM classes --- .../accountLanding/table/cells/AccountLinkCell.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/js/components/accountLanding/table/cells/AccountLinkCell.jsx b/src/js/components/accountLanding/table/cells/AccountLinkCell.jsx index dd93f7dca5..226a9b688f 100644 --- a/src/js/components/accountLanding/table/cells/AccountLinkCell.jsx +++ b/src/js/components/accountLanding/table/cells/AccountLinkCell.jsx @@ -21,7 +21,11 @@ export default class AccountLinkCell extends React.Component { // highlight the matched string if applicable if (this.props.accountSearchString) { name = reactStringReplace(this.props.name, this.props.accountSearchString, (match, i) => ( - {match} + + {match} + )); } From 21487d2698568429163274035ebe322472184aba Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Mon, 12 Feb 2018 10:48:03 -0500 Subject: [PATCH 73/89] Use pipe delimiters --- src/js/containers/search/helpers/searchAnalytics.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/containers/search/helpers/searchAnalytics.js b/src/js/containers/search/helpers/searchAnalytics.js index db6f1d4c5b..4534298443 100644 --- a/src/js/containers/search/helpers/searchAnalytics.js +++ b/src/js/containers/search/helpers/searchAnalytics.js @@ -219,7 +219,7 @@ export const sendFieldCombinations = (events) => { Analytics.event({ category: 'Advanced Search - Search Fields', - action: fields.sort().join('-') + action: fields.sort().join('|') }); }; @@ -231,5 +231,5 @@ export const uniqueFilterFields = (redux) => { } return parsed; }, [])); - return fields.sort().join('-'); + return fields.sort().join('|'); }; From fc0c67c78b70a0d709728cea0254b20fda6c50b6 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Mon, 12 Feb 2018 11:15:59 -0500 Subject: [PATCH 74/89] Award type grouping and updated tests --- .../search/helpers/searchAnalytics.js | 34 +++++++++++++++++-- src/js/dataMapping/search/awardType.js | 9 +++++ .../search/helpers/searchAnalytics-test.js | 26 +++++++++++--- 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/js/containers/search/helpers/searchAnalytics.js b/src/js/containers/search/helpers/searchAnalytics.js index 4534298443..6191c39dd1 100644 --- a/src/js/containers/search/helpers/searchAnalytics.js +++ b/src/js/containers/search/helpers/searchAnalytics.js @@ -5,7 +5,11 @@ import { Set } from 'immutable'; import { uniq } from 'lodash'; -import { awardTypeCodes } from 'dataMapping/search/awardType'; +import { + awardTypeCodes, + awardTypeGroups, + awardTypeGroupLabels +} from 'dataMapping/search/awardType'; import { recipientTypes, groupLabels } from 'dataMapping/search/recipientType'; import { pricingTypeDefinitions, @@ -99,13 +103,39 @@ export const convertLocation = (locations, type) => ( ) ); +export const combineAwardTypeGroups = (filters) => { + // look in each award type group and check if the full award type group is satisfied + // if so, this indicates that the user clicked "All [type]" + let groupedFilters = []; + const fullTypes = Object.keys(awardTypeGroups).reduce((groups, key) => { + const groupValues = awardTypeGroups[key]; + const fullMembership = groupValues.every((value) => filters.includes(value)); + if (fullMembership) { + groups.push(`All ${awardTypeGroupLabels[key]}`); + groupedFilters = groupedFilters.concat(groupValues); + } + return groups; + }, []); + + // add the remaining filters + const remainingFilters = filters.reduce((parsed, value) => { + if (groupedFilters.indexOf(value) === -1) { + // this filter isn't already included in a group + parsed.push(value); + } + return parsed; + }, []); + + return fullTypes.concat(remainingFilters); +}; + export const convertFilter = (type, value) => { switch (type) { case 'timePeriod': return convertTimePeriod(value); case 'awardType': return convertReducibleValue( - value, + combineAwardTypeGroups(value), 'Award Type', (item) => awardTypeCodes[item] || item ); diff --git a/src/js/dataMapping/search/awardType.js b/src/js/dataMapping/search/awardType.js index d3e70d79a7..2ab71ad302 100644 --- a/src/js/dataMapping/search/awardType.js +++ b/src/js/dataMapping/search/awardType.js @@ -30,3 +30,12 @@ export const awardTypeGroups = { loans: ['07', '08'], other: ['09', '11'] }; + + +export const awardTypeGroupLabels = { + contracts: 'Contracts', + grants: 'Grants', + direct_payments: 'Direct Payments', + loans: 'Loans', + other: 'Other' +}; diff --git a/tests/containers/search/helpers/searchAnalytics-test.js b/tests/containers/search/helpers/searchAnalytics-test.js index 668fc595ac..fa3c5a0671 100644 --- a/tests/containers/search/helpers/searchAnalytics-test.js +++ b/tests/containers/search/helpers/searchAnalytics-test.js @@ -185,6 +185,24 @@ describe('searchAnalytics', () => { }); }); + describe('combineAwardTypeGroups', () => { + it('should combine award types into a single `All` item when an entire group is selected', () => { + const data = new Set(['A', 'B', 'C', 'D']) + const combined = searchAnalytics.combineAwardTypeGroups(data); + expect(combined).toEqual(['All Contracts']); + }); + it('should not combine award types into a single `All` item when some members of the group are not selected', () => { + const data = new Set(['A', 'B', 'C']) + const combined = searchAnalytics.combineAwardTypeGroups(data); + expect(combined).toEqual(['A', 'B', 'C']); + }); + it('when some full groups are selected and some incomplete groups are selected, the incomplete items should be reported individually', () => { + const data = new Set(['A', 'B', 'C', 'D', '01']) + const combined = searchAnalytics.combineAwardTypeGroups(data); + expect(combined).toEqual(['All Contracts', '01']); + }); + }); + describe('unifyDateFields', () => { it('should set the `timePeriod` field to the `timePeriodFY` redux filter when fiscal years are selected', () => { const filters = { @@ -248,7 +266,7 @@ describe('searchAnalytics', () => { }); describe('sendFieldCombinations', () => { - it('should send an Analytic event with a non-repeating `-` separated string of filter names', () => { + it('should send an Analytic event with a non-repeating `|` separated string of filter names', () => { const events = [{ action: 'action', label: 'label' @@ -266,7 +284,7 @@ describe('searchAnalytics', () => { expect(Analytics.event).toHaveBeenCalledTimes(1); expect(Analytics.event).toHaveBeenCalledWith({ category: 'Advanced Search - Search Fields', - action: 'action-z' + action: 'action|z' }); Analytics.event.mockClear(); @@ -274,7 +292,7 @@ describe('searchAnalytics', () => { }); describe('uniqueFilterFields', () => { - it('should return a string of non-repeating `-` separated string of filter names', () => { + it('should return a string of non-repeating `|` separated string of filter names', () => { const filters = { timePeriodType: 'fy', timePeriodFY: new Set(['1900']), @@ -284,7 +302,7 @@ describe('searchAnalytics', () => { }; const fields = searchAnalytics.uniqueFilterFields(filters); - expect(fields).toEqual('Award ID-Time Period - Fiscal Year'); + expect(fields).toEqual('Award ID|Time Period - Fiscal Year'); }); }); }); \ No newline at end of file From 8b81f105653167d41a55b1f045eb36ceceb3abcf Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Mon, 12 Feb 2018 17:00:59 -0500 Subject: [PATCH 75/89] Added keyword param to the keyword search url --- src/js/components/keyword/KeywordPage.jsx | 1 + .../components/keyword/KeywordSearchBar.jsx | 21 +++++++++ .../containers/keyword/KeywordContainer.jsx | 45 +++++++++++++++++++ .../keyword/table/ResultsTableContainer.jsx | 8 ++++ src/js/containers/router/RouterRoutes.jsx | 9 ++++ src/js/helpers/keywordHelper.js | 11 +++++ tests/containers/keyword/mockResults.js | 4 +- 7 files changed, 98 insertions(+), 1 deletion(-) diff --git a/src/js/components/keyword/KeywordPage.jsx b/src/js/components/keyword/KeywordPage.jsx index 1bd18d39a7..ef15aab8fd 100644 --- a/src/js/components/keyword/KeywordPage.jsx +++ b/src/js/components/keyword/KeywordPage.jsx @@ -171,6 +171,7 @@ export default class KeywordPage extends React.Component {
    Use the Keyword Search to get a broad picture of award data on a given theme. diff --git a/src/js/components/keyword/KeywordSearchBar.jsx b/src/js/components/keyword/KeywordSearchBar.jsx index b2b3992c88..9372d0641a 100644 --- a/src/js/components/keyword/KeywordSearchBar.jsx +++ b/src/js/components/keyword/KeywordSearchBar.jsx @@ -9,6 +9,7 @@ import PropTypes from 'prop-types'; import { Search } from 'components/sharedComponents/icons/Icons'; const propTypes = { + keyword: PropTypes.string, updateKeyword: PropTypes.func }; @@ -24,6 +25,26 @@ export default class KeywordSearchBar extends React.Component { this.searchKeyword = this.searchKeyword.bind(this); } + componentDidMount() { + if (this.props.keyword) { + // Show the keyword derived from the url + this.updateSearchString(this.props.keyword); + } + } + + componentDidUpdate(prevProps) { + // Show the keyword derived from a new url + if (prevProps.keyword !== this.props.keyword) { + this.updateSearchString(this.props.keyword); + } + } + + updateSearchString(searchString) { + this.setState({ + searchString + }); + } + searchKeyword(e) { e.preventDefault(); if (this.state.searchString.length > 2) { diff --git a/src/js/containers/keyword/KeywordContainer.jsx b/src/js/containers/keyword/KeywordContainer.jsx index a77bc3c6ba..9525cd4155 100644 --- a/src/js/containers/keyword/KeywordContainer.jsx +++ b/src/js/containers/keyword/KeywordContainer.jsx @@ -9,6 +9,8 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { isCancel } from 'axios'; +import Router from 'containers/router/Router'; + import * as bulkDownloadActions from 'redux/actions/bulkDownload/bulkDownloadActions'; import * as BulkDownloadHelper from 'helpers/bulkDownloadHelper'; import * as KeywordHelper from 'helpers/keywordHelper'; @@ -18,6 +20,7 @@ import KeywordPage from 'components/keyword/KeywordPage'; require('pages/keyword/keywordPage.scss'); const propTypes = { + params: PropTypes.object, bulkDownload: PropTypes.object, setDownloadPending: PropTypes.func, setDownloadExpectedFile: PropTypes.func, @@ -43,6 +46,43 @@ export class KeywordContainer extends React.Component { this.startDownload = this.startDownload.bind(this); } + componentWillMount() { + this.handleInitialUrl(this.props.params.keyword); + } + + componentWillReceiveProps(nextProps) { + const nextKeywordUrl = nextProps.params.keyword; + if (nextKeywordUrl) { + // Convert the url to a keyword + const nextKeyword = KeywordHelper.stringFromSlug(nextKeywordUrl); + // Update the keyword only if it has changed and is more than two characters + if (nextKeyword !== this.state.keyword && nextKeyword.length > 2) { + this.setState({ + keyword: nextKeyword + }); + } + } + else { + // The keyword param was removed from the url, reset the keyword + this.setState({ + keyword: '' + }); + } + } + + handleInitialUrl(urlKeyword) { + if (urlKeyword) { + // Convert the url to a keyword + const keyword = KeywordHelper.stringFromSlug(urlKeyword); + // Update the keyword only if it has more than two characters + if (keyword.length > 2) { + this.setState({ + keyword + }); + } + } + } + startDownload() { const params = { award_levels: ['prime_awards'], @@ -139,8 +179,13 @@ export class KeywordContainer extends React.Component { } updateKeyword(keyword) { + // Convert the keyword to a url slug + const slug = KeywordHelper.slugFromString(keyword); this.setState({ keyword + }, () => { + // update the url + Router.history.replace(`/keyword_search/${slug}`); }); } diff --git a/src/js/containers/keyword/table/ResultsTableContainer.jsx b/src/js/containers/keyword/table/ResultsTableContainer.jsx index 62467d7d86..e4881f952c 100644 --- a/src/js/containers/keyword/table/ResultsTableContainer.jsx +++ b/src/js/containers/keyword/table/ResultsTableContainer.jsx @@ -70,6 +70,14 @@ export default class ResultsTableContainer extends React.Component { this.updateSort = this.updateSort.bind(this); } + componentDidMount() { + // Perform a search for a keyword derived from the url + if (this.props.keyword) { + this.loadColumns(); + this.pickDefaultTab(); + } + } + componentDidUpdate(prevProps) { if (prevProps.keyword !== this.props.keyword) { // filters changed, update the search object diff --git a/src/js/containers/router/RouterRoutes.jsx b/src/js/containers/router/RouterRoutes.jsx index 9b9a1404c4..8adf0fa8df 100644 --- a/src/js/containers/router/RouterRoutes.jsx +++ b/src/js/containers/router/RouterRoutes.jsx @@ -145,6 +145,15 @@ const routes = { cb(require('containers/keyword/KeywordContainer').default); }); } + }, + { + path: '/keyword_search/:keyword', + parent: '/keyword_search', + component: (cb) => { + require.ensure([], (require) => { + cb(require('containers/keyword/KeywordContainer').default); + }); + } } ], notFound: { diff --git a/src/js/helpers/keywordHelper.js b/src/js/helpers/keywordHelper.js index dd289c9b3d..f6a38ab75c 100644 --- a/src/js/helpers/keywordHelper.js +++ b/src/js/helpers/keywordHelper.js @@ -54,3 +54,14 @@ export const performTabCountSearch = (params) => { } }; }; + +export const slugFromString = (string) => + string.toString().toLowerCase().trim() + .replace(/\s+/g, "-") // Replace spaces with - + .replace(/[^\w-]+/g, "") // Remove non word or hyphen characters + .replace(/--+/g, "-") // Replace multiple - with single - + .replace(/^-+/, "") // Don't use hyphens at the beginning or end of the slug + .replace(/-+$/, ""); + +export const stringFromSlug = (slug) => + slug.replace(/[-._~]+|([a-z])([A-Z])/g, ' '); diff --git a/tests/containers/keyword/mockResults.js b/tests/containers/keyword/mockResults.js index 9ab977d2d1..ecb0317e00 100644 --- a/tests/containers/keyword/mockResults.js +++ b/tests/containers/keyword/mockResults.js @@ -46,6 +46,9 @@ export const mockTableProps = { }; export const mockRedux = { + params: { + keyword: '' + }, bulkDownload: { download: { expectedFile: '', @@ -56,7 +59,6 @@ export const mockRedux = { } }; - export const mockActions = { setDownloadExpectedFile: jest.fn(), setDownloadPending: jest.fn(), From a0b8ee8ad769c4e4b935991cb74480c7b85b34c2 Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Tue, 13 Feb 2018 12:25:48 -0500 Subject: [PATCH 76/89] Created modified Contract Summary page for IDV Awards --- src/js/components/award/AwardInfo.jsx | 9 + src/js/components/award/SummaryBar.jsx | 8 +- src/js/components/award/contract/AwardIDV.jsx | 34 +++ .../components/award/contract/IDVDetails.jsx | 229 ++++++++++++++++++ .../award/details/DetailsSection.jsx | 5 +- src/js/dataMapping/contracts/idvAwardTypes.js | 15 ++ 6 files changed, 297 insertions(+), 3 deletions(-) create mode 100644 src/js/components/award/contract/AwardIDV.jsx create mode 100644 src/js/components/award/contract/IDVDetails.jsx create mode 100644 src/js/dataMapping/contracts/idvAwardTypes.js diff --git a/src/js/components/award/AwardInfo.jsx b/src/js/components/award/AwardInfo.jsx index a42b6f9aea..476e49d900 100644 --- a/src/js/components/award/AwardInfo.jsx +++ b/src/js/components/award/AwardInfo.jsx @@ -11,6 +11,7 @@ import { scrollToY } from 'helpers/scrollToHelper'; import SummaryBar from './SummaryBar'; import AwardInfoBar from './AwardInfoBar'; import AwardContract from './contract/AwardContract'; +import AwardIDV from './contract/AwardIDV'; import AwardFinancialAssistance from './financialAssistance/AwardFinancialAssistance'; import DetailsSection from './details/DetailsSection'; @@ -59,6 +60,14 @@ export default class AwardInfo extends React.Component { seeAdditional={this.seeAdditional} /> ); } + else if (type === 'unknown') { + amountsDetailsSection = ( + + ); + } else { amountsDetailsSection = ( + + +
    + ); + } +} + +AwardIDV.propTypes = propTypes; diff --git a/src/js/components/award/contract/IDVDetails.jsx b/src/js/components/award/contract/IDVDetails.jsx new file mode 100644 index 0000000000..e85af644c8 --- /dev/null +++ b/src/js/components/award/contract/IDVDetails.jsx @@ -0,0 +1,229 @@ +/** + * IDVDetails.jsx + * Created by michaelbray on 2/13/18. + **/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import { idvAwardTypes } from 'dataMapping/contracts/idvAwardTypes'; +import DetailRow from '../DetailRow'; + +const propTypes = { + selectedAward: PropTypes.object, + seeAdditional: PropTypes.func, + maxChars: PropTypes.number +}; + +const isEmpty = (field, ignoreDefault) => { + if (!field) { + return true; + } + if (field === '') { + return true; + } + if (!ignoreDefault && field === ignoreDefault) { + return true; + } + return false; +}; + +export default class IDVDetails extends React.Component { + constructor(props) { + super(props); + + this.state = { + desc: "", + date: "", + place: "", + typeDesc: "", + price: "", + overflow: false + }; + } + + componentDidMount() { + this.prepareValues(this.props.selectedAward); + } + + componentWillReceiveProps(nextProps) { + if (!Object.is(nextProps.selectedAward, this.props.selectedAward)) { + this.prepareValues(nextProps.selectedAward); + } + } + + parsePlaceOfPerformance(award) { + // Location + let popPlace = ''; + + let cityState = null; + const city = award.pop_city; + const stateProvince = award.pop_state_province; + + if (!isEmpty(city) && !isEmpty(stateProvince)) { + cityState = `${city}, ${stateProvince}`; + } + else if (!isEmpty(city)) { + cityState = city; + } + else if (!isEmpty(stateProvince)) { + cityState = stateProvince; + } + if (award.pop_country_code === 'USA') { + if (!isEmpty(cityState)) { + popPlace = cityState; + } + if (!isEmpty(award.pop_zip)) { + if (popPlace !== '') { + popPlace += ' '; + } + popPlace += award.pop_zip; + } + + if (!isEmpty(award.pop_state_code) && !isEmpty(award.pop_congressional_district)) { + if (popPlace !== '') { + popPlace += '\n'; + } + popPlace += + `Congressional District: ${award.pop_state_code}-${award.pop_congressional_district}`; + } + } + else if (award.pop_country_code !== 'USA') { + popPlace = `${award.pop_country}`; + } + + if (popPlace === '') { + popPlace = 'Not available'; + } + + return popPlace; + } + + prepareValues(award) { + let yearRangeTotal = ""; + let monthRangeTotal = ""; + let description = null; + + // Date Range + const formattedStartDate = award.period_of_performance_start_date; + // Convert this datetime into a moment date + const formattedEndDate = moment(award.latest_transaction.contract_data.ordering_period_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 (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 if (yearRangeTotal) { + timeRange = `(${yearRangeTotal})`; + } + + popDate = `${formattedStartDate} - ${formattedEndDate.format('M/D/YYYY')} ${timeRange}`; + } + + if (award.description) { + description = award.description; + } + else { + description = "Not Available"; + } + + // Award Type + let awardType = "Not Available"; + if (award.latest_transaction.contract_data.idv_type) { + awardType = idvAwardTypes[award.latest_transaction.contract_data.idv_type]; + } + + // Pricing + let pricing = "Not Available"; + if (award.type_of_contract_pricing_description) { + pricing = award.type_of_contract_pricing_description; + } + + // char count + let seeMore = false; + if (award.description.length > this.props.maxChars) { + seeMore = true; + } + + this.setState({ + desc: description, + overflow: seeMore, + date: popDate, + place: this.parsePlaceOfPerformance(award), + typeDesc: awardType, + price: pricing + }); + } + + render() { + return ( +
    +
    +

    Contract Details

    +
    { + this.sectionHr = hr; + }} /> +
    - -
    + - - - - {headers} - - - - {rows} - -
    - + + + + {headers} + + + + {rows} + +
    ); } } diff --git a/src/js/components/accountLanding/table/TableRow.jsx b/src/js/components/accountLanding/table/TableRow.jsx index 4514599864..fc1ff54bfa 100644 --- a/src/js/components/accountLanding/table/TableRow.jsx +++ b/src/js/components/accountLanding/table/TableRow.jsx @@ -7,7 +7,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import GenericCell from 'components/agencyLanding/table/cells/GenericCell'; import AccountLinkCell from './cells/AccountLinkCell'; -import GenericAccountCell from './cells/GenericAccountCell'; +import HighlightedCell from './cells/HighlightedCell'; const propTypes = { columns: PropTypes.array.isRequired, @@ -18,16 +18,16 @@ const propTypes = { export default class TableRow extends React.PureComponent { render() { - let rowClass = 'row-even'; + let rowClass = ''; if (this.props.rowIndex % 2 === 0) { - rowClass = 'row-odd'; + rowClass = 'results-table__data_even'; } const cells = this.props.columns.map((column) => { if (column.columnName === 'account_name') { // show the account link cell return (
    - + searchString={this.props.accountSearchString} />
    { - if (column.columnName === 'account_name') { + if (column.columnName === 'accountName') { // show the account link cell return ( + key={`${column.columnName}-${this.props.account.accountId}`}> + key={`${column.columnName}-${this.props.account.accountId}`}> + key={`${column.columnName}-${this.props.account.accountId}`}>
    + + + + + + + +
    +
    + + + ); + } +} + +IDVDetails.propTypes = propTypes; diff --git a/src/js/components/award/details/DetailsSection.jsx b/src/js/components/award/details/DetailsSection.jsx index 18bdc0cec7..71c6da246d 100644 --- a/src/js/components/award/details/DetailsSection.jsx +++ b/src/js/components/award/details/DetailsSection.jsx @@ -94,7 +94,7 @@ export default class DetailsSection extends React.Component { tableWidth={this.state.tableWidth} />); case 'additional': - if (type === 'contract') { + if (type === 'contract' || type === 'unknown') { return (); } return (); @@ -109,7 +109,8 @@ export default class DetailsSection extends React.Component { const tabs = concat([], commonTabs); - if (this.props.award.selectedAward.internal_general_type === 'contract') { + if (this.props.award.selectedAward.internal_general_type === 'contract' + || this.props.award.selectedAward.internal_general_type === 'unknown') { tabs.push({ label: 'Additional Details', internal: 'additional', diff --git a/src/js/dataMapping/contracts/idvAwardTypes.js b/src/js/dataMapping/contracts/idvAwardTypes.js new file mode 100644 index 0000000000..31ca975083 --- /dev/null +++ b/src/js/dataMapping/contracts/idvAwardTypes.js @@ -0,0 +1,15 @@ +/** + * idvAwardTypes.js + * Created by michaelbray on 2/13/18. + */ + +/* eslint-disable import/prefer-default-export */ +// We only have one export but want to maintain consistency with other functions +export const idvAwardTypes = { + A: 'Government-Wide Acquisition Contract (GWAC)', + B: 'Indefinite Delivery Contract (IDC)', + C: 'Federal Supply Schedule (FSS)', + D: 'Basic Ordering Agreement (BOA)', + E: 'Blanket Purchase Agreement (BPA)' +}; +/* eslint-enable import/prefer-default-export */ From c05eeab9def548b3384be3385d39fc6c22f36113 Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Tue, 13 Feb 2018 12:55:18 -0500 Subject: [PATCH 77/89] Double check for IDV type and date existences --- src/js/components/award/AwardInfo.jsx | 2 ++ src/js/components/award/SummaryBar.jsx | 2 +- src/js/components/award/contract/IDVDetails.jsx | 9 +++++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/js/components/award/AwardInfo.jsx b/src/js/components/award/AwardInfo.jsx index 476e49d900..0e2a363f8a 100644 --- a/src/js/components/award/AwardInfo.jsx +++ b/src/js/components/award/AwardInfo.jsx @@ -52,6 +52,8 @@ export default class AwardInfo extends React.Component { let amountsDetailsSection = null; + console.log(this.props.selectedAward); + if (type === 'contract') { amountsDetailsSection = ( Date: Tue, 13 Feb 2018 12:55:41 -0500 Subject: [PATCH 78/89] Remove console statement --- src/js/components/award/AwardInfo.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/js/components/award/AwardInfo.jsx b/src/js/components/award/AwardInfo.jsx index 0e2a363f8a..476e49d900 100644 --- a/src/js/components/award/AwardInfo.jsx +++ b/src/js/components/award/AwardInfo.jsx @@ -52,8 +52,6 @@ export default class AwardInfo extends React.Component { let amountsDetailsSection = null; - console.log(this.props.selectedAward); - if (type === 'contract') { amountsDetailsSection = ( Date: Tue, 13 Feb 2018 13:09:04 -0500 Subject: [PATCH 79/89] Opting to not repeat code and instead pass unique Contract and IDV data elements to ContractDetails --- .../award/contract/AwardContract.jsx | 12 +- src/js/components/award/contract/AwardIDV.jsx | 17 +- .../award/contract/ContractDetails.jsx | 11 +- .../components/award/contract/IDVDetails.jsx | 234 ------------------ 4 files changed, 30 insertions(+), 244 deletions(-) delete mode 100644 src/js/components/award/contract/IDVDetails.jsx diff --git a/src/js/components/award/contract/AwardContract.jsx b/src/js/components/award/contract/AwardContract.jsx index 2693d29318..e625ceb8c7 100644 --- a/src/js/components/award/contract/AwardContract.jsx +++ b/src/js/components/award/contract/AwardContract.jsx @@ -17,6 +17,13 @@ const propTypes = { export default class AwardContract extends React.Component { render() { + let awardType = "Not Available"; + if (this.props.selectedAward.latest_transaction.contract_data.contract_award_type_desc) { + awardType = this.props.selectedAward.latest_transaction.contract_data.contract_award_type_desc; + } + + const endDate = this.props.selectedAward.period_of_performance_current_end_date; + return (
    + maxChars={SummaryPageHelper.maxDescriptionCharacters} + awardType={awardType} + endDate={endDate} + />
    ); } diff --git a/src/js/components/award/contract/AwardIDV.jsx b/src/js/components/award/contract/AwardIDV.jsx index 84ccc85988..7f64659b66 100644 --- a/src/js/components/award/contract/AwardIDV.jsx +++ b/src/js/components/award/contract/AwardIDV.jsx @@ -5,10 +5,12 @@ import React from 'react'; import PropTypes from 'prop-types'; +import moment from 'moment'; import * as SummaryPageHelper from 'helpers/summaryPageHelper'; +import { idvAwardTypes } from 'dataMapping/contracts/idvAwardTypes'; import AwardAmounts from '../AwardAmounts'; -import IDVDetails from './IDVDetails'; +import ContractDetails from './ContractDetails'; const propTypes = { selectedAward: PropTypes.object, @@ -17,15 +19,24 @@ const propTypes = { export default class AwardIDV extends React.Component { render() { + let awardType = "Not Available"; + if (this.props.selectedAward.latest_transaction.contract_data.idv_type) { + awardType = idvAwardTypes[this.props.selectedAward.latest_transaction.contract_data.idv_type]; + } + + const endDate = moment(this.props.selectedAward.latest_transaction.contract_data.ordering_period_end_date).format('M/D/YYYY'); + return (
    - + maxChars={SummaryPageHelper.maxDescriptionCharacters} + awardType={awardType} + endDate={endDate} />
    ); } diff --git a/src/js/components/award/contract/ContractDetails.jsx b/src/js/components/award/contract/ContractDetails.jsx index 287468db88..dae18c0346 100644 --- a/src/js/components/award/contract/ContractDetails.jsx +++ b/src/js/components/award/contract/ContractDetails.jsx @@ -11,7 +11,9 @@ import DetailRow from '../DetailRow'; const propTypes = { selectedAward: PropTypes.object, seeAdditional: PropTypes.func, - maxChars: PropTypes.number + maxChars: PropTypes.number, + awardType: PropTypes.string, + endDate: PropTypes.string }; const isEmpty = (field, ignoreDefault) => { @@ -105,7 +107,7 @@ export default class ContractDetails extends React.Component { // Date Range const formattedStartDate = award.period_of_performance_start_date; - const formattedEndDate = award.period_of_performance_current_end_date; + const formattedEndDate = this.props.endDate; const startDate = moment(formattedStartDate, 'M/D/YYYY'); const endDate = moment(formattedEndDate, 'M/D/YYYY'); @@ -156,10 +158,7 @@ export default class ContractDetails extends React.Component { } // Award Type - let awardType = "Not Available"; - if (award.latest_transaction.contract_data.contract_award_type_desc) { - awardType = award.latest_transaction.contract_data.contract_award_type_desc; - } + let awardType = this.props.awardType; // Pricing let pricing = "Not Available"; diff --git a/src/js/components/award/contract/IDVDetails.jsx b/src/js/components/award/contract/IDVDetails.jsx deleted file mode 100644 index 2a17371969..0000000000 --- a/src/js/components/award/contract/IDVDetails.jsx +++ /dev/null @@ -1,234 +0,0 @@ -/** - * IDVDetails.jsx - * Created by michaelbray on 2/13/18. - **/ - -import React from 'react'; -import PropTypes from 'prop-types'; -import moment from 'moment'; -import { idvAwardTypes } from 'dataMapping/contracts/idvAwardTypes'; -import DetailRow from '../DetailRow'; - -const propTypes = { - selectedAward: PropTypes.object, - seeAdditional: PropTypes.func, - maxChars: PropTypes.number -}; - -const isEmpty = (field, ignoreDefault) => { - if (!field) { - return true; - } - if (field === '') { - return true; - } - if (!ignoreDefault && field === ignoreDefault) { - return true; - } - return false; -}; - -export default class IDVDetails extends React.Component { - constructor(props) { - super(props); - - this.state = { - desc: "", - date: "", - place: "", - typeDesc: "", - price: "", - overflow: false - }; - } - - componentDidMount() { - this.prepareValues(this.props.selectedAward); - } - - componentWillReceiveProps(nextProps) { - if (!Object.is(nextProps.selectedAward, this.props.selectedAward)) { - this.prepareValues(nextProps.selectedAward); - } - } - - parsePlaceOfPerformance(award) { - // Location - let popPlace = ''; - - let cityState = null; - const city = award.pop_city; - const stateProvince = award.pop_state_province; - - if (!isEmpty(city) && !isEmpty(stateProvince)) { - cityState = `${city}, ${stateProvince}`; - } - else if (!isEmpty(city)) { - cityState = city; - } - else if (!isEmpty(stateProvince)) { - cityState = stateProvince; - } - if (award.pop_country_code === 'USA') { - if (!isEmpty(cityState)) { - popPlace = cityState; - } - if (!isEmpty(award.pop_zip)) { - if (popPlace !== '') { - popPlace += ' '; - } - popPlace += award.pop_zip; - } - - if (!isEmpty(award.pop_state_code) && !isEmpty(award.pop_congressional_district)) { - if (popPlace !== '') { - popPlace += '\n'; - } - popPlace += - `Congressional District: ${award.pop_state_code}-${award.pop_congressional_district}`; - } - } - else if (award.pop_country_code !== 'USA') { - popPlace = `${award.pop_country}`; - } - - if (popPlace === '') { - popPlace = 'Not available'; - } - - return popPlace; - } - - prepareValues(award) { - let yearRangeTotal = ""; - let monthRangeTotal = ""; - let description = null; - - // Date Range - const formattedStartDate = award.period_of_performance_start_date; - - // Convert this datetime into a moment date if it exists - let formattedEndDate = null; - if (award.latest_transaction.contract_data.ordering_period_end_date !== null) { - formattedEndDate = moment( - award.latest_transaction.contract_data.ordering_period_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 (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 if (yearRangeTotal) { - timeRange = `(${yearRangeTotal})`; - } - - popDate = `${formattedStartDate} - ${formattedEndDate.format('M/D/YYYY')} ${timeRange}`; - } - - if (award.description) { - description = award.description; - } - else { - description = "Not Available"; - } - - // Award Type - let awardType = "Not Available"; - if (award.latest_transaction.contract_data.idv_type) { - awardType = idvAwardTypes[award.latest_transaction.contract_data.idv_type]; - } - - // Pricing - let pricing = "Not Available"; - if (award.type_of_contract_pricing_description) { - pricing = award.type_of_contract_pricing_description; - } - - // char count - let seeMore = false; - if (award.description.length > this.props.maxChars) { - seeMore = true; - } - - this.setState({ - desc: description, - overflow: seeMore, - date: popDate, - place: this.parsePlaceOfPerformance(award), - typeDesc: awardType, - price: pricing - }); - } - - render() { - return ( -
    -
    -

    Contract Details

    -
    { - this.sectionHr = hr; - }} /> - - - - - - - - -
    -
    - -
    - ); - } -} - -IDVDetails.propTypes = propTypes; From a197f957988f91d348aeab8fbcda7128561c4c3d Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Tue, 13 Feb 2018 13:10:11 -0500 Subject: [PATCH 80/89] Fixing linting --- src/js/components/award/contract/AwardContract.jsx | 3 +-- src/js/components/award/contract/ContractDetails.jsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/js/components/award/contract/AwardContract.jsx b/src/js/components/award/contract/AwardContract.jsx index e625ceb8c7..82c33b5398 100644 --- a/src/js/components/award/contract/AwardContract.jsx +++ b/src/js/components/award/contract/AwardContract.jsx @@ -34,8 +34,7 @@ export default class AwardContract extends React.Component { seeAdditional={this.props.seeAdditional} maxChars={SummaryPageHelper.maxDescriptionCharacters} awardType={awardType} - endDate={endDate} - /> + endDate={endDate} /> ); } diff --git a/src/js/components/award/contract/ContractDetails.jsx b/src/js/components/award/contract/ContractDetails.jsx index dae18c0346..9cf9bdbd92 100644 --- a/src/js/components/award/contract/ContractDetails.jsx +++ b/src/js/components/award/contract/ContractDetails.jsx @@ -158,7 +158,7 @@ export default class ContractDetails extends React.Component { } // Award Type - let awardType = this.props.awardType; + const awardType = this.props.awardType; // Pricing let pricing = "Not Available"; From ffb595a3f54a556e53959e2a2f04177da4023e18 Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Tue, 13 Feb 2018 13:16:30 -0500 Subject: [PATCH 81/89] Further refactoring --- src/js/components/award/AwardInfo.jsx | 9 ++-- .../award/contract/AwardContract.jsx | 24 ++++++++-- src/js/components/award/contract/AwardIDV.jsx | 45 ------------------- 3 files changed, 25 insertions(+), 53 deletions(-) delete mode 100644 src/js/components/award/contract/AwardIDV.jsx diff --git a/src/js/components/award/AwardInfo.jsx b/src/js/components/award/AwardInfo.jsx index 476e49d900..4a64dad750 100644 --- a/src/js/components/award/AwardInfo.jsx +++ b/src/js/components/award/AwardInfo.jsx @@ -11,7 +11,6 @@ import { scrollToY } from 'helpers/scrollToHelper'; import SummaryBar from './SummaryBar'; import AwardInfoBar from './AwardInfoBar'; import AwardContract from './contract/AwardContract'; -import AwardIDV from './contract/AwardIDV'; import AwardFinancialAssistance from './financialAssistance/AwardFinancialAssistance'; import DetailsSection from './details/DetailsSection'; @@ -57,15 +56,17 @@ export default class AwardInfo extends React.Component { + seeAdditional={this.seeAdditional} + type="contract" /> ); } else if (type === 'unknown') { amountsDetailsSection = ( - + seeAdditional={this.seeAdditional} + type="idv" /> ); } else { diff --git a/src/js/components/award/contract/AwardContract.jsx b/src/js/components/award/contract/AwardContract.jsx index 82c33b5398..30dee74a55 100644 --- a/src/js/components/award/contract/AwardContract.jsx +++ b/src/js/components/award/contract/AwardContract.jsx @@ -5,24 +5,40 @@ import React from 'react'; import PropTypes from 'prop-types'; +import moment from 'moment'; import * as SummaryPageHelper from 'helpers/summaryPageHelper'; +import { idvAwardTypes } from 'dataMapping/contracts/idvAwardTypes'; import AwardAmounts from '../AwardAmounts'; import ContractDetails from './ContractDetails'; const propTypes = { selectedAward: PropTypes.object, - seeAdditional: PropTypes.func + seeAdditional: PropTypes.func, + type: PropTypes.string }; export default class AwardContract extends React.Component { render() { let awardType = "Not Available"; - if (this.props.selectedAward.latest_transaction.contract_data.contract_award_type_desc) { - awardType = this.props.selectedAward.latest_transaction.contract_data.contract_award_type_desc; + let endDate = ""; + + // This component is used for both Contracts and IDVs + if (this.props.type === 'contract') { + if (this.props.selectedAward.latest_transaction.contract_data.contract_award_type_desc) { + awardType = this.props.selectedAward.latest_transaction.contract_data.contract_award_type_desc; + } + + endDate = this.props.selectedAward.period_of_performance_current_end_date; } + else { + // Use IDV fields + if (this.props.selectedAward.latest_transaction.contract_data.idv_type) { + awardType = idvAwardTypes[this.props.selectedAward.latest_transaction.contract_data.idv_type]; + } - const endDate = this.props.selectedAward.period_of_performance_current_end_date; + endDate = moment(this.props.selectedAward.latest_transaction.contract_data.ordering_period_end_date).format('M/D/YYYY'); + } return (
    diff --git a/src/js/components/award/contract/AwardIDV.jsx b/src/js/components/award/contract/AwardIDV.jsx deleted file mode 100644 index 7f64659b66..0000000000 --- a/src/js/components/award/contract/AwardIDV.jsx +++ /dev/null @@ -1,45 +0,0 @@ -/** - * AwardIDV.jsx - * Created by michaelbray on 2/13/18. - **/ - -import React from 'react'; -import PropTypes from 'prop-types'; -import moment from 'moment'; -import * as SummaryPageHelper from 'helpers/summaryPageHelper'; -import { idvAwardTypes } from 'dataMapping/contracts/idvAwardTypes'; - -import AwardAmounts from '../AwardAmounts'; -import ContractDetails from './ContractDetails'; - -const propTypes = { - selectedAward: PropTypes.object, - seeAdditional: PropTypes.func -}; - -export default class AwardIDV extends React.Component { - render() { - let awardType = "Not Available"; - if (this.props.selectedAward.latest_transaction.contract_data.idv_type) { - awardType = idvAwardTypes[this.props.selectedAward.latest_transaction.contract_data.idv_type]; - } - - const endDate = moment(this.props.selectedAward.latest_transaction.contract_data.ordering_period_end_date).format('M/D/YYYY'); - - return ( -
    - - -
    - ); - } -} - -AwardIDV.propTypes = propTypes; From 0aa5de2b3e38ab51ee2590ef865abeb116b093df Mon Sep 17 00:00:00 2001 From: Michael Bray Date: Tue, 13 Feb 2018 13:55:53 -0500 Subject: [PATCH 82/89] Enabling parent award IDs for IDV Summary Bar --- src/js/components/award/SummaryBar.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/js/components/award/SummaryBar.jsx b/src/js/components/award/SummaryBar.jsx index 199753e9dd..c155693a32 100644 --- a/src/js/components/award/SummaryBar.jsx +++ b/src/js/components/award/SummaryBar.jsx @@ -46,9 +46,11 @@ export default class SummaryBar extends React.Component { let progress = ""; let awardType = startCase(toLower(SummaryPageHelper.awardType(award.award_type))); + let isIDV = false; if (award.award_type === "" && award.latest_transaction.contract_data.idv_type !== null) { // Award is an IDV - use "Contract" awardType = "Contract"; + isIDV = true; } let parentId = null; @@ -62,7 +64,7 @@ export default class SummaryBar extends React.Component { else { progress = "In Progress"; } - if (includes(awardTypeGroups.contracts, award.award_type)) { + if (includes(awardTypeGroups.contracts, award.award_type) || isIDV) { if (award.parent_award_id) { parentId = award.parent_award_id; } From 40dcaf1439877ca56c0709c3e54848f034268e4f Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Wed, 14 Feb 2018 09:55:18 -0500 Subject: [PATCH 83/89] Reset the page number to 1 when the sort changes --- src/js/containers/accountLanding/AccountLandingContainer.jsx | 4 +++- .../accountLanding/AccountLandingContainer-test.jsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/js/containers/accountLanding/AccountLandingContainer.jsx b/src/js/containers/accountLanding/AccountLandingContainer.jsx index 3ec1b68550..e62273d88c 100644 --- a/src/js/containers/accountLanding/AccountLandingContainer.jsx +++ b/src/js/containers/accountLanding/AccountLandingContainer.jsx @@ -77,11 +77,13 @@ export default class AccountLandingContainer extends React.Component { updateSort(field, direction) { // Change sort in the state and make a new request + // Reset the page number to 1 this.setState({ order: { field, direction - } + }, + pageNumber: 1 }, () => { this.fetchAccounts(); }); diff --git a/tests/containers/accountLanding/AccountLandingContainer-test.jsx b/tests/containers/accountLanding/AccountLandingContainer-test.jsx index 299b87cc03..46509a9d9f 100644 --- a/tests/containers/accountLanding/AccountLandingContainer-test.jsx +++ b/tests/containers/accountLanding/AccountLandingContainer-test.jsx @@ -74,7 +74,7 @@ describe('AccountLandingContainer', () => { }); describe('updateSort', () => { - it('should update the container state', () => { + it('should update the container state and reset the page number to 1', () => { const container = shallow(); // change the sort order @@ -84,6 +84,8 @@ describe('AccountLandingContainer', () => { field: 'managing_agency', direction: 'asc' }); + + expect(container.state().pageNumber).toEqual(1); }); }); From 60944f169d4636735423628e42447bd7f0e1d4b1 Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Wed, 14 Feb 2018 11:28:34 -0500 Subject: [PATCH 84/89] Added handle url tests --- .../keyword/KeywordContainer-test.jsx | 43 +++++++++++++++++-- tests/containers/keyword/keywordHelper.js | 13 +++++- tests/containers/keyword/mockRouter.js | 7 +++ 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 tests/containers/keyword/mockRouter.js diff --git a/tests/containers/keyword/KeywordContainer-test.jsx b/tests/containers/keyword/KeywordContainer-test.jsx index 9206106fd1..6cb8dc8673 100644 --- a/tests/containers/keyword/KeywordContainer-test.jsx +++ b/tests/containers/keyword/KeywordContainer-test.jsx @@ -4,18 +4,19 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, mount } from 'enzyme'; import { KeywordContainer } from 'containers/keyword/KeywordContainer'; import { mockRedux, mockSummary, mockActions } from './mockResults'; +import Router from './mockRouter'; jest.mock('helpers/keywordHelper', () => require('./keywordHelper')); jest.mock('helpers/bulkDownloadHelper', () => require('../bulkDownload/mockBulkDownloadHelper')); // mock the child component by replacing it with a function that returns a null element -jest.mock('components/keyword/KeywordPage', () => - jest.fn(() => null)); +jest.mock('components/keyword/KeywordPage', () => jest.fn(() => null)); +jest.mock('containers/router/Router', () => require('./mockRouter')); describe('KeywordContainer', () => { describe('updateKeyword', () => { @@ -28,6 +29,42 @@ describe('KeywordContainer', () => { expect(container.state().keyword).toEqual('blah blah'); }); + it('should update the page url', () => { + const container = shallow(); + + container.instance().updateKeyword('blah blah'); + + expect(Router.history.replace).toHaveBeenLastCalledWith('/keyword_search/blah-blah'); + }); + }); + + describe('handleInitialUrl', ()=> { + it('should update the state if there is a keyword in the url', () => { + const modifiedRedux = Object.assign({}, mockRedux, { + params: { + keyword: 'test' + } + }); + const container = mount(); + + expect(container.state().keyword).toEqual('test'); + }); + it('should not update the state if the keyword is less than three characters', () => { + const modifiedRedux = Object.assign({}, mockRedux, { + params: { + keyword: 'hi' + } + }); + const container = mount(); + + expect(container.state().keyword).toEqual(''); + }); }); describe('fetchSummary', () => { diff --git a/tests/containers/keyword/keywordHelper.js b/tests/containers/keyword/keywordHelper.js index 6d84ecc2cc..f74d485141 100644 --- a/tests/containers/keyword/keywordHelper.js +++ b/tests/containers/keyword/keywordHelper.js @@ -37,4 +37,15 @@ export const performTabCountSearch = () => ( }), cancel: jest.fn() } -); \ No newline at end of file +); + +export const slugFromString = (string) => + string.toString().toLowerCase().trim() + .replace(/\s+/g, "-") // Replace spaces with - + // .replace(/[^\w-]+/g, "") // Remove non word or hyphen characters + .replace(/--+/g, "-") // Replace multiple - with single - + .replace(/^-+/, "") // Don't use hyphens at the beginning or end of the slug + .replace(/-+$/, ""); + +export const stringFromSlug = (slug) => + slug.replace(/[-._~]+|([a-z])([A-Z])/g, ' '); \ No newline at end of file diff --git a/tests/containers/keyword/mockRouter.js b/tests/containers/keyword/mockRouter.js new file mode 100644 index 0000000000..8f68fa7e07 --- /dev/null +++ b/tests/containers/keyword/mockRouter.js @@ -0,0 +1,7 @@ +const Router = { + history: { + replace: jest.fn() + } +}; + +export default Router; From 7884c3955bdc81c86665eabfd4268688d362e530 Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Wed, 14 Feb 2018 14:37:10 -0500 Subject: [PATCH 85/89] Silently update; use encodeURIcomponent --- src/js/containers/keyword/KeywordContainer.jsx | 6 +++--- src/js/containers/router/RouterRoutes.jsx | 2 ++ src/js/helpers/keywordHelper.js | 11 ----------- tests/containers/keyword/KeywordContainer-test.jsx | 2 +- tests/containers/keyword/keywordHelper.js | 13 +------------ 5 files changed, 7 insertions(+), 27 deletions(-) diff --git a/src/js/containers/keyword/KeywordContainer.jsx b/src/js/containers/keyword/KeywordContainer.jsx index 9525cd4155..3ade3fa1f2 100644 --- a/src/js/containers/keyword/KeywordContainer.jsx +++ b/src/js/containers/keyword/KeywordContainer.jsx @@ -54,7 +54,7 @@ export class KeywordContainer extends React.Component { const nextKeywordUrl = nextProps.params.keyword; if (nextKeywordUrl) { // Convert the url to a keyword - const nextKeyword = KeywordHelper.stringFromSlug(nextKeywordUrl); + const nextKeyword = decodeURIComponent(nextKeywordUrl); // Update the keyword only if it has changed and is more than two characters if (nextKeyword !== this.state.keyword && nextKeyword.length > 2) { this.setState({ @@ -73,7 +73,7 @@ export class KeywordContainer extends React.Component { handleInitialUrl(urlKeyword) { if (urlKeyword) { // Convert the url to a keyword - const keyword = KeywordHelper.stringFromSlug(urlKeyword); + const keyword = decodeURIComponent(urlKeyword); // Update the keyword only if it has more than two characters if (keyword.length > 2) { this.setState({ @@ -180,7 +180,7 @@ export class KeywordContainer extends React.Component { updateKeyword(keyword) { // Convert the keyword to a url slug - const slug = KeywordHelper.slugFromString(keyword); + const slug = encodeURIComponent(keyword); this.setState({ keyword }, () => { diff --git a/src/js/containers/router/RouterRoutes.jsx b/src/js/containers/router/RouterRoutes.jsx index 8adf0fa8df..c8f280668a 100644 --- a/src/js/containers/router/RouterRoutes.jsx +++ b/src/js/containers/router/RouterRoutes.jsx @@ -140,6 +140,7 @@ const routes = { { path: '/keyword_search', parent: '/keyword_search', + silentlyUpdate: true, component: (cb) => { require.ensure([], (require) => { cb(require('containers/keyword/KeywordContainer').default); @@ -149,6 +150,7 @@ const routes = { { path: '/keyword_search/:keyword', parent: '/keyword_search', + silentlyUpdate: true, component: (cb) => { require.ensure([], (require) => { cb(require('containers/keyword/KeywordContainer').default); diff --git a/src/js/helpers/keywordHelper.js b/src/js/helpers/keywordHelper.js index f6a38ab75c..dd289c9b3d 100644 --- a/src/js/helpers/keywordHelper.js +++ b/src/js/helpers/keywordHelper.js @@ -54,14 +54,3 @@ export const performTabCountSearch = (params) => { } }; }; - -export const slugFromString = (string) => - string.toString().toLowerCase().trim() - .replace(/\s+/g, "-") // Replace spaces with - - .replace(/[^\w-]+/g, "") // Remove non word or hyphen characters - .replace(/--+/g, "-") // Replace multiple - with single - - .replace(/^-+/, "") // Don't use hyphens at the beginning or end of the slug - .replace(/-+$/, ""); - -export const stringFromSlug = (slug) => - slug.replace(/[-._~]+|([a-z])([A-Z])/g, ' '); diff --git a/tests/containers/keyword/KeywordContainer-test.jsx b/tests/containers/keyword/KeywordContainer-test.jsx index 6cb8dc8673..a3eed78183 100644 --- a/tests/containers/keyword/KeywordContainer-test.jsx +++ b/tests/containers/keyword/KeywordContainer-test.jsx @@ -36,7 +36,7 @@ describe('KeywordContainer', () => { container.instance().updateKeyword('blah blah'); - expect(Router.history.replace).toHaveBeenLastCalledWith('/keyword_search/blah-blah'); + expect(Router.history.replace).toHaveBeenLastCalledWith('/keyword_search/blah%20blah'); }); }); diff --git a/tests/containers/keyword/keywordHelper.js b/tests/containers/keyword/keywordHelper.js index f74d485141..6d84ecc2cc 100644 --- a/tests/containers/keyword/keywordHelper.js +++ b/tests/containers/keyword/keywordHelper.js @@ -37,15 +37,4 @@ export const performTabCountSearch = () => ( }), cancel: jest.fn() } -); - -export const slugFromString = (string) => - string.toString().toLowerCase().trim() - .replace(/\s+/g, "-") // Replace spaces with - - // .replace(/[^\w-]+/g, "") // Remove non word or hyphen characters - .replace(/--+/g, "-") // Replace multiple - with single - - .replace(/^-+/, "") // Don't use hyphens at the beginning or end of the slug - .replace(/-+$/, ""); - -export const stringFromSlug = (slug) => - slug.replace(/[-._~]+|([a-z])([A-Z])/g, ' '); \ No newline at end of file +); \ No newline at end of file From 1afa7467e8406bd73335c32cf1db614d05a3ff38 Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Wed, 14 Feb 2018 15:41:46 -0500 Subject: [PATCH 86/89] Refactored handle url logic --- .../containers/keyword/KeywordContainer.jsx | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/js/containers/keyword/KeywordContainer.jsx b/src/js/containers/keyword/KeywordContainer.jsx index 1d419d127f..4a593e1968 100644 --- a/src/js/containers/keyword/KeywordContainer.jsx +++ b/src/js/containers/keyword/KeywordContainer.jsx @@ -49,30 +49,14 @@ export class KeywordContainer extends React.Component { } componentWillMount() { - this.handleInitialUrl(this.props.params.keyword); + this.handleUrl(this.props.params.keyword); } componentWillReceiveProps(nextProps) { - const nextKeywordUrl = nextProps.params.keyword; - if (nextKeywordUrl) { - // Convert the url to a keyword - const nextKeyword = decodeURIComponent(nextKeywordUrl); - // Update the keyword only if it has changed and is more than two characters - if (nextKeyword !== this.state.keyword && nextKeyword.length > 2) { - this.setState({ - keyword: nextKeyword - }); - } - } - else { - // The keyword param was removed from the url, reset the keyword - this.setState({ - keyword: '' - }); - } + this.handleUrl(nextProps.params.keyword); } - handleInitialUrl(urlKeyword) { + handleUrl(urlKeyword) { if (urlKeyword) { // Convert the url to a keyword const keyword = decodeURIComponent(urlKeyword); @@ -83,6 +67,12 @@ export class KeywordContainer extends React.Component { }); } } + else if (this.state.keyword) { + // The keyword param was removed from the url, reset the keyword + this.setState({ + keyword: '' + }); + } } startDownload() { From 7de74e01fbf974510ef5de88c4ae839f61914e93 Mon Sep 17 00:00:00 2001 From: Lizzie Salita Date: Wed, 14 Feb 2018 15:47:36 -0500 Subject: [PATCH 87/89] check next props --- src/js/containers/keyword/KeywordContainer.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/js/containers/keyword/KeywordContainer.jsx b/src/js/containers/keyword/KeywordContainer.jsx index 4a593e1968..dc231b09fd 100644 --- a/src/js/containers/keyword/KeywordContainer.jsx +++ b/src/js/containers/keyword/KeywordContainer.jsx @@ -53,7 +53,9 @@ export class KeywordContainer extends React.Component { } componentWillReceiveProps(nextProps) { - this.handleUrl(nextProps.params.keyword); + if (this.props.params.keyword !== nextProps.params.keyword) { + this.handleUrl(nextProps.params.keyword); + } } handleUrl(urlKeyword) { From 11c0bb534bc4e016a3ff1c68b21e72fef65d75e1 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Wed, 21 Feb 2018 16:12:15 -0500 Subject: [PATCH 88/89] Enable keyword download --- src/js/components/keyword/KeywordPage.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/components/keyword/KeywordPage.jsx b/src/js/components/keyword/KeywordPage.jsx index e7f97106fc..8c0738a2e0 100644 --- a/src/js/components/keyword/KeywordPage.jsx +++ b/src/js/components/keyword/KeywordPage.jsx @@ -168,7 +168,7 @@ export default class KeywordPage extends React.Component { {searchSummary}
    From a04ab275c98e82c9b6dae732f6391cb8df85e4c5 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Wed, 21 Feb 2018 16:37:57 -0500 Subject: [PATCH 89/89] Refactor download button to appropriately handle its shared use on keyword search --- src/_scss/pages/keyword/header/header.scss | 8 -------- src/js/components/keyword/KeywordPage.jsx | 3 ++- src/js/components/search/SearchPage.jsx | 2 +- src/js/components/search/header/DownloadButton.jsx | 9 ++------- 4 files changed, 5 insertions(+), 17 deletions(-) diff --git a/src/_scss/pages/keyword/header/header.scss b/src/_scss/pages/keyword/header/header.scss index 1223259a5e..e9c9a9941a 100644 --- a/src/_scss/pages/keyword/header/header.scss +++ b/src/_scss/pages/keyword/header/header.scss @@ -71,13 +71,5 @@ float: right; } } - - &.no-hover { - .download-wrap { - .download-hover-spacer { - display: none; - } - } - } } } diff --git a/src/js/components/keyword/KeywordPage.jsx b/src/js/components/keyword/KeywordPage.jsx index 8c0738a2e0..d6d2b3872d 100644 --- a/src/js/components/keyword/KeywordPage.jsx +++ b/src/js/components/keyword/KeywordPage.jsx @@ -166,8 +166,9 @@ export default class KeywordPage extends React.Component {

    Keyword Search

    {searchSummary} -
    +
    diff --git a/src/js/components/search/SearchPage.jsx b/src/js/components/search/SearchPage.jsx index aeb672c9da..7bda71dc8b 100644 --- a/src/js/components/search/SearchPage.jsx +++ b/src/js/components/search/SearchPage.jsx @@ -147,7 +147,7 @@ export default class SearchPage extends React.Component { + disableHover={this.state.filterCount === 0} />
    diff --git a/src/js/components/search/header/DownloadButton.jsx b/src/js/components/search/header/DownloadButton.jsx index b9339b723e..599b76b621 100644 --- a/src/js/components/search/header/DownloadButton.jsx +++ b/src/js/components/search/header/DownloadButton.jsx @@ -11,11 +11,7 @@ import NoDownloadHover from './NoDownloadHover'; const propTypes = { onClick: PropTypes.func, downloadAvailable: PropTypes.bool, - filterCount: PropTypes.number -}; - -const defaultProps = { - filterCount: 0 + disableHover: PropTypes.bool }; export default class DownloadButton extends React.Component { @@ -54,7 +50,7 @@ export default class DownloadButton extends React.Component { render() { let hover = null; - if (this.state.showHover && !this.props.downloadAvailable && this.props.filterCount > 0) { + if (this.state.showHover && !this.props.downloadAvailable && !this.props.disableHover) { hover = (); } @@ -88,4 +84,3 @@ export default class DownloadButton extends React.Component { } DownloadButton.propTypes = propTypes; -DownloadButton.defaultProps = defaultProps;