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/_scss/components/_pagination.scss b/src/_scss/components/_pagination.scss index 4bdc553417..58ffdf7355 100644 --- a/src/_scss/components/_pagination.scss +++ b/src/_scss/components/_pagination.scss @@ -1,18 +1,20 @@ .pagination { - margin: rem(15); - .results-text { - @include display(flex); - @include flex-direction(row); - float: left; + @include display(flex); + @include flex-direction(row); + @include align-items(center); + .pagination__totals { + @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; - li { - button { + .pager__item { + @include flex(0 0 auto); + .pager__button { padding: rem(7) rem(10); background-color: $color-white; color: $color-gray; @@ -23,16 +25,7 @@ &:hover { background-color: $color-primary-alt-lightest; } - } - &.active { - button { - background-color: $color-primary-alt-dark; - color: $color-white; - border-color: $color-primary-alt-dark; - } - } - &.disabled { - button { + &.pager__button_disabled { border-color: $color-gray-lighter; color: $color-gray-light; &:hover { @@ -40,9 +33,14 @@ cursor: auto; } } + &.pager__button_active { + background-color: $color-primary-alt-dark; + color: $color-white; + border-color: $color-primary-alt-dark; + } } } - .pagination-ellipsis { + .pager__ellipsis { font-weight: bold; margin-top: 14px; margin-right: 5px; 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/default/stickyHeader/header.scss b/src/_scss/layouts/default/stickyHeader/header.scss new file mode 100644 index 0000000000..b9a59a47ab --- /dev/null +++ b/src/_scss/layouts/default/stickyHeader/header.scss @@ -0,0 +1,50 @@ +.sticky-header { + $sticky-header-height: rem(66); + + position: relative; + height: $sticky-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_sticky { + @include media($medium-screen) { + position: fixed; + top: 0; + z-index: $z-header; + } + } + + .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: $sticky-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__options { + @include flex(0 0 auto); + @import "../../tabbedSearch/header/downloadButton"; + } + } + } +} 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/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/layouts/tabbedSearch/header/header.scss b/src/_scss/layouts/tabbedSearch/header/header.scss deleted file mode 100644 index 804f9f894e..0000000000 --- a/src/_scss/layouts/tabbedSearch/header/header.scss +++ /dev/null @@ -1,51 +0,0 @@ -$search-header-height: rem(66); - -.search-header-wrapper { - position: relative; - height: $search-header-height; -} - -.search-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 { - @include media($medium-screen) { - position: fixed; - top: 0; - z-index: $z-header; - } - } - - .search-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; - - .search-title { - @include flex(1 1 auto); - h1 { - font-size: rem(24); - line-height: rem(31); - font-weight: $font-semibold; - margin: 0; - } - } - - .search-options { - @include flex(0 0 auto); - @import "./_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/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/accountLanding/accountLandingPage.scss b/src/_scss/pages/accountLanding/accountLandingPage.scss new file mode 100644 index 0000000000..6ef3122118 --- /dev/null +++ b/src/_scss/pages/accountLanding/accountLandingPage.scss @@ -0,0 +1,12 @@ +.usa-da-account-landing { + @import "all"; + @import "layouts/landingPage/landingPage"; + @import "components/pagination"; + @import "layouts/default/stickyHeader/header"; + @import './searchSection'; + @import './table/resultsSection'; + + .search-section { + display: none; + } +} \ 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..d43320e656 --- /dev/null +++ b/src/_scss/pages/accountLanding/searchSection.scss @@ -0,0 +1,39 @@ +.search-section { + background-color: $color-primary-alt-lightest; + padding: rem(10); + border: 1px solid $color-vis-lightest; + text-align: center; + @include media($medium-screen) { + padding: rem(38); + } + .search-section__form { + @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: rem(10) rem(15); + border: 0; + @include media($medium-screen) { + font-size: rem(22); + } + } + .search-section__button { + @include flex(0 0 auto); + @include button-unstyled; + padding: 0 rem(15); + .search-section__button-icon { + height: rem(24); + width: rem(24); + svg { + fill: $color-gray-light; + } + } + } + } +} \ 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..a35e8acdf6 --- /dev/null +++ b/src/_scss/pages/accountLanding/table/cells/_accountLandingHeaderCell.scss @@ -0,0 +1,46 @@ +.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..22588e921e --- /dev/null +++ b/src/_scss/pages/accountLanding/table/resultsSection.scss @@ -0,0 +1,8 @@ +@import "pages/search/results/table/_tableMessages"; +@import "pages/search/results/screens/screens"; +.results-table-section { + position: relative; + min-height: rem(200); + @include transition(opacity 0.25s ease-in); + @import './table'; +} diff --git a/src/_scss/pages/accountLanding/table/table.scss b/src/_scss/pages/accountLanding/table/table.scss new file mode 100644 index 0000000000..89410ebeb7 --- /dev/null +++ b/src/_scss/pages/accountLanding/table/table.scss @@ -0,0 +1,61 @@ +.results-table { + display: block; + overflow: scroll; + .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"; + } + } + .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 { + text-align: right; + } + } + } + .results-table-cell { + .results-table-cell__content { + .results-table-cell__matched { + &.results-table-cell__matched_highlight { + text-decoration: underline; + font-weight: 600; + } + } + } + } + // gray out even rows + .results-table__data_even { + background-color: #f7f7f7; + } + } + } + +} \ 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..a56b17d5ad 100644 --- a/src/_scss/pages/agencyLanding/agencyLandingPage.scss +++ b/src/_scss/pages/agencyLanding/agencyLandingPage.scss @@ -1,18 +1,14 @@ .usa-da-agency-landing { @import "all"; - @import "layouts/landingPage/landingPage"; - @import "./header/header"; - @import './agencyLandingSearch'; + @import "layouts/landingPage/landingPage"; + @import "layouts/default/stickyHeader/header"; + @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/_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/archive/archive.scss b/src/_scss/pages/bulkDownload/archive/archive.scss index 77608f6ce7..69dd9f5f11 100644 --- a/src/_scss/pages/bulkDownload/archive/archive.scss +++ b/src/_scss/pages/bulkDownload/archive/archive.scss @@ -4,6 +4,7 @@ } h2 { margin-top: 0; + font-weight: 400; } @import "archiveForm"; @import "archiveTable"; diff --git a/src/_scss/pages/bulkDownload/bulkDownloadPage.scss b/src/_scss/pages/bulkDownload/bulkDownloadPage.scss index 57c2360feb..6fbbfaa2dc 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/bulkDownload/downloadData.scss b/src/_scss/pages/bulkDownload/downloadData.scss index 848d3d2b93..4f6353a258 100644 --- a/src/_scss/pages/bulkDownload/downloadData.scss +++ b/src/_scss/pages/bulkDownload/downloadData.scss @@ -8,9 +8,7 @@ margin-bottom: rem(30); h2 { margin: 0 rem(30); - span { - font-weight: 400; - } + font-weight: 400; } @import "./form/downloadForm"; .reset-button { 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/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; } 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/keyword/_searchBar.scss b/src/_scss/pages/keyword/_searchBar.scss index 1ccb620408..01831c2e69 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,67 +8,72 @@ @include align-items(center); @include justify-content(space-evenly); } - .keyword-search-bar { + + .keyword-search-bar__form { + border: 1px solid $color-gray-light; + margin-right: rem(15); + background-color: $color-white; + @include display(flex); + @include flex-direction(row); + @include align-items(center); + @include justify-content(flex-start); @include media($medium-screen) { - padding-right: rem(10); + @include flex(1 1 auto); 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); + .keyword-search-bar__input { + @include flex(1 1 auto); + margin-right: 0; + height: rem(66); + border: 0; + padding: rem(15); + font-size: rem(20); + font-weight: 300; + @include media($tablet-screen) { font-size: rem(28); - font-weight: 300; line-height: rem(36); } - 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 { + @include flex(0 0 auto); + @include button-unstyled; + height: rem(66); + padding: 0 rem(15); + .keyword-search-bar__button-icon { + height: rem(30); + width: rem(30); + svg { + fill: $color-gray; + } + } + &.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); padding-top: rem(10); @include media($medium-screen) { width: 50%; + @include flex(1 1 auto); padding-top: 0; + padding-left: rem(10); } - .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/header/header.scss b/src/_scss/pages/keyword/header/header.scss index b3b7e04566..e9c9a9941a 100644 --- a/src/_scss/pages/keyword/header/header.scss +++ b/src/_scss/pages/keyword/header/header.scss @@ -1,58 +1,75 @@ -@import "layouts/tabbedSearch/header/header"; +.keyword-header { + $keyword-header-height: rem(66); -.search-header-container { - .search-header { - .search-title { + @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; + + .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 media($medium-screen) { @include span-columns(8); - @include media($medium-screen) { - @include span-columns(4); - } + @include display(flex); } - .search-summary { - // Hide the search summary for small screens - display: none; - @include media($medium-screen) { - @include span-columns(8); - @include display(flex); - } - font-size: $small-font-size; - .summary-title { + + .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-amount_bold { font-weight: $font-semibold; - padding-right: rem(10); - margin-right: rem(10); - border-right: solid 1px $color-white; - } - .award-amounts, .award-counts { - margin-right: rem(20); - .number { - font-weight: $font-semibold; - } } } - .search-options { - @include span-columns(8); - @include media($medium-screen) { - @include span-columns(4); - } - .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); + } + + .download-wrap { + .download-hover-spacer { + left: rem(-140); + width: rem(290); } - &.no-hover { - .download-wrap { - .download-hover-spacer { - display: none; - } - } + .download-button { + float: right; } } - &:after { - display: block; - } } -} \ No newline at end of file +} diff --git a/src/_scss/pages/keyword/keywordPage.scss b/src/_scss/pages/keyword/keywordPage.scss index c0846985ed..b36341ab90 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 { @@ -14,6 +15,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/_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 { 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/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/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..f8e4855f09 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'; @@ -13,6 +14,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 +34,10 @@ const aboutSections = [ section: 'data-quality', label: 'Data Quality' }, + { + section: 'more-info', + label: 'More Information' + }, { section: 'contact', label: 'Contact' @@ -122,7 +128,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 +220,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} />
@@ -222,6 +229,7 @@ export default class AboutContent extends React.Component { +
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/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/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; 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/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/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/accountLanding/AccountLandingContent.jsx b/src/js/components/accountLanding/AccountLandingContent.jsx new file mode 100644 index 0000000000..ae7d7e502f --- /dev/null +++ b/src/js/components/accountLanding/AccountLandingContent.jsx @@ -0,0 +1,66 @@ +/** + * AccountLandingContent.jsx + * Created by Lizzie Salita 8/4/17 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import Pagination from 'components/sharedComponents/Pagination'; +import AccountLandingSearchBar from './AccountLandingSearchBar'; +import AccountLandingResultsSection from './AccountLandingResultsSection'; + +const propTypes = { + results: PropTypes.array, + accountSearchString: PropTypes.string, + inFlight: PropTypes.bool, + error: PropTypes.bool, + columns: PropTypes.array, + setAccountSearchString: PropTypes.func, + onChangePage: PropTypes.func, + pageNumber: PropTypes.number, + totalItems: PropTypes.number, + pageSize: PropTypes.number, + order: PropTypes.object, + updateSort: 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. +

+
+ + + + +
+ ); + } +} + +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..8219746ff0 --- /dev/null +++ b/src/js/components/accountLanding/AccountLandingPage.jsx @@ -0,0 +1,38 @@ +/** + * 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 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'; + +export default class AccountLandingPage extends React.Component { + render() { + return ( +
+ +
+ +
+

+ Federal Account Profiles +

+
+
+
+ +
+
+
+ ); + } +} diff --git a/src/js/components/accountLanding/AccountLandingResultsSection.jsx b/src/js/components/accountLanding/AccountLandingResultsSection.jsx new file mode 100644 index 0000000000..8c92764c8d --- /dev/null +++ b/src/js/components/accountLanding/AccountLandingResultsSection.jsx @@ -0,0 +1,63 @@ +/** + * AccountLandingResultsSection.jsx + * Created by Lizzie Salita 8/4/17 + */ + +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 = { + inFlight: PropTypes.bool, + error: PropTypes.bool, + results: PropTypes.array, + columns: PropTypes.array, + accountSearchString: PropTypes.string, + order: PropTypes.object, + updateSort: PropTypes.func +}; + +export default class AccountLandingResultsSection extends React.Component { + render() { + let message = null; + let table = ( + + ); + if (this.props.inFlight) { + message = ( +
+ +
+ ); + } + else if (this.props.error) { + table = null; + message = ( +
+ +
+ ); + } + + return ( +
+ + {message} + + {table} +
+ ); + } +} + +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..238e3d473e --- /dev/null +++ b/src/js/components/accountLanding/AccountLandingSearchBar.jsx @@ -0,0 +1,43 @@ +/** + * 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/table/AccountLandingTable.jsx b/src/js/components/accountLanding/table/AccountLandingTable.jsx new file mode 100644 index 0000000000..29b482afe1 --- /dev/null +++ b/src/js/components/accountLanding/table/AccountLandingTable.jsx @@ -0,0 +1,59 @@ +/** + * AccountLandingTable.jsx + * Created by Lizzie Salita 8/4/17 + */ + +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'; + +const propTypes = { + results: PropTypes.array, + columns: PropTypes.array, + accountSearchString: PropTypes.string, + order: PropTypes.object, + updateSort: PropTypes.func +}; + +export default class AccountLandingTable extends React.PureComponent { + render() { + const rows = this.props.results.map((account, index) => ( + + )); + + const headers = this.props.columns.map((column, index) => ( + + + + )); + + return ( + + + + {headers} + + + + {rows} + +
+ ); + } +} + +AccountLandingTable.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..5b1dc4fe02 --- /dev/null +++ b/src/js/components/accountLanding/table/TableRow.jsx @@ -0,0 +1,74 @@ +/** + * TableRow.jsx + * Created by Lizzie Salita 8/4/17 + **/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import GenericCell from 'components/agencyLanding/table/cells/GenericCell'; +import AccountLinkCell from './cells/AccountLinkCell'; +import HighlightedCell from './cells/HighlightedCell'; + +const propTypes = { + columns: PropTypes.array.isRequired, + account: PropTypes.object, + rowIndex: PropTypes.number.isRequired, + accountSearchString: PropTypes.string +}; + +export default class TableRow extends React.PureComponent { + render() { + let rowClass = ''; + if (this.props.rowIndex % 2 === 0) { + rowClass = 'results-table__data_even'; + } + const cells = this.props.columns.map((column) => { + if (column.columnName === 'accountName') { + // show the account link cell + return ( + + + + ); + } + else if (column.columnName === 'budgetaryResources') { + 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..226a9b688f --- /dev/null +++ b/src/js/components/accountLanding/table/cells/AccountLinkCell.jsx @@ -0,0 +1,44 @@ +/** + * 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() { + 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/components/accountLanding/table/cells/HighlightedCell.jsx b/src/js/components/accountLanding/table/cells/HighlightedCell.jsx new file mode 100644 index 0000000000..a1fc6ebb62 --- /dev/null +++ b/src/js/components/accountLanding/table/cells/HighlightedCell.jsx @@ -0,0 +1,40 @@ +/** + * HighlightedCell.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, + searchString: PropTypes.string +}; + +export default class HighlightedCell extends React.Component { + render() { + let data = this.props.data; + // highlight the matched string if applicable + if (this.props.searchString) { + data = reactStringReplace(this.props.data, this.props.searchString, (match, i) => ( + + {match} + + )); + } + return ( +
+
+ {data} +
+
+ ); + } +} + +HighlightedCell.propTypes = propTypes; 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/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/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 fbf96048cf..dd9495ac5e 100644 --- a/src/js/components/agencyLanding/AgencyLandingSearchBar.jsx +++ b/src/js/components/agencyLanding/AgencyLandingSearchBar.jsx @@ -20,17 +20,19 @@ export default class AgencyLandingSearchBar extends React.Component { render() { return ( -
-
+
+
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..4a64dad750 100644 --- a/src/js/components/award/AwardInfo.jsx +++ b/src/js/components/award/AwardInfo.jsx @@ -56,7 +56,17 @@ export default class AwardInfo extends React.Component { + seeAdditional={this.seeAdditional} + type="contract" /> + ); + } + else if (type === 'unknown') { + amountsDetailsSection = ( + ); } else { @@ -73,7 +83,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..c155693a32 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'; @@ -42,7 +44,15 @@ export default class SummaryBar extends React.Component { const awardEnd = moment(award.period_of_performance_current_end_date, 'MM-DD-YYYY'); const current = moment(); let progress = ""; - const awardType = startCase(toLower(SummaryPageHelper.awardType(award.award_type))); + + 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; if (current.isSameOrBefore(awardStart, 'day')) { @@ -54,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; } @@ -78,12 +88,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/award/contract/AwardContract.jsx b/src/js/components/award/contract/AwardContract.jsx index 2693d29318..30dee74a55 100644 --- a/src/js/components/award/contract/AwardContract.jsx +++ b/src/js/components/award/contract/AwardContract.jsx @@ -5,18 +5,41 @@ 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"; + 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]; + } + + 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..9cf9bdbd92 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; - } + const awardType = this.props.awardType; // Pricing let pricing = "Not Available"; 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/components/bulkDownload/BulkDownloadPage.jsx b/src/js/components/bulkDownload/BulkDownloadPage.jsx index 9e945ad412..44f4bfe956 100644 --- a/src/js/components/bulkDownload/BulkDownloadPage.jsx +++ b/src/js/components/bulkDownload/BulkDownloadPage.jsx @@ -7,10 +7,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import { downloadPageMetaTags } from 'helpers/metaTagHelper'; +import { downloadOptions } from 'dataMapping/navigation/menuOptions'; 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'; @@ -19,33 +21,6 @@ import BulkDownloadModalContainer from 'containers/bulkDownload/modal/BulkDownloadModalContainer'; import BulkDownloadSidebar from './sidebar/BulkDownloadSidebar'; -export const dataTypes = [ - { - type: 'awards', - label: 'Award Data', - disabled: false, - url: '' - }, - { - type: 'accounts', - label: 'Account Data', - disabled: true, - url: '' - }, - { - type: '', - label: 'Agency Submission Files', - disabled: false, - url: 'http://usaspending-submissions.s3-website-us-gov-west-1.amazonaws.com/' - }, - { - type: 'snapshots', - label: 'Database Snapshots', - disabled: true, - url: '' - } -]; - const propTypes = { setDataType: PropTypes.func, dataType: PropTypes.string, @@ -111,34 +86,35 @@ export default class BulkDownloadPage extends React.Component {
-
-
-

- Bulk Download + +
+

+ Download Center

-

+
-
- -
-
Interested in our API?
-

- Take a look at our API documentation. -

+ id="main-content"> +
+
+ +
+
Interested in our API?
+

+ Take a look at our API documentation. +

+
+
+ {downloadDataContent} +
+
-
- {downloadDataContent} -
-
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/awards/AwardDataContent.jsx b/src/js/components/bulkDownload/awards/AwardDataContent.jsx index 406ed5ea87..6910c5618f 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/components/bulkDownload/awards/filters/dateRange/DownloadDateRange.jsx b/src/js/components/bulkDownload/awards/filters/dateRange/DownloadDateRange.jsx index 44c3cb9f39..cf7be517c0 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,11 +43,48 @@ export default class DownloadDateRange extends React.Component { } } + generateStartDateDisabledDays() { + const 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() { + const 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; }} allowClearing /> { 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 1bf40620eb..ba28ef3502 100644 --- a/src/js/components/bulkDownload/awards/filters/dateRange/TimePeriodFilter.jsx +++ b/src/js/components/bulkDownload/awards/filters/dateRange/TimePeriodFilter.jsx @@ -21,13 +21,24 @@ 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: 'Choose one of the ranges below or set your own range of one year or less.' + } +}; + export default class TimePeriodFilter extends React.Component { constructor(props) { super(props); this.state = { - startDateUI: null, - endDateUI: null, + startDateBulkUI: null, + endDateBulkUI: null, showError: false, header: '', description: '', @@ -56,8 +67,8 @@ export default class TimePeriodFilter extends React.Component { if (startDate.isValid() && endDate.isValid()) { this.setState({ - startDateUI: startDate, - endDateUI: endDate + startDateBulkUI: startDate, + endDateBulkUI: endDate }); } } @@ -74,12 +85,12 @@ export default class TimePeriodFilter extends React.Component { // start date did change and it is a valid date (not null) if (startDate.isValid()) { datesChanged = true; - newState.startDateUI = startDate; + newState.startDateBulkUI = startDate; } else { // value became null datesChanged = true; - newState.startDateUI = null; + newState.startDateBulkUI = null; } } @@ -89,12 +100,12 @@ export default class TimePeriodFilter extends React.Component { if (endDate.isValid()) { // end date did change and it is a valid date (not null) datesChanged = true; - newState.endDateUI = endDate; + newState.endDateBulkUI = endDate; } else if (this.props.filterTimePeriodEnd) { // value became null datesChanged = true; - newState.endDateUI = null; + newState.endDateBulkUI = null; } } @@ -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.startDateUI; - const end = this.state.endDateUI; + const start = this.state.startDateBulkUI; + const end = this.state.endDateBulkUI; + + const yearBeforeEnd = moment(this.state.endDateBulkUI).subtract(1, 'y'); + 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! @@ -204,15 +224,14 @@ export default class TimePeriodFilter extends React.Component { } let start = ''; - if (this.state.startDateUI !== null) { - start = this.state.startDateUI.format('YYYY-MM-DD'); + if (this.state.startDateBulkUI !== null) { + start = this.state.startDateBulkUI.format('YYYY-MM-DD'); } let end = ''; - if (this.state.endDateUI !== null) { - end = this.state.endDateUI.format('YYYY-MM-DD'); + if (this.state.endDateBulkUI !== null) { + end = this.state.endDateBulkUI.format('YYYY-MM-DD'); } - return (
@@ -221,8 +240,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/bulkDownload/sidebar/BulkDownloadSidebar.jsx b/src/js/components/bulkDownload/sidebar/BulkDownloadSidebar.jsx index ae7b2feb4b..af63b012ad 100644 --- a/src/js/components/bulkDownload/sidebar/BulkDownloadSidebar.jsx +++ b/src/js/components/bulkDownload/sidebar/BulkDownloadSidebar.jsx @@ -18,12 +18,13 @@ export default class BulkDownloadSidebar extends React.Component { render() { const items = this.props.dataTypes.map((type) => ( )); diff --git a/src/js/components/bulkDownload/sidebar/SidebarButton.jsx b/src/js/components/bulkDownload/sidebar/SidebarButton.jsx index de931b77cd..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, @@ -12,6 +13,7 @@ const propTypes = { active: PropTypes.string, url: PropTypes.string, changeDataType: PropTypes.func, + newTab: PropTypes.bool, disabled: PropTypes.bool }; @@ -20,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) { @@ -50,12 +60,13 @@ export default class SidebarButton extends React.Component {
); } - else if (this.props.url !== '') { + else if (this.props.url !== '' && this.props.newTab) { button = ( + rel="noopener noreferrer" + onClick={this.logExternalLink}> {this.props.label} ); diff --git a/src/js/components/explorer/ExplorerWrapperPage.jsx b/src/js/components/explorer/ExplorerWrapperPage.jsx index 0f66736eb0..6fc1eb9f4d 100644 --- a/src/js/components/explorer/ExplorerWrapperPage.jsx +++ b/src/js/components/explorer/ExplorerWrapperPage.jsx @@ -10,6 +10,7 @@ import { explorerPageMetaTags } 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'; const propTypes = { @@ -20,13 +21,13 @@ const ExplorerWrapperPage = (props) => (
-
-
-

+ +
+

Spending Explorer

-

+
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/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/keyword/KeywordPage.jsx b/src/js/components/keyword/KeywordPage.jsx index 3dcfa84374..d6d2b3872d 100644 --- a/src/js/components/keyword/KeywordPage.jsx +++ b/src/js/components/keyword/KeywordPage.jsx @@ -6,18 +6,22 @@ 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'; 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'; @@ -71,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() { @@ -85,37 +93,102 @@ 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} +
+ +
+
+
-
-
+
-
+
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..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) { @@ -38,33 +59,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/header/KeywordHeader.jsx b/src/js/components/keyword/header/KeywordHeader.jsx deleted file mode 100644 index 01892d3eb0..0000000000 --- a/src/js/components/keyword/header/KeywordHeader.jsx +++ /dev/null @@ -1,105 +0,0 @@ -/** - * KeywordHeader.jsx - * Created by Lizzie Salita 1/4/18 - */ - -import React from 'react'; -import PropTypes from 'prop-types'; - -import * as MoneyFormatter from 'helpers/moneyFormatter'; -import DownloadButton from 'components/search/header/DownloadButton'; - -const propTypes = { - summary: PropTypes.object, - inFlight: PropTypes.bool, - clickedDownload: PropTypes.func, - downloadAvailable: PropTypes.bool, - keyword: PropTypes.string -}; - -export class KeywordHeader extends React.Component { - constructor(props) { - super(props); - - this.generateSummary = this.generateSummary.bind(this); - } - - generateSummary() { - let formattedPrimeCount = ( — ); - 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/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 ( -
+
+ +
+

+ Advanced Search +

+
+
+ +
+
-
{ fullSidebar } 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/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 ed230ca485..03107c25d4 100644 --- a/src/js/components/search/filters/awardAmount/SpecificAwardAmountItem.jsx +++ b/src/js/components/search/filters/awardAmount/SpecificAwardAmountItem.jsx @@ -15,17 +15,7 @@ const propTypes = { searchSpecificRange: PropTypes.func }; -const ga = require('react-ga'); - export default class SpecificAwardAmountItem extends React.Component { - static logAmountRangeEvent(range) { - ga.event({ - category: 'Search Page Filter Applied', - action: 'Applied Award Amount Range Filter', - label: range - }); - } - constructor(props) { super(props); @@ -70,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/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/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 c72d880c8f..00e66d460b 100644 --- a/src/js/components/search/filters/timePeriod/AllFiscalYears.jsx +++ b/src/js/components/search/filters/timePeriod/AllFiscalYears.jsx @@ -15,17 +15,7 @@ const propTypes = { updateFilter: PropTypes.func }; -const ga = require('react-ga'); - export default class AllFiscalYears extends React.Component { - static logFYEvent(year) { - ga.event({ - category: 'Search Page Filter Applied', - 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/DateRange.jsx b/src/js/components/search/filters/timePeriod/DateRange.jsx index 7ddbd73666..745a4443f8 100644 --- a/src/js/components/search/filters/timePeriod/DateRange.jsx +++ b/src/js/components/search/filters/timePeriod/DateRange.jsx @@ -69,11 +69,42 @@ 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) + const 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) { + const 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 +150,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 +163,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/search/filters/timePeriod/TimePeriod.jsx b/src/js/components/search/filters/timePeriod/TimePeriod.jsx index bd18684bb4..085aac4bf5 100644 --- a/src/js/components/search/filters/timePeriod/TimePeriod.jsx +++ b/src/js/components/search/filters/timePeriod/TimePeriod.jsx @@ -33,25 +33,7 @@ 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}`; - if (!start) { - label = `Through ${end}`; - } - else if (!end) { - label = `On or after ${start}`; - } - - ga.event({ - label, - category: 'Search Page Filter Applied', - 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/search/header/DownloadButton.jsx b/src/js/components/search/header/DownloadButton.jsx index e5b53184e7..599b76b621 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, + disableHover: PropTypes.bool }; 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.disableHover) { hover = (); } diff --git a/src/js/components/search/header/NoDownloadHover.jsx b/src/js/components/search/header/NoDownloadHover.jsx index 937688d6df..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 Bulk Download 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.
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/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/components/search/visualizations/VisualizationWrapper.jsx b/src/js/components/search/visualizations/VisualizationWrapper.jsx index 12872fedac..2aea4905ed 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'; @@ -50,12 +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 + }, () => { + this.logVisualizationTab(tab); }); } diff --git a/src/js/components/sharedComponents/DatePicker.jsx b/src/js/components/sharedComponents/DatePicker.jsx index 80e5a4ee2e..5b6a4ae3b0 100644 --- a/src/js/components/sharedComponents/DatePicker.jsx +++ b/src/js/components/sharedComponents/DatePicker.jsx @@ -212,25 +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.value) { - disabledDays = []; - } - const inputId = `picker-${uniqueId()}`; return ( @@ -263,8 +244,8 @@ export default class DatePicker extends React.Component { ref={(daypicker) => { this.datepicker = daypicker; }} - initialMonth={pickedDay} - disabledDays={disabledDays} + month={pickedDay} + disabledDays={this.props.disabledDays} selectedDays={(day) => DateUtils.isSameDay(pickedDay, day)} onDayClick={this.handleDatePick} onFocus={this.handleDateFocus} 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/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} ); diff --git a/src/js/components/sharedComponents/Pagination.jsx b/src/js/components/sharedComponents/Pagination.jsx index ad06f78b18..19dcac2799 100644 --- a/src/js/components/sharedComponents/Pagination.jsx +++ b/src/js/components/sharedComponents/Pagination.jsx @@ -34,16 +34,24 @@ export default class Pagination extends React.Component { let startPage; let endPage; - let prevEllipses = (...); - let nextEllipses = (...); + let prevEllipses = (...); + let nextEllipses = (...); let firstButton = ( -
  • - +
  • +
  • ); let lastButton = ( -
  • - +
  • +
  • ); if (totalPages < 5) { @@ -68,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)) { @@ -79,7 +87,7 @@ export default class Pagination extends React.Component { endPage = currentPage; } else if (currentPage === (totalPages - 2)) { - startPage = currentPage; + startPage = currentPage - 1; endPage = totalPages; } } @@ -117,8 +125,14 @@ export default class Pagination extends React.Component { generatePageButtons(pages, totalPages) { return (pages.map((page, index) => ( -
  • - +
  • +
  • ) )); @@ -142,20 +156,28 @@ export default class Pagination extends React.Component { return (
    -
    +
    {resultsText}
      -
    • - +
    • +
    • {pager.firstButton} {pager.prevEllipses} {pageButtons} {pager.nextEllipses} {pager.lastButton} -
    • - +
    • +
    diff --git a/src/js/components/sharedComponents/checkbox/PrimaryCheckboxType.jsx b/src/js/components/sharedComponents/checkbox/PrimaryCheckboxType.jsx index 8ac465f22e..e290a11b70 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,20 +36,18 @@ const defaultProps = { enableAnalytics: false }; -const ga = require('react-ga'); - export default class PrimaryCheckboxType extends React.Component { static logPrimaryTypeFilterEvent(type, filter) { - ga.event({ - category: 'Search Page Filter Applied', + Analytics.event({ + category: 'Search Filter Interaction', action: `Selected ${filter} Type`, label: type }); } static logDeselectFilterEvent(type, filter) { - ga.event({ - category: 'Search Page Filter Applied', + Analytics.event({ + 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 2d50b82233..94edd1b348 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,20 +26,18 @@ const defaultProps = { enableAnalytics: false }; -const ga = require('react-ga'); - export default class SecondaryCheckboxType extends React.Component { static logSecondaryTypeFilterEvent(type, filter) { - ga.event({ - category: 'Search Page Filter Applied', + Analytics.event({ + category: 'Search Filter Interaction', action: `Selected Secondary ${filter} Type`, label: type }); } static logDeselectFilterEvent(type, filter) { - ga.event({ - category: 'Search Page Filter Applied', + Analytics.event({ + 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 893063885b..eda11c240e 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,20 +25,18 @@ const defaultProps = { enableAnalytics: false }; -const ga = require('react-ga'); - export default class SingleCheckboxType extends React.Component { static logSingleTypeFilterEvent(type, filter) { - ga.event({ - category: 'Search Page Filter Applied', + Analytics.event({ + category: 'Search Filter Interaction', action: `Selected ${filter} Type`, label: type }); } static logDeselectSingleTypeFilterEvent(type, filter) { - ga.event({ - category: 'Search Page Filter Applied', + Analytics.event({ + category: 'Search Filter Interaction', 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..1b2caa3adc 100644 --- a/src/js/components/sharedComponents/filterSidebar/FilterOption.jsx +++ b/src/js/components/sharedComponents/filterSidebar/FilterOption.jsx @@ -22,17 +22,7 @@ const defaultProps = { defaultExpand: true }; -const ga = require('react-ga'); - export default class FilterOption extends React.Component { - static logFilterEvent(name) { - ga.event({ - category: 'Search Filters', - action: 'Expanded Filter', - label: name - }); - } - constructor(props) { super(props); @@ -87,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/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/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/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..885d34ab45 100644 --- a/src/js/components/search/header/SearchHeader.jsx +++ b/src/js/components/sharedComponents/stickyHeader/StickyHeader.jsx @@ -1,19 +1,20 @@ /** - * 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 const stickyHeaderHeight = 66; + +export default class StickyHeader extends React.Component { constructor(props) { super(props); @@ -65,33 +66,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 +91,4 @@ export default class SearchHeader extends React.Component { } } -SearchHeader.propTypes = propTypes; +StickyHeader.propTypes = propTypes; diff --git a/src/js/containers/account/AccountContainer.jsx b/src/js/containers/account/AccountContainer.jsx index 1b4e96f476..c81f5c71ba 100644 --- a/src/js/containers/account/AccountContainer.jsx +++ b/src/js/containers/account/AccountContainer.jsx @@ -8,7 +8,6 @@ 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'; @@ -16,7 +15,7 @@ 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 +45,7 @@ export class AccountContainer extends React.Component { }; this.accountRequest = null; - this.balanceRequests = []; + this.fiscalYearSnapshotRequest = null; } componentDidMount() { @@ -78,7 +77,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 +103,47 @@ 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; + const currentFiscalYear = FiscalYearHelper.defaultFiscalYear(); - requests.push(request); - promises.push(request.promise); - }); - - this.balanceRequests = requests; + this.fiscalYearSnapshotRequest = AccountHelper.fetchFederalAccountFYSnapshot( + id, + currentFiscalYear + ); - 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/containers/accountLanding/AccountLandingContainer.jsx b/src/js/containers/accountLanding/AccountLandingContainer.jsx new file mode 100644 index 0000000000..e62273d88c --- /dev/null +++ b/src/js/containers/accountLanding/AccountLandingContainer.jsx @@ -0,0 +1,195 @@ +/** + * AccountLandingContainer.jsx + * Created by Lizzie Salita 8/4/17 + */ + +import React from 'react'; +import { isCancel } from 'axios'; +import { inRange } from 'lodash'; + +import AccountsTableFields from 'dataMapping/accountLanding/accountsTableFields'; +import * as AccountLandingHelper from 'helpers/accountLandingHelper'; +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 { + constructor(props) { + super(props); + + this.state = { + pageNumber: 1, + order: { + field: 'budgetary_resources', + direction: 'desc' + }, + columns: [], + inFlight: false, + error: false, + searchString: '', + results: [], + totalItems: 0, + pageSize: 50 + }; + + this.accountsRequest = null; + this.setAccountSearchString = this.setAccountSearchString.bind(this); + this.onChangePage = this.onChangePage.bind(this); + this.updateSort = this.updateSort.bind(this); + } + + componentDidMount() { + this.showColumns(); + } + + componentWillUnmount() { + if (this.accountsRequest) { + this.accountsRequest.cancel(); + } + } + + onChangePage(pageNumber) { + 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) { + // 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 + // Reset the page number to 1 + this.setState({ + order: { + field, + direction + }, + pageNumber: 1 + }, () => { + this.fetchAccounts(); + }); + } + + showColumns() { + const columns = []; + const sortOrder = AccountsTableFields.defaultSortDirection; + + AccountsTableFields.order.forEach((col) => { + let displayName = AccountsTableFields[col]; + if (col === 'budgetaryResources') { + // Add default fiscal year to Budgetary Resources column header + const fy = FiscalYearHelper.defaultFiscalYear(); + displayName = `${fy} ${displayName}`; + } + 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, + error: false + }); + + // generate the params + const pageSize = 50; + const fy = `${FiscalYearHelper.defaultFiscalYear()}`; + const params = { + sort: this.state.order, + page: this.state.pageNumber, + limit: pageSize, + filters: { + fy + } + }; + + 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, + error: true + }); + console.log(err); + } + }); + } + + parseAccounts(data) { + const accounts = []; + + data.results.forEach((item) => { + const account = Object.create(BaseFederalAccountLandingRow); + account.parse(item); + accounts.push(account); + }); + + this.setState({ + totalItems: data.count, + results: accounts + }); + } + + render() { + return ( + + ); + } +} 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/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) { diff --git a/src/js/containers/keyword/KeywordContainer.jsx b/src/js/containers/keyword/KeywordContainer.jsx index a77bc3c6ba..dc231b09fd 100644 --- a/src/js/containers/keyword/KeywordContainer.jsx +++ b/src/js/containers/keyword/KeywordContainer.jsx @@ -9,6 +9,10 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { isCancel } from 'axios'; +import Router from 'containers/router/Router'; + +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'; @@ -18,6 +22,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 +48,35 @@ export class KeywordContainer extends React.Component { this.startDownload = this.startDownload.bind(this); } + componentWillMount() { + this.handleUrl(this.props.params.keyword); + } + + componentWillReceiveProps(nextProps) { + if (this.props.params.keyword !== nextProps.params.keyword) { + this.handleUrl(nextProps.params.keyword); + } + } + + handleUrl(urlKeyword) { + if (urlKeyword) { + // Convert the url to a keyword + const keyword = decodeURIComponent(urlKeyword); + // Update the keyword only if it has more than two characters + if (keyword.length > 2) { + this.setState({ + keyword + }); + } + } + else if (this.state.keyword) { + // The keyword param was removed from the url, reset the keyword + this.setState({ + keyword: '' + }); + } + } + startDownload() { const params = { award_levels: ['prime_awards'], @@ -139,8 +173,17 @@ export class KeywordContainer extends React.Component { } updateKeyword(keyword) { + // Convert the keyword to a url slug + const slug = encodeURIComponent(keyword); this.setState({ keyword + }, () => { + // update the url + Router.history.replace(`/keyword_search/${slug}`); + 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..830dd701e7 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 = { @@ -70,6 +72,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 @@ -238,6 +248,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/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/router/RouterRoutes.jsx b/src/js/containers/router/RouterRoutes.jsx index 8a2f981a0b..640869e693 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', @@ -131,11 +140,31 @@ const routes = { { path: '/keyword_search', parent: '/keyword_search', + silentlyUpdate: true, component: (cb) => { require.ensure([], (require) => { cb(require('containers/keyword/KeywordContainer').default); }); } + }, + { + path: '/keyword_search/:keyword', + parent: '/keyword_search', + silentlyUpdate: true, + component: (cb) => { + require.ensure([], (require) => { + cb(require('containers/keyword/KeywordContainer').default); + }); + } + }, + { + path: '/federal_account', + parent: '/federal_account', + component: (cb) => { + require.ensure([], (require) => { + cb(require('components/accountLanding/AccountLandingPage').default); + }); + } } ], notFound: { diff --git a/src/js/containers/search/SearchContainer.jsx b/src/js/containers/search/SearchContainer.jsx index e12e93df37..4d54ca113b 100644 --- a/src/js/containers/search/SearchContainer.jsx +++ b/src/js/containers/search/SearchContainer.jsx @@ -29,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 = { @@ -232,6 +239,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 043809f706..d493e9f4e4 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,12 @@ import { clearAllFilters as clearStagedFilters } from 'redux/actions/search/sear import SearchSidebarSubmit from 'components/search/SearchSidebarSubmit'; +import { + convertFiltersToAnalyticEvents, + sendAnalyticEvents, + sendFieldCombinations +} from './helpers/searchAnalytics'; + const combinedActions = Object.assign({}, appliedFilterActions, { clearStagedFilters }); @@ -86,6 +91,10 @@ export class SearchSidebarSubmitContainer extends React.Component { this.setState({ filtersChanged: false }); + + 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 6d3e338050..0bced4da32 100644 --- a/src/js/containers/search/filters/AgencyContainer.jsx +++ b/src/js/containers/search/filters/AgencyContainer.jsx @@ -22,17 +22,7 @@ const propTypes = { appliedAwardingAgencies: PropTypes.object }; -const ga = require('react-ga'); - export class AgencyContainer extends React.Component { - static logAgencyFilterEvent(agencyType, agency) { - ga.event({ - category: 'Search Page Filter Applied', - 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 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/filters/awardID/AwardIDSearchContainer.jsx b/src/js/containers/search/filters/awardID/AwardIDSearchContainer.jsx index 8e4346f52d..bbe96b0fc5 100644 --- a/src/js/containers/search/filters/awardID/AwardIDSearchContainer.jsx +++ b/src/js/containers/search/filters/awardID/AwardIDSearchContainer.jsx @@ -19,17 +19,7 @@ const propTypes = { updateGenericFilter: PropTypes.func }; -const ga = require('react-ga'); - export class AwardIDSearchContainer extends React.Component { - static logIdEvent(id, type) { - ga.event({ - category: 'Search Page Filter Applied', - 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 faa2c11136..d57ac11e1f 100644 --- a/src/js/containers/search/filters/cfda/CFDASearchContainer.jsx +++ b/src/js/containers/search/filters/cfda/CFDASearchContainer.jsx @@ -19,17 +19,7 @@ const propTypes = { updateSelectedCFDA: PropTypes.func }; -const ga = require('react-ga'); - export class CFDASearchContainer extends React.Component { - static logCFDAFilterEvent(place) { - ga.event({ - category: 'Search Page Filter Applied', - 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 d3913cee79..6f2c3a0e5a 100644 --- a/src/js/containers/search/filters/location/POPFilterContainer.jsx +++ b/src/js/containers/search/filters/location/POPFilterContainer.jsx @@ -13,8 +13,6 @@ 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, @@ -22,14 +20,6 @@ const propTypes = { }; export class POPFilterContainer extends React.Component { - static logLocationFilterEvent(label, event) { - ga.event({ - label, - category: 'Search Page Filter Applied', - 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 a9ccc0b6ed..6c563b8fc8 100644 --- a/src/js/containers/search/filters/location/RecipientFilterContainer.jsx +++ b/src/js/containers/search/filters/location/RecipientFilterContainer.jsx @@ -13,8 +13,6 @@ 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, @@ -22,14 +20,6 @@ const propTypes = { }; export class RecipientFilterContainer extends React.Component { - static logLocationFilterEvent(label, event) { - ga.event({ - label, - category: 'Search Page Filter Applied', - 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 1f68b06752..a0a276d7ec 100644 --- a/src/js/containers/search/filters/naics/NAICSSearchContainer.jsx +++ b/src/js/containers/search/filters/naics/NAICSSearchContainer.jsx @@ -19,17 +19,7 @@ const propTypes = { appliedNAICS: PropTypes.object }; -const ga = require('react-ga'); - export class NAICSSearchContainer extends React.Component { - static logPlaceFilterEvent(naics) { - ga.event({ - category: 'Search Page Filter Applied', - 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 3eeb2ce2b3..861b836275 100644 --- a/src/js/containers/search/filters/psc/PSCSearchContainer.jsx +++ b/src/js/containers/search/filters/psc/PSCSearchContainer.jsx @@ -19,17 +19,7 @@ const propTypes = { appliedPSC: PropTypes.object }; -const ga = require('react-ga'); - export class PSCSearchContainer extends React.Component { - static logPSCFilterEvent(psc) { - ga.event({ - category: 'Search Page Filter Applied', - 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 c70e8c0d62..26783317ab 100644 --- a/src/js/containers/search/filters/recipient/RecipientSearchContainer.jsx +++ b/src/js/containers/search/filters/recipient/RecipientSearchContainer.jsx @@ -19,17 +19,7 @@ const propTypes = { appliedRecipients: PropTypes.object }; -const ga = require('react-ga'); - export class RecipientSearchContainer extends React.Component { - static logRecipientFilterEvent(name) { - ga.event({ - category: 'Search Page Filter Applied', - 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 01a26e0b7d..7e408d9792 100644 --- a/src/js/containers/search/filters/recipient/RecipientTypeContainer.jsx +++ b/src/js/containers/search/filters/recipient/RecipientTypeContainer.jsx @@ -24,17 +24,7 @@ const propTypes = { appliedType: PropTypes.object }; -const ga = require('react-ga'); - export class RecipientTypeContainer extends React.Component { - static logLocationFilterEvent(placeType, place, event) { - ga.event({ - category: 'Search Page Filter Applied', - 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 new file mode 100644 index 0000000000..6191c39dd1 --- /dev/null +++ b/src/js/containers/search/helpers/searchAnalytics.js @@ -0,0 +1,265 @@ +/** + * searchAnalytics.js + * Created by Kevin Li 2/2/18 + */ + +import { Set } from 'immutable'; +import { uniq } from 'lodash'; +import { + awardTypeCodes, + awardTypeGroups, + awardTypeGroupLabels +} 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 = 'Advanced Search - Search Filter'; + +export 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}` + }]; +}; + +export 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; +}; + + +export const convertReducibleValue = (value, type, parser) => ( + value.reduce((events, item) => { + events.push({ + action: type, + label: (parser && parser(item)) || item + }); + return events; + }, []) +); + +export const convertTimePeriod = (value) => { + if (Set.isSet(value)) { + return convertReducibleValue( + value, + 'Time Period - Fiscal Year' + ); + } + else if (Array.isArray(value)) { + return convertDateRange(value); + } + return null; +}; + +export const convertAgency = (agencies, type) => ( + convertReducibleValue( + agencies, + type, + parseAgency + ) +); + + +export const convertLocation = (locations, type) => ( + convertReducibleValue( + locations, + type, + (location) => `${location.display.entity} - ${location.display.standalone}` + ) +); + +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( + combineAwardTypeGroups(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; + } +}; + +export 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) => { + events.forEach((event) => { + Analytics.event(Object.assign({}, event, { + category: eventCategory + })); + }); +}; + +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: 'Advanced Search - Search Fields', + 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..fb59303e4a 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 { } } }); + + // 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, + label: uniqueFilterFields(filters) + }); } checkStatus() { diff --git a/src/js/containers/search/table/ResultsTableContainer.jsx b/src/js/containers/search/table/ResultsTableContainer.jsx index c3a6a3c766..69d464a871 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,23 +64,7 @@ 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 - const currentType = tableTypes.filter((type) => - type.internal === tableType - ); - const typeLabel = currentType[0].label; - - ga.event({ - category: 'Search Page Spending By Award', - action: `Scrolled to next page of ${typeLabel}`, - label: page - }); - } - constructor(props) { super(props); @@ -400,6 +385,10 @@ export class ResultsTableContainer extends React.Component { this.setState(newState, () => { this.performSearch(true); + Analytics.event({ + category: 'Advanced Search - Table Tab', + action: tab + }); }); } @@ -412,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/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/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/src/js/dataMapping/accountLanding/accountsTableFields.js b/src/js/dataMapping/accountLanding/accountsTableFields.js new file mode 100644 index 0000000000..d8a3c54a49 --- /dev/null +++ b/src/js/dataMapping/accountLanding/accountsTableFields.js @@ -0,0 +1,26 @@ +const accountsTableFields = { + defaultSortDirection: { + accountNumber: 'desc', + accountName: 'asc', + managingAgency: 'asc', + budgetaryResources: 'desc' + }, + modelMapping: { + accountNumber: 'account_number', + accountName: 'account_name', + managingAgency: 'managing_agency', + budgetaryResources: 'budgetary_resources' + }, + order: [ + 'accountNumber', + 'accountName', + 'managingAgency', + 'budgetaryResources' + ], + accountNumber: 'Account Number', + accountName: 'Account Name', + managingAgency: 'Managing Agency', + budgetaryResources: 'Budgetary Resources' +}; + +export default accountsTableFields; 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/dataMapping/bulkDownload/bulkDownloadOptions.js b/src/js/dataMapping/bulkDownload/bulkDownloadOptions.js index b69bfb896e..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'), @@ -122,11 +127,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') } ] } 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 */ diff --git a/src/js/dataMapping/navigation/menuOptions.js b/src/js/dataMapping/navigation/menuOptions.js index e29a17c4fa..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', @@ -39,30 +39,37 @@ export const profileOptions = [ export const downloadOptions = [ { label: 'Award Data Archive', + type: 'award_data_archive', url: '#/bulk_download/award_data_archive', code: 'archive', description: 'The quickest way to grab award data. Pre-generated award files for each major agency (by fiscal year) save on download time.', callToAction: 'Grab Award Files', + newTab: false, enabled: true }, { label: 'Custom Award Data', + type: 'awards', url: '#/bulk_download', code: 'award', description: 'The best way to grab detailed slices of award data. Specify the agency, timeframe, award type, award level, and more.', callToAction: 'Download Award Data', + newTab: false, enabled: true }, { label: 'Custom Account Data', + type: 'accounts', url: '#/bulk_download/account', code: 'account', description: 'The best way to grab detailed subsets of account data, which offer a broad view of how the government allocates funding from top to bottom.', callToAction: 'Download Account Data', + newTab: false, enabled: false }, { label: 'Agency Submission Files', + type: 'snapshots', url: 'http://usaspending-submissions.s3-website-us-gov-west-1.amazonaws.com/', code: 'submission', description: 'Raw, unadulterated data submitted by federal agencies in compliance with the DATA Act.', @@ -72,6 +79,7 @@ export const downloadOptions = [ }, { label: 'Database Snapshots', + type: '', url: 'https://aws.amazon.com/public-datasets/usaspending/', code: 'database', description: 'Our entire database as a PostgreSQL snapshot \u2014 the most complete download option available for advanced users.', @@ -81,6 +89,7 @@ export const downloadOptions = [ }, { label: 'API', + type: '', url: 'https://api.usaspending.gov', code: 'api', description: 'An automated way for advanced users to access all the data behind USAspending.gov. Accessible documentation includes tutorials, best practices, and more.', 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/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/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/accountHelper.js b/src/js/helpers/accountHelper.js index acfccc308c..fb7f182d5d 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, fy) => { + const source = CancelToken.source(); + return { + promise: Axios.request({ + url: `v2/federal_accounts/${id}/fiscal_year_snapshot/${fy}`, + 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/helpers/accountLandingHelper.js b/src/js/helpers/accountLandingHelper.js new file mode 100644 index 0000000000..de249409be --- /dev/null +++ b/src/js/helpers/accountLandingHelper.js @@ -0,0 +1,24 @@ +/** + * accountLandingHelper.js + * Created by Lizzie Salita 8/4/17 + **/ + +import Axios, { CancelToken } from 'axios'; +import kGlobalConstants from 'GlobalConstants'; + +export const fetchAllAccounts = (data) => { + const source = CancelToken.source(); + return { + promise: Axios.request({ + url: 'v2/federal_accounts/', + baseURL: kGlobalConstants.API, + method: 'post', + data, + cancelToken: source.token + }), + cancel() { + source.cancel(); + } + }; +}; + diff --git a/src/js/helpers/analytics/Analytics.js b/src/js/helpers/analytics/Analytics.js new file mode 100644 index 0000000000..6b9c44178b --- /dev/null +++ b/src/js/helpers/analytics/Analytics.js @@ -0,0 +1,57 @@ +/** + * Analytics.js + * Created by Kevin Li 2/2/18 + */ + + +const Analytics = { + _prefix: 'USAspending - ', + _execute(...args) { + if (this.isDAP) { + return window.gas(...args); + } + 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._execute( + 'send', + 'event', + `${this._prefix}${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._execute( + 'send', + 'pageview', + path, + title + ); + } +}; + +// no hack approaches allowed +Object.freeze(Analytics); + +export default Analytics; 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/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 } ]; 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/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/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)); 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/account/accountReducer.js b/src/js/redux/reducers/account/accountReducer.js index 65f2ff5784..f4a8d86cd2 100644 --- a/src/js/redux/reducers/account/accountReducer.js +++ b/src/js/redux/reducers/account/accountReducer.js @@ -32,14 +32,13 @@ export const initialState = { title: '', description: '', totals: { - obligated: {}, - unobligated: {}, - budgetAuthority: {}, - outlay: {}, - balanceBroughtForward1: {}, - balanceBroughtForward2: {}, - otherBudgetaryResources: {}, - appropriations: {} + obligated: 0, + unobligated: 0, + budgetAuthority: 0, + outlay: 0, + balanceBroughtForward: 0, + otherBudgetaryResources: 0, + appropriations: 0 } }, totalSpending: 0 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/account/AccountContainer-test.jsx b/tests/containers/account/AccountContainer-test.jsx index 72d8403a96..bdf1613cb4 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, 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,15 +112,18 @@ 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 = { - outlay: {}, - obligated: {}, - unobligated: {}, - budgetAuthority: {} + obligated: 0, + unobligated: 0, + budgetAuthority: 0, + outlay: 0, + balanceBroughtForward: 0, + otherBudgetaryResources: 0, + appropriations: 0 }; const reduxAction = jest.fn((args) => { @@ -135,35 +138,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 + } +}; diff --git a/tests/containers/accountLanding/AccountLandingContainer-test.jsx b/tests/containers/accountLanding/AccountLandingContainer-test.jsx new file mode 100644 index 0000000000..46509a9d9f --- /dev/null +++ b/tests/containers/accountLanding/AccountLandingContainer-test.jsx @@ -0,0 +1,116 @@ +/** + * AccountLandingContainer-test.jsx + * Created by Lizzie Salita 2/7/18 + */ + +import React from 'react'; +import { mount, shallow } 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', () => { + const container = shallow(); + + container.instance().showColumns(); + + // validate the state contains the correctly parsed values + const fy = FiscalYearHelper.defaultFiscalYear(); + const expectedState = [ + { + columnName: "accountNumber", + defaultDirection: "desc", + displayName: "Account Number" + }, + { + columnName: "accountName", + defaultDirection: "asc", + displayName: "Account Name" + }, + { + columnName: "managingAgency", + defaultDirection: "asc", + displayName: "Managing Agency" + }, + { + columnName: "budgetaryResources", + 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 = shallow(); + + container.instance().parseAccounts(mockData.data); + expect(container.state().results).toEqual(mockParsed); + }); + }); + + describe('updateSort', () => { + it('should update the container state and reset the page number to 1', () => { + const container = shallow(); + + // change the sort order + container.instance().updateSort('managing_agency', 'asc'); + + expect(container.state().order).toEqual({ + field: 'managing_agency', + direction: 'asc' + }); + + expect(container.state().pageNumber).toEqual(1); + }); + }); + + describe('onChangePage', () => { + 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); + + expect(container.state().pageNumber).toEqual(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); + }); + }); +}); 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..c0555846e6 --- /dev/null +++ b/tests/containers/accountLanding/mockFederalAccounts.js @@ -0,0 +1,59 @@ +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 = [ + { + _budgetaryResources: 5000000, + _managingAgency: "Mock Agency", + _managingAgencyAcronym: "XYZ", + accountId: 1, + accountName: "Mock Account", + accountNumber: "123-4567" + }, { + _budgetaryResources: 6500000, + _managingAgency: "Mock Agency 2", + _managingAgencyAcronym: "ABC", + accountId: 2, + accountName: "Mock Account 2", + accountNumber: "098-7654" + }, { + _budgetaryResources: 4500000, + _managingAgency: "Mock Agency 3", + _managingAgencyAcronym: "DEF", + accountId: 3, + accountName: "Test Account", + accountNumber: "234-5678" + } +]; \ No newline at end of file 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/keyword/KeywordContainer-test.jsx b/tests/containers/keyword/KeywordContainer-test.jsx index 9206106fd1..a3eed78183 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%20blah'); + }); + }); + + 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/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(), 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; 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/helpers/searchAnalytics-test.js b/tests/containers/search/helpers/searchAnalytics-test.js new file mode 100644 index 0000000000..fa3c5a0671 --- /dev/null +++ b/tests/containers/search/helpers/searchAnalytics-test.js @@ -0,0 +1,308 @@ +/** + * searchAnalytics-test.js + * Created by Kevin Li 2/5/18 + */ + +import { Set, OrderedMap } from 'immutable'; +import * as searchAnalytics from 'containers/search/helpers/searchAnalytics'; + +// mock the child component by replacing it with a function that returns a null element +jest.mock('helpers/analytics/Analytics', () => ({ + event: jest.fn(), + pageview: jest.fn() +})); + +describe('searchAnalytics', () => { + describe('convertDateRange', () => { + it('should parse a date range object from the Redux store into an Analytics event', () => { + const mockRange = ['1900-01-01', '1900-01-02']; + const event = searchAnalytics.convertDateRange(mockRange); + expect(event).toEqual([{ + action: 'Time Period - Date Range', + label: '1900-01-01 to 1900-01-02' + }]); + }); + it('should handle null start dates as an open-ended date range', () => { + const mockRange = [null, '1900-01-02']; + const event = searchAnalytics.convertDateRange(mockRange); + expect(event).toEqual([{ + action: 'Time Period - Date Range', + label: '... to 1900-01-02' + }]); + }); + it('should handle null end dates as date range ending on the present day', () => { + const mockRange = ['1900-01-01', null]; + const event = searchAnalytics.convertDateRange(mockRange); + expect(event).toEqual([{ + action: 'Time Period - Date Range', + label: '1900-01-01 to present' + }]); + }); + it('should ignore an incomplete date range', () => { + const mockRange = ['abc']; + const event = searchAnalytics.convertDateRange(mockRange); + expect(event).toBeFalsy(); + }); + }); + + describe('parseAgency', () => { + it('should return the top tier agency information for a top-tier agency', () => { + const data = { + agencyType: 'toptier', + toptier_agency: { + name: 'Test', + abbreviation: 'ABC', + cgac_code: '123' + } + }; + const agency = searchAnalytics.parseAgency(data); + expect(agency).toEqual('Test (ABC)/123'); + }); + it('should return the sub tier agency information for a sub-tier agency', () => { + const data = { + agencyType: 'subtier', + toptier_agency: { + name: 'Test', + abbreviation: 'ABC', + cgac_code: '123' + }, + subtier_agency: { + name: 'Sub', + subtier_code: '2222' + } + }; + const agency = searchAnalytics.parseAgency(data); + expect(agency).toEqual('Sub/2222 - Test/123'); + }); + it('should return the office information for an office agency', () => { + const data = { + agencyType: 'office', + toptier_agency: { + name: 'Test', + abbreviation: 'ABC', + cgac_code: '123' + }, + office_agency: { + name: 'Office', + aac_code: '333' + } + }; + const agency = searchAnalytics.parseAgency(data); + expect(agency).toEqual('Office/333 - Test/123'); + }); + it('should return null for an invalid or unexpected agency object structure', () => { + const bad = { + agency: { + name: 'Hello' + } + }; + const agency = searchAnalytics.parseAgency(bad); + expect(agency).toBeFalsy(); + }); + }); + + describe('convertReducibleValue', () => { + it('should return an array of objects of equal length to the inbound value count',() => { + const data = Set([1, 2, 3, 4]); + expect(data.count()).toEqual(4); + + const reduced = searchAnalytics.convertReducibleValue(data, 'action'); + expect(Array.isArray(reduced)).toBeTruthy(); + expect(reduced.length).toEqual(4); + }); + it('should return an array of objects with `action` and `label` properties', () => { + const data = [1, 2, 3, 4, 5]; + const reduced = searchAnalytics.convertReducibleValue(data, 'action'); + + expect(reduced.every( + (result) => ({}.hasOwnProperty.call(result, 'action') && {}.hasOwnProperty.call(result, 'label')) + )).toBeTruthy(); + }); + it('should return an array of objects with an `action` of the specified type', () => { + const data = [1, 2, 3, 4, 5]; + const reduced = searchAnalytics.convertReducibleValue(data, 'action'); + + expect(reduced.every( + (result) => result.action === 'action' + )).toBeTruthy(); + }); + it('should return an array of objects with a `label` value that is the result of the iterated value item passed through the parser function', () => { + const data = [1, 2, 3, 4, 5]; + const reduced = searchAnalytics.convertReducibleValue( + data, + 'action', + (item) => item * 2 + ); + + const expected = [2, 4, 6, 8, 10]; + expect(reduced.every( + (result, index) => result.label === expected[index] + )).toBeTruthy(); + }); + it('should return an array of objects with a `label` value equal to the iterated value item when no parser function is provided', () => { + const data = [1, 2, 3, 4, 5]; + const reduced = searchAnalytics.convertReducibleValue(data, 'action', undefined); + + const expected = [1, 2, 3, 4, 5]; + expect(reduced.every( + (result, index) => result.label === expected[index] + )).toBeTruthy(); + }); + }); + + describe('convertTimePeriod', () => { + it('should assume an Immutable Set is a fiscal year', () => { + const data = new Set(['1900']); + const converted = searchAnalytics.convertTimePeriod(data); + expect(Array.isArray(converted)).toBeTruthy(); + expect(converted[0].action).toEqual('Time Period - Fiscal Year'); + }); + it('should assume an an array is a date range', () => { + const data = ['1900-01-01', '1900-02-01']; + const converted = searchAnalytics.convertTimePeriod(data); + expect(Array.isArray(converted)).toBeTruthy(); + expect(converted[0].action).toEqual('Time Period - Date Range'); + }); + it('should return null in other situations', () => { + const data = 'hello'; + const converted = searchAnalytics.convertTimePeriod(data); + expect(converted).toBeFalsy(); + }); + }); + + describe('convertLocation', () => { + it('should parse locations into strings with the location level and name', () => { + const data = new OrderedMap({ + '123': { + display: { + entity: 'County', + standalone: 'Orange County, CA' + } + } + }); + const converted = searchAnalytics.convertLocation(data, 'test'); + expect(converted[0].label).toEqual('County - Orange County, CA'); + }); + }); + + 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 = { + timePeriodType: 'fy', + timePeriodFY: new Set(['1900']) + }; + const redux = searchAnalytics.unifyDateFields(filters); + expect(redux.timePeriod).toEqual(new Set(['1900'])); + }); + it('should set the `timePeriod` field to an array of `timePeriodStart` and `timePeriodEnd` filters when a date range is selected', () => { + const filters = { + timePeriodType: 'dr', + timePeriodStart: '1900-01-01', + timePeriodEnd: '1900-02-01' + }; + const redux = searchAnalytics.unifyDateFields(filters); + expect(redux.timePeriod).toEqual(['1900-01-01', '1900-02-01']); + }); + }); + + describe('convertFiltersToAnalyticEvents', () => { + it('should flatten an array of converted filters arrays to a single-level array', () => { + const filters = { + timePeriodType: 'fy', + timePeriodFY: new Set(['1900', '1902']) + }; + const events = searchAnalytics.convertFiltersToAnalyticEvents(filters); + expect(Array.isArray(events)).toBeTruthy(); + expect(events).toEqual([ + { + action: 'Time Period - Fiscal Year', + label: '1900' + }, + { + action: 'Time Period - Fiscal Year', + label: '1902' + } + ]); + }); + }); + + describe('sendAnalyticEvents', () => { + it('should update each event with a `category` property', () => { + const events = [{ + action: 'action', + label: 'label' + }]; + + const Analytics = require('helpers/analytics/Analytics'); + + searchAnalytics.sendAnalyticEvents(events); + expect(Analytics.event).toHaveBeenCalledTimes(1); + expect(Analytics.event).toHaveBeenCalledWith({ + category: 'Advanced Search - Search Filter', + action: 'action', + label: 'label' + }); + + Analytics.event.mockClear(); + }); + }); + + describe('sendFieldCombinations', () => { + it('should send an Analytic event with a non-repeating `|` separated string of filter names', () => { + const events = [{ + action: 'action', + label: 'label' + }, { + action: 'action', + label: 'label2' + }, { + action: 'z', + label: '123' + }]; + + const Analytics = require('helpers/analytics/Analytics'); + + searchAnalytics.sendFieldCombinations(events); + expect(Analytics.event).toHaveBeenCalledTimes(1); + expect(Analytics.event).toHaveBeenCalledWith({ + category: 'Advanced Search - Search Fields', + action: 'action|z' + }); + + Analytics.event.mockClear(); + }); + }); + + 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 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([])); });