diff --git a/src/_scss/components/datePicker/_datePicker.scss b/src/_scss/components/datePicker/_datePicker.scss index 82b091b008..67f6695174 100644 --- a/src/_scss/components/datePicker/_datePicker.scss +++ b/src/_scss/components/datePicker/_datePicker.scss @@ -108,8 +108,8 @@ } } -.set-date-button { - @include flex(1 1 auto); +.set-date-submit { + @include flex(0 0 auto); height: rem(42); width: 100%; margin-left: 0; @@ -122,14 +122,4 @@ margin-bottom: rem(3); width: rem(15); } - - @include display(flex); - @include align-items(center); - @include justify-content(center); - svg { - @include flex(0 0 auto); - width: rem(15); - height: rem(15); - fill: $color-white; - } } \ No newline at end of file diff --git a/src/_scss/layouts/default/footer/_downloadBottomBar.scss b/src/_scss/layouts/default/footer/_downloadBottomBar.scss index 6b4a052435..22ee0fa911 100644 --- a/src/_scss/layouts/default/footer/_downloadBottomBar.scss +++ b/src/_scss/layouts/default/footer/_downloadBottomBar.scss @@ -30,6 +30,33 @@ p { margin: 0; font-size: $small-font-size; + display: inline-block; + } + + button { + @include button-unstyled; + display: inline-block; + margin: rem(10) auto; + color: $color-primary; + font-weight: 600; + margin-left: rem(5); + } + + .link { + background-color: $color-white; + border: solid 1px $color-gray-light; + padding: rem(5); + } + + .icon { + height: rem(20); + display: inline-block; + width: rem(15); + svg { + width: rem(25); + height: rem(10); + fill: $color-green; + } } } diff --git a/src/_scss/layouts/tabbedSearch/header/_downloadButton.scss b/src/_scss/layouts/tabbedSearch/header/_downloadButton.scss index 5fb71417b8..85030063a7 100644 --- a/src/_scss/layouts/tabbedSearch/header/_downloadButton.scss +++ b/src/_scss/layouts/tabbedSearch/header/_downloadButton.scss @@ -5,7 +5,6 @@ .download-button { @include button-unstyled; padding: rem(8) rem(20); - border: 1px solid $color-white; cursor: pointer; @include display(flex); @@ -15,19 +14,11 @@ width: rem(155); height: rem(36); - @include transition(opacity 0.2s ease-in-out); + border: none; + background-color: $color-primary; - .icon { - @include flex(0 0 auto); - width: rem(18); - height: rem(18); - margin-right: rem(5); - svg { - fill: $color-white; - width: rem(18); - height: rem(18); - } - } + + @include transition(opacity 0.2s ease-in-out); .label { @include flex(1 1 auto); @@ -41,11 +32,14 @@ &:hover, &:active { opacity: 1; + background-color: $color-primary-darker; } &[disabled], &.disabled { cursor: default; opacity: 0.4; + border: 1px solid $color-white; + background-color: transparent; } } } \ No newline at end of file diff --git a/src/_scss/layouts/tabbedSearch/header/header.scss b/src/_scss/layouts/tabbedSearch/header/header.scss index 65e138a7de..804f9f894e 100644 --- a/src/_scss/layouts/tabbedSearch/header/header.scss +++ b/src/_scss/layouts/tabbedSearch/header/header.scss @@ -7,7 +7,7 @@ $search-header-height: rem(66); .search-header-container { width: 100%; - background-color: $color-primary; + background-color: #4A4A4A; color: $color-white; // bottom shadow cast on the content box-shadow: 0 2px 2px rgba(0,0,0,.3); diff --git a/src/_scss/pages/bulkDownload/_archiveInfoBox.scss b/src/_scss/pages/bulkDownload/_archiveInfoBox.scss index 80a00792a6..394dc00dd0 100644 --- a/src/_scss/pages/bulkDownload/_archiveInfoBox.scss +++ b/src/_scss/pages/bulkDownload/_archiveInfoBox.scss @@ -1,5 +1,5 @@ .archive-info-box { - display: none; + @include display(flex); background-color: $color-primary-alt-lightest; border: solid 1px $color-primary-alt-light; margin: rem(30) rem(20) rem(30) rem(30); diff --git a/src/_scss/pages/modals/bulkDownload/bulkDownload.scss b/src/_scss/pages/modals/bulkDownload/bulkDownload.scss index 4138c31079..7d924b21b0 100644 --- a/src/_scss/pages/modals/bulkDownload/bulkDownload.scss +++ b/src/_scss/pages/modals/bulkDownload/bulkDownload.scss @@ -1,6 +1,6 @@ .search-section-extra-modal { // override the modal widths - width: 85%; + width: 68%; } .bulk-download-modal { @@ -10,7 +10,7 @@ background-color: $color-primary; } .download-body { - padding: rem(20) rem(60); + padding: rem(20) rem(60) rem(40); .download-status-screen { text-align: center; .link-box { @@ -21,6 +21,7 @@ p { margin-top: 0; font-size: rem(14); + font-weight: bold; } button { @include button-unstyled; @@ -29,24 +30,37 @@ color: $color-primary; font-weight: 600; text-transform: uppercase; + padding: rem(10) rem(10) rem(5) rem(5); } + .link { background-color: $color-white; border: solid 1px $color-gray-light; padding: rem(5); } + .icon { - margin: auto; + margin-right: rem(2.5); width: rem(20); height: rem(20); + display: inline-block; svg { width: rem(25); - height: rem(25); + height: rem(11); fill: $color-green; } } } + + h3 { + margin-top: rem(15); + } + + .sub-details { + padding-bottom: rem(16); + } + .finish-button { display: block; margin: auto; diff --git a/src/_scss/pages/modals/fullDownload/_filterBar.scss b/src/_scss/pages/modals/fullDownload/_filterBar.scss index 5a984cdd26..fe4fdc1603 100644 --- a/src/_scss/pages/modals/fullDownload/_filterBar.scss +++ b/src/_scss/pages/modals/fullDownload/_filterBar.scss @@ -16,12 +16,21 @@ .filter-group-container { display: inline-block; + margin-right: rem(10); + + .filter-name { + font-size: $smallest-font-size; + font-weight: $font-semibold; + text-transform: uppercase; + + margin-bottom: rem(5); + } .filter-values { white-space: nowrap; .filter-item-container { display: inline-block; - margin-left: rem(10); + margin-right: rem(5); } } } diff --git a/src/_scss/pages/modals/fullDownload/_progressScreen.scss b/src/_scss/pages/modals/fullDownload/_progressScreen.scss index 751c56c8db..64df4fadb6 100644 --- a/src/_scss/pages/modals/fullDownload/_progressScreen.scss +++ b/src/_scss/pages/modals/fullDownload/_progressScreen.scss @@ -1,4 +1,5 @@ .download-progress-screen { + text-align: center; .main-title { h2 { text-align: center; @@ -11,5 +12,59 @@ .details { text-align: center; } + + .sub-details { + padding-bottom: rem(14); + } + } + .link-box { + padding: rem(30); + margin: rem(20) auto; + background-color: $color-gray-lightest; + text-align: center; + p { + margin-top: 0; + font-size: rem(14); + font-weight: bold; + } + + h3 { + margin-top: rem(15); + } + button { + @include button-unstyled; + display: block; + margin: rem(10) auto; + color: $color-primary; + font-weight: 600; + text-transform: uppercase; + padding: rem(10) rem(10) rem(5) rem(5); + } + + .link { + background-color: $color-white; + border: solid 1px $color-gray-light; + padding: rem(5); + } + + .icon { + margin-right: rem(2.5); + width: rem(20); + height: rem(20); + display: inline-block; + + svg { + width: rem(25); + height: rem(11); + fill: $color-green; + } + } + } + .finish-button { + display: block; + margin: auto; + background-color: $color-white; + color: $color-primary; + border: solid rem(2) $color-primary; } } \ No newline at end of file diff --git a/src/_scss/pages/modals/fullDownload/fullDownload.scss b/src/_scss/pages/modals/fullDownload/fullDownload.scss index f4c34f188f..a4fbf4e8a3 100644 --- a/src/_scss/pages/modals/fullDownload/fullDownload.scss +++ b/src/_scss/pages/modals/fullDownload/fullDownload.scss @@ -1,6 +1,6 @@ .search-section-extra-modal { // override the modal widths - width: 85%; + width: 50%; } .full-download-modal { @@ -9,7 +9,7 @@ background-color: $color-white; .download-body { - padding: rem(20) rem(30); + padding: rem(20) rem(60) rem(40); @import "./_filterBar"; } diff --git a/src/_scss/pages/search/_filterExpand.scss b/src/_scss/pages/search/_filterExpand.scss index 8f14cffd35..ce73fdd392 100644 --- a/src/_scss/pages/search/_filterExpand.scss +++ b/src/_scss/pages/search/_filterExpand.scss @@ -14,6 +14,7 @@ svg { @include flex(0 0 auto); cursor: pointer; + pointer-events: none; fill: $color-gray; height: rem(11); width: rem(11); @@ -43,16 +44,16 @@ .accessory-view { @include flex(0 0 auto); - width: rem(20); - height: rem(20); + width: rem(15); + height: rem(15); position: relative; color: $color-base; svg { - width: rem(20); - height: rem(20); - fill: $color-gray; + width: rem(15); + height: rem(15); + fill: $color-gray-light; } } @@ -75,6 +76,12 @@ fill: $color-black; @include transition(all 0.25s $ease-in-out-sine); } + + .accessory-view { + svg { + fill: $color-gray; + } + } } @import "pages/search/filters/keyword/_keywordHover"; diff --git a/src/_scss/pages/search/_searchSubmit.scss b/src/_scss/pages/search/_searchSubmit.scss new file mode 100644 index 0000000000..5d3721a2fd --- /dev/null +++ b/src/_scss/pages/search/_searchSubmit.scss @@ -0,0 +1,53 @@ +.sidebar-submit { + padding: rem(15) rem(25); + .submit-button { + display: block; + width: 100%; + + transition: opacity 0.15s; + opacity: 1; + + background-color: $color-primary; + + &:hover { + background-color: $color-primary-darker; + } + + &[disabled] { + opacity: 0.5; + cursor: not-allowed; + &:hover { + background-color: $color-primary; + } + } + } + .reset-button { + display: block; + @include button-unstyled; + + transition: opacity 0.15s; + opacity: 1; + + text-align: center; + + color: $color-primary; + font-size: $smallest-font-size; + + margin-top: rem(8); + margin-left: auto; + margin-right: auto; + + &:hover { + text-decoration: underline; + } + + &[disabled] { + cursor: not-allowed; + opacity: 0.5; + } + } +} + +.sidebar-top-submit { + border-bottom: 1px solid #e4e2e0; +} diff --git a/src/_scss/pages/search/filters/keyword/keyword.scss b/src/_scss/pages/search/filters/keyword/keyword.scss index 94afa6d65f..ac1a2070b2 100644 --- a/src/_scss/pages/search/filters/keyword/keyword.scss +++ b/src/_scss/pages/search/filters/keyword/keyword.scss @@ -1,10 +1,13 @@ .keyword-filter { + @import "elements/filters/_selectedFilterBtn"; + @import "mixins/selectedFilterWrap"; @import "../_singleSubmit"; + form { width: 100%; } - .filter-item-wrap { + .keyword-input-wrapper { @include display(flex); @include justify-content(center); @include align-items(center); @@ -25,4 +28,8 @@ @include flex(0 0 auto); @include singleSubmit(); } + + .selected-filters { + @include selected-filter-wrap; + } } \ No newline at end of file diff --git a/src/_scss/pages/search/filters/timePeriod/timePeriod.scss b/src/_scss/pages/search/filters/timePeriod/timePeriod.scss index ca94bd3df7..13ede13a12 100644 --- a/src/_scss/pages/search/filters/timePeriod/timePeriod.scss +++ b/src/_scss/pages/search/filters/timePeriod/timePeriod.scss @@ -6,6 +6,10 @@ @import 'components/_alerts'; @import 'components/datePicker/_datePicker'; @import './_toggleButtons'; + @import "elements/filters/_selectedFilterBtn"; + @import "mixins/selectedFilterWrap"; + @import "../_singleSubmit"; + padding: 0 $global-pad rem(20); ul.fiscal-years { @include clearfix; @@ -66,10 +70,24 @@ } .date-range-wrapper { margin-top: rem(25); + + .set-date-submit { + @include singleSubmit; + @include media($large-screen) { + @include flex(0 0 auto); + @include align-self(flex-end); + margin: 0; + margin-bottom: rem(3); + } + } } } .error-message { margin: $global-mrg } + + .selected-filters { + @include selected-filter-wrap; + } } } \ No newline at end of file diff --git a/src/_scss/pages/search/results/_tabs.scss b/src/_scss/pages/search/results/_tabs.scss index 609f0fd888..0efbf25596 100644 --- a/src/_scss/pages/search/results/_tabs.scss +++ b/src/_scss/pages/search/results/_tabs.scss @@ -37,7 +37,7 @@ background-color: transparent; transition: opacity 0.15s; - opacity: 0.5; + opacity: 1; border: none; border-top-left-radius: 5px; @@ -50,8 +50,45 @@ padding: rem(20) rem(30); } + .icon { + @include flex(0 0 auto) + width: rem(20); + height: rem(20); + + svg { + fill: $color-primary; + width: rem(20); + height: rem(20); + } + + @include media($tablet-screen) { + margin-right: rem(15); + width: rem(25); + height: rem(25); + + svg { + width: rem(25); + height: rem(25); + } + } + } + + .label { + @include flex(1 1 auto); + display: none; + + @include media($tablet-screen) { + display: block; + color: $color-primary; + font-weight: $font-semibold; + text-transform: uppercase; + line-height: rem(18); + letter-spacing: rem(2); + } + } + + &.active { - opacity: 1; background-color: $color-white; border: 1px solid $color-gray-lighter; border-bottom: 1px solid $color-white; @@ -84,46 +121,24 @@ border-bottom: 1px solid $color-gray-lighter; box-shadow: -4px 4px 0 4px $color-white; } - } - - &:active, &:hover { - opacity: 1; - } - - .icon { - @include flex(0 0 auto) - width: rem(20); - height: rem(20); - svg { - fill: $color-gray; - width: rem(20); - height: rem(20); + .label { + color: $color-gray; } - @include media($tablet-screen) { - margin-right: rem(15); - width: rem(25); - height: rem(25); - + .icon { svg { - width: rem(25); - height: rem(25); + fill: $color-gray; } } } - .label { - @include flex(1 1 auto); - display: none; + &[disabled] { + opacity: 0.5; + cursor: not-allowed; - @include media($tablet-screen) { - display: block; - color: $color-gray; - font-weight: $font-semibold; - text-transform: uppercase; - line-height: rem(18); - letter-spacing: rem(2); + &.active { + opacity: 1; } } } diff --git a/src/_scss/pages/search/results/screens/_loading.scss b/src/_scss/pages/search/results/screens/_loading.scss new file mode 100644 index 0000000000..7aabb64238 --- /dev/null +++ b/src/_scss/pages/search/results/screens/_loading.scss @@ -0,0 +1,39 @@ +svg.loading-bars { + @keyframes loading-bar-animation { + 0% { + transform: translate(0px 40px) scaleY(0.2); + } + 50% { + transform: translate(0px 10px) scaleY(0.8); + } + 100% { + transform: translate(0px 40px) scaleY(0.2); + } + } + + @keyframes loading-bars-loading { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + + @include animation(loading-bars-loading 0.2s ease-in forwards); + + rect { + fill: $color-gray-light; + @include animation(loading-bar-animation 1s infinite ease-in-out both); + + &.bar-two { + @include animation-delay(-0.6s); + } + &.bar-three{ + @include animation-delay(-0.9s); + } + &.bar-four { + @include animation-delay(-0.3s); + } + } +} \ No newline at end of file diff --git a/src/_scss/pages/search/results/screens/_noFilters.scss b/src/_scss/pages/search/results/screens/_noFilters.scss new file mode 100644 index 0000000000..4e0227b5c9 --- /dev/null +++ b/src/_scss/pages/search/results/screens/_noFilters.scss @@ -0,0 +1,34 @@ +.visualization-no-filters-screen { + .icon { + height: rem(70); + width: rem(70); + margin-left: auto; + margin-right: auto; + + svg { + fill: $color-gray-light; + height: rem(70); + width: rem(70); + + transform: rotate(90deg); + @include media($medium-screen) { + transform: none; + } + } + } + .message { + font-size: rem(36); + line-height: rem(45); + color: $color-gray-light; + text-align: center; + + margin-top: rem(10); + + max-width: rem(450); + padding-left: rem(10); + padding-right: rem(10); + margin-left: auto; + margin-right: auto; + } + +} \ No newline at end of file diff --git a/src/_scss/pages/search/results/screens/screens.scss b/src/_scss/pages/search/results/screens/screens.scss new file mode 100644 index 0000000000..e14a61e5e0 --- /dev/null +++ b/src/_scss/pages/search/results/screens/screens.scss @@ -0,0 +1,34 @@ +@import "./_loading"; +@import "./_noFilters"; + +.visualization-status-screen { + padding-top: rem(180); + padding-bottom: rem(180); +} + +// slide in animation +@keyframes visualizationFadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes visualizationFadeOut { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +.visualization-message-fade-enter, .visualization-message-fade-enter-active { + @include animation(visualizationFadeIn 0.225s ease-in forwards); +} + +.visualization-message-fade-leave, .visualization-message-fade-leave-active { + @include animation(visualizationFadeOut 0.195s ease-in forwards); +} \ No newline at end of file diff --git a/src/_scss/pages/search/results/searchResults.scss b/src/_scss/pages/search/results/searchResults.scss index 4f9fb2d988..c0c94a5f6c 100644 --- a/src/_scss/pages/search/results/searchResults.scss +++ b/src/_scss/pages/search/results/searchResults.scss @@ -16,5 +16,6 @@ @import "./table/resultsTable"; @import "./visualizations/visualizations"; + @import "./screens/screens"; } } diff --git a/src/_scss/pages/search/results/table/_tableMessages.scss b/src/_scss/pages/search/results/table/_tableMessages.scss new file mode 100644 index 0000000000..cd1a836716 --- /dev/null +++ b/src/_scss/pages/search/results/table/_tableMessages.scss @@ -0,0 +1,161 @@ +// slide in animation +@keyframes tableFadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes tableFadeOut { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +.table-message-fade-enter, .table-message-fade-enter-active { + @include animation(tableFadeIn 0.225s ease-in forwards); +} + +.table-message-fade-leave, .table-message-fade-leave-active { + @include animation(tableFadeOut 0.195s ease-in forwards); +} + +.results-table-message-container { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 3; + + @include display(flex); + @include justify-content(center); + @include align-items(flex-start); + + + + background-color: rgba(255, 255, 255, 0.9); + + &.full { + position: relative; + display: block; + background-color: $color-white; + } + + .results-table-loading { + @include flex(1 1 auto); + + margin-top: rem(50); + max-width: rem(200); + padding-left: rem(10); + padding-right: rem(10); + margin-left: auto; + margin-right: auto; + + @include media($tablet-screen) { + @include flex(0 0 auto); + max-width: rem(600); + margin-top: rem(100); + } + + .loading-animation { + width: rem(50); + height: rem(50); + margin-left: auto; + margin-right: auto; + } + + .loading-message { + font-size: rem(36); + line-height: rem(45); + color: $color-gray-light; + } + } + + .results-table-no-results { + @include flex(1 1 auto); + margin-top: rem(100); + margin-bottom: rem(100); + max-width: rem(450); + padding-left: rem(10); + padding-right: rem(10); + margin-left: auto; + margin-right: auto; + + @include media($tablet-screen) { + max-width: rem(600); + } + + .no-results-icon { + width: rem(75); + height: rem(72); + + margin-left: auto; + margin-right: auto; + + background-image: url('#{$image-path}/no_results.png'); + background-size: contain; + background-repeat: no-repeat; + } + .title { + font-size: rem(36); + line-height: rem(45); + color: $color-gray-light; + text-align: center; + font-weight: $font-semibold; + } + .description { + font-size: rem(36); + line-height: rem(45); + color: $color-gray-light; + text-align: center; + } + } + + .results-table-error { + @include flex(1 1 auto); + margin-top: rem(100); + margin-bottom: rem(100); + max-width: rem(450); + padding-left: rem(10); + padding-right: rem(10); + margin-left: auto; + margin-right: auto; + + @include media($tablet-screen) { + max-width: rem(600); + } + + .icon { + width: rem(75); + height: rem(72); + + margin-left: auto; + margin-right: auto; + + svg { + width: rem(72); + height: rem(72); + fill: $color-gray-light; + } + } + .title { + font-size: rem(36); + line-height: rem(45); + color: $color-gray-light; + text-align: center; + font-weight: $font-semibold; + } + .description { + font-size: rem(36); + line-height: rem(45); + color: $color-gray-light; + text-align: center; + } + } +} \ No newline at end of file diff --git a/src/_scss/pages/search/results/table/_tableTypes.scss b/src/_scss/pages/search/results/table/_tableTypes.scss index 7d398d9b62..bf11ce621f 100644 --- a/src/_scss/pages/search/results/table/_tableTypes.scss +++ b/src/_scss/pages/search/results/table/_tableTypes.scss @@ -21,8 +21,8 @@ position: relative; top: rem(1); @include media($large-screen) { - padding: rem(15); -} + padding: rem(15); + } &:hover{ @include transition(all 0.3s ease-in-out); background-color: darken($color-gray-lightest, 5%); @@ -58,45 +58,9 @@ } } } - } -} - -@import "components/_comingSoon"; -@include comingSoon; - -button.table-type-toggle.coming-soon { - background: repeating-linear-gradient( - 45deg, - rgba($color-gray-lightest, .2), - rgba($color-gray-lightest, .2) rem(2px), - rgba($color-gray-lighter, .3) rem(2px), - rgba($color-gray-lighter, .3) rem(4px) - ); - color: $color-gray-light; - .coming-soon-container { - width: 100%; - text-align: center; - height: 10px; - .coming-soon-icon { - bottom: rem(0px); - display: inline; - float: none; - } - .coming-soon-label { - bottom: rem(2px); - display: inline; - float: none; - left: rem(3px); + &[disabled] { + cursor: not-allowed; } } - &:hover { - background: repeating-linear-gradient( - 45deg, - rgba($color-gray-lightest, .2), - rgba($color-gray-lightest, .2) rem(2px), - rgba($color-gray-lighter, .3) rem(2px), - rgba($color-gray-lighter, .3) rem(4px) - ); - } } \ No newline at end of file diff --git a/src/_scss/pages/search/results/table/resultsTable.scss b/src/_scss/pages/search/results/table/resultsTable.scss index 653df2e2fa..72069879cd 100644 --- a/src/_scss/pages/search/results/table/resultsTable.scss +++ b/src/_scss/pages/search/results/table/resultsTable.scss @@ -4,16 +4,15 @@ @import "_tableTypes"; @import "_tableStyle"; @import "_resultsDropdownPickerWrapper"; - .loading-table { - opacity: 0.5; - @include transition(opacity 0.25s ease-in); - margin-top: rem(15); - } - .loaded-table { - opacity: 1; - @include transition(opacity 0.25s ease-in); + @import "./_tableMessages"; + + .results-table-content { + position: relative; margin-top: rem(15); + min-height: rem(200); + @include transition(opacity 0.25s ease-in); } + .ibt-table-container { border: 1px solid $color-gray-light; } diff --git a/src/_scss/pages/search/results/visualizations/_messages.scss b/src/_scss/pages/search/results/visualizations/_messages.scss new file mode 100644 index 0000000000..522ff5e5db --- /dev/null +++ b/src/_scss/pages/search/results/visualizations/_messages.scss @@ -0,0 +1,72 @@ +.visualization-message-container { + background-color: $color-white; + margin-top: rem(100); + + max-width: rem(450); + padding-left: rem(10); + padding-right: rem(10); + margin-left: auto; + margin-right: auto; + + @include media($tablet-screen) { + max-width: rem(600); + } + + .loading-animation { + width: rem(50); + height: rem(50); + margin-left: auto; + margin-right: auto; + } + + .message { + font-size: rem(36); + line-height: rem(45); + color: $color-gray-light; + text-align: center; + } + + .visualization-no-results { + margin-top: rem(100); + + .no-results-icon { + width: rem(75); + height: rem(72); + + margin-left: auto; + margin-right: auto; + + background-image: url('#{$image-path}/no_results.png'); + background-size: contain; + background-repeat: no-repeat; + } + .error-icon { + width: rem(75); + height: rem(72); + + margin-left: auto; + margin-right: auto; + margin-bottom: rem(10); + + svg { + width: rem(72); + height: rem(72); + fill: $color-gray-light; + } + } + .title { + font-size: rem(36); + line-height: rem(45); + color: $color-gray-light; + text-align: center; + font-weight: $font-semibold; + } + .description { + font-size: rem(36); + line-height: rem(45); + color: $color-gray-light; + text-align: center; + } + } + +} diff --git a/src/_scss/pages/search/results/visualizations/geo/_message.scss b/src/_scss/pages/search/results/visualizations/geo/_message.scss index 191ce1f7e4..e5d6f1c405 100644 --- a/src/_scss/pages/search/results/visualizations/geo/_message.scss +++ b/src/_scss/pages/search/results/visualizations/geo/_message.scss @@ -27,4 +27,72 @@ font-weight: $font-normal; line-height: rem(18); } +} + +.map-loading, .map-no-results { + padding: rem(10); + .loading-animation { + width: rem(50); + height: rem(50); + margin-left: auto; + margin-right: auto; + } + + .loading-message { + font-size: rem(36); + line-height: rem(45); + color: $color-gray-light; + text-align: center; + } + + .no-results-icon { + width: rem(75); + height: rem(72); + + margin-left: auto; + margin-right: auto; + + background-image: url('#{$image-path}/no_results.png'); + background-size: contain; + background-repeat: no-repeat; + } + .error-icon { + width: rem(75); + height: rem(72); + + margin-left: auto; + margin-right: auto; + margin-bottom: rem(10); + + svg { + width: rem(72); + height: rem(72); + fill: $color-gray-light; + } + } + .title, .description { + color: $color-gray-light; + text-align: center; + + margin-left: auto; + margin-right: auto; + max-width: rem(200); + font-size: rem(18); + + @include media($tablet-screen) { + max-width: rem(400); + font-size: rem(24); + line-height: rem(30); + } + + @include media($large-screen) { + max-width: rem(600); + font-size: rem(36); + line-height: rem(45); + } + } + + .title { + font-weight: $font-semibold; + } } \ No newline at end of file diff --git a/src/_scss/pages/search/results/visualizations/visualizations.scss b/src/_scss/pages/search/results/visualizations/visualizations.scss index 987c25cf12..85347ba844 100644 --- a/src/_scss/pages/search/results/visualizations/visualizations.scss +++ b/src/_scss/pages/search/results/visualizations/visualizations.scss @@ -1,3 +1,5 @@ @import "./time/timeVisualization"; @import "./rank/rankVisualization"; -@import "./geo/geoVisualization"; \ No newline at end of file +@import "./geo/geoVisualization"; + +@import "./_messages"; \ No newline at end of file diff --git a/src/_scss/pages/search/searchSidebar.scss b/src/_scss/pages/search/searchSidebar.scss index 2677f24c98..7539858c12 100644 --- a/src/_scss/pages/search/searchSidebar.scss +++ b/src/_scss/pages/search/searchSidebar.scss @@ -1,10 +1,12 @@ .search-sidebar { @import "./_searchOption"; + @import "./_searchSubmit"; @import "./_filters"; @import '../../elements/_inputs'; @import '../../elements/_labels'; background-color: $color-white; box-shadow: $container-shadow; + border-radius: rem(5); margin-top: rem(32); .sidebar-header { display: none; @@ -15,7 +17,7 @@ @include justify-content(flex-start); position: relative; border-bottom: rem(1) solid $color-gray-lighter; - height: rem(40); + height: rem(70); margin: 0; padding: 0 0 0 rem(15); & h6 { diff --git a/src/_scss/pages/search/topFilterBar/_group.scss b/src/_scss/pages/search/topFilterBar/_group.scss index 26bd7a1c7c..c6fc3d59b8 100644 --- a/src/_scss/pages/search/topFilterBar/_group.scss +++ b/src/_scss/pages/search/topFilterBar/_group.scss @@ -1,12 +1,11 @@ .filter-group-container { display: inline-block; - margin-right: 10px; + margin-right: 15px; margin-top: 10px; .filter-group { display: block; - background-color: $top-group-color; - padding: 5px 10px; + padding: 5px 0px; .filter-group-top { @include clearfix; @@ -14,9 +13,11 @@ .filter-name { float: left; - font-size: 12px; - line-height: 15px; + font-size: $smallest-font-size; + line-height: rem(13); color: $top-text-color; + font-weight: $font-semibold; + text-transform: uppercase; } .filter-group-close { diff --git a/src/_scss/pages/search/topFilterBar/_tag.scss b/src/_scss/pages/search/topFilterBar/_tag.scss index b8b722d4e6..d31db87490 100644 --- a/src/_scss/pages/search/topFilterBar/_tag.scss +++ b/src/_scss/pages/search/topFilterBar/_tag.scss @@ -1,6 +1,6 @@ .filter-item-container { display: inline-block; - margin-right: 6px; + margin-right: 5px; .filter-item { @include button-unstyled(); @@ -9,9 +9,10 @@ margin-bottom: 0.5rem; background-color: $color-white; padding: 8px; - border: 1px solid $color-gray-light; - @include border-top-radius(4px); - @include border-bottom-radius(4px); + border: 1px solid $color-gray-lighter; + box-shadow: $container-shadow; + @include border-top-radius(5px); + @include border-bottom-radius(5px); font-size: $smallest-font-size; diff --git a/src/graphics/icons.svg b/src/graphics/icons.svg index 78ea990e63..ee6669bf5d 100644 --- a/src/graphics/icons.svg +++ b/src/graphics/icons.svg @@ -212,4 +212,9 @@ + + + + + \ No newline at end of file diff --git a/src/img/no_results.png b/src/img/no_results.png new file mode 100644 index 0000000000..391a453711 Binary files /dev/null and b/src/img/no_results.png differ diff --git a/src/img/no_results@2x.png b/src/img/no_results@2x.png new file mode 100644 index 0000000000..391a453711 Binary files /dev/null and b/src/img/no_results@2x.png differ diff --git a/src/js/components/account/topFilterBar/LegacyTopFilterBar.jsx b/src/js/components/account/topFilterBar/LegacyTopFilterBar.jsx new file mode 100644 index 0000000000..d4c25c895d --- /dev/null +++ b/src/js/components/account/topFilterBar/LegacyTopFilterBar.jsx @@ -0,0 +1,88 @@ +/** + * LegacyTopFilterBar.jsx + * Created by Kevin Li 12/13/16 + * + * TopFilterBar is a React component that creates the sticky filter bar at the top of the search + * results page. It receives parsed filter groups from its parent Redux container. + * + * @extends React.Component + **/ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import * as Icons from 'components/sharedComponents/icons/Icons'; + +const propTypes = { + filters: PropTypes.array, + filterCount: PropTypes.number, + clearAllFilters: PropTypes.func, + groupGenerator: PropTypes.func, + compressed: PropTypes.bool +}; + +export default class LegacyTopFilterBar extends React.Component { + constructor(props) { + super(props); + + this.pressedClearAll = this.pressedClearAll.bind(this); + } + + + pressedClearAll() { + this.props.clearAllFilters(); + } + + render() { + const filters = this.props.filters.map((filter) => + this.props.groupGenerator({ + filter, + redux: this.props, + compressed: this.props.compressed + })); + + let filterBarHeader = `${this.props.filterCount} Current Filter`; + if (this.props.filterCount !== 1) { + filterBarHeader += 's'; + } + filterBarHeader += ':'; + + let hideCompressed = ''; + if (this.props.compressed) { + hideCompressed = 'hide'; + } + + return ( +
+
+
+
+ {filterBarHeader} +
+
+ +
+
+
+
+ {filters} +
+
+
+
+ ); + } +} + +LegacyTopFilterBar.propTypes = propTypes; diff --git a/src/js/components/account/topFilterBar/LegacyTopFilterItem.jsx b/src/js/components/account/topFilterBar/LegacyTopFilterItem.jsx new file mode 100644 index 0000000000..72910f0738 --- /dev/null +++ b/src/js/components/account/topFilterBar/LegacyTopFilterItem.jsx @@ -0,0 +1,72 @@ +/** + * LegacyTopFilterItem.jsx + * Created by Kevin Li 12/13/16 + **/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import * as Icons from 'components/sharedComponents/icons/Icons'; + +const propTypes = { + title: PropTypes.string.isRequired, + value: PropTypes.any, + removeFilter: PropTypes.func, + compressed: PropTypes.bool +}; + +const defaultProps = { + title: 'Filter', + compressed: false +}; + +export default class LegacyTopFilterItem extends React.Component { + constructor(props) { + super(props); + + this.clickedButton = this.clickedButton.bind(this); + } + + clickedButton() { + if (this.props.compressed) { + return; + } + this.props.removeFilter(this.props.value); + } + + render() { + const accessibleLabel = `Remove filter for ${this.props.title}`; + + let hideCompressed = ''; + if (this.props.compressed) { + hideCompressed = 'hide'; + } + + return ( +
+ +
+ ); + } +} + +LegacyTopFilterItem.propTypes = propTypes; +LegacyTopFilterItem.defaultProps = defaultProps; diff --git a/src/js/components/account/topFilterBar/filterGroups/LegacyBaseTopFilterGroup.jsx b/src/js/components/account/topFilterBar/filterGroups/LegacyBaseTopFilterGroup.jsx new file mode 100644 index 0000000000..582c531d13 --- /dev/null +++ b/src/js/components/account/topFilterBar/filterGroups/LegacyBaseTopFilterGroup.jsx @@ -0,0 +1,88 @@ +/** + * LegacyBaseTopFilterGroup.jsx + * Created by Kevin Li 12/13/16 + **/ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import * as Icons from 'components/sharedComponents/icons/Icons'; +import LegacyTopFilterItem from '../LegacyTopFilterItem'; + +const propTypes = { + filter: PropTypes.object, + tags: PropTypes.array, + clearFilterGroup: PropTypes.func, + compressed: PropTypes.bool +}; + +const defaultProps = { + tags: [], + compressed: false +}; + +export default class LegacyBaseTopFilterGroup extends React.Component { + constructor(props) { + super(props); + + this.clearFilterGroup = this.clearFilterGroup.bind(this); + } + + clearFilterGroup() { + this.props.clearFilterGroup(); + } + + render() { + const tags = this.props.tags.map((tag) => ( + + )); + + let showClose = ''; + if (tags.length < 2) { + showClose = ' hide'; + } + + let hideCompressed = ''; + if (this.props.compressed) { + hideCompressed = 'hide'; + } + + return ( +
+
+
+
+ {this.props.filter.name}: +
+
+ +
+
+
+
+ {tags} +
+
+
+
+ ); + } +} + +LegacyBaseTopFilterGroup.propTypes = propTypes; +LegacyBaseTopFilterGroup.defaultProps = defaultProps; diff --git a/src/js/components/account/topFilterBar/filterGroups/ObjectClassFilterGroup.jsx b/src/js/components/account/topFilterBar/filterGroups/ObjectClassFilterGroup.jsx index 0825b96922..046baffb25 100644 --- a/src/js/components/account/topFilterBar/filterGroups/ObjectClassFilterGroup.jsx +++ b/src/js/components/account/topFilterBar/filterGroups/ObjectClassFilterGroup.jsx @@ -6,7 +6,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import BaseTopFilterGroup from 'components/search/topFilterBar/filterGroups/BaseTopFilterGroup'; +import LegacyBaseTopFilterGroup from './LegacyBaseTopFilterGroup'; const propTypes = { filter: PropTypes.object, @@ -53,7 +53,7 @@ export default class ObjectClassFilterGroup extends React.Component { render() { const tags = this.generateTags(); - return (); diff --git a/src/js/components/account/topFilterBar/filterGroups/ProgramActivityFilterGroup.jsx b/src/js/components/account/topFilterBar/filterGroups/ProgramActivityFilterGroup.jsx index 973d537d2c..52bff0911a 100644 --- a/src/js/components/account/topFilterBar/filterGroups/ProgramActivityFilterGroup.jsx +++ b/src/js/components/account/topFilterBar/filterGroups/ProgramActivityFilterGroup.jsx @@ -7,7 +7,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { find } from 'lodash'; -import BaseTopFilterGroup from 'components/search/topFilterBar/filterGroups/BaseTopFilterGroup'; +import LegacyBaseTopFilterGroup from './LegacyBaseTopFilterGroup'; const propTypes = { filter: PropTypes.object, @@ -60,7 +60,7 @@ export default class ProgramActivityFilterGroup extends React.Component { render() { const tags = this.generateTags(); - return (); diff --git a/src/js/components/account/topFilterBar/filterGroups/TimePeriodDRFilterGroup.jsx b/src/js/components/account/topFilterBar/filterGroups/TimePeriodDRFilterGroup.jsx index 93981b3b87..d4b512cfdd 100644 --- a/src/js/components/account/topFilterBar/filterGroups/TimePeriodDRFilterGroup.jsx +++ b/src/js/components/account/topFilterBar/filterGroups/TimePeriodDRFilterGroup.jsx @@ -6,7 +6,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import BaseTopFilterGroup from 'components/search/topFilterBar/filterGroups/BaseTopFilterGroup'; +import LegacyBaseTopFilterGroup from './LegacyBaseTopFilterGroup'; const propTypes = { filter: PropTypes.object, @@ -55,7 +55,7 @@ export default class TimePeriodDRFilterGroup extends React.Component { render() { const tags = this.generateTags(); - return (); diff --git a/src/js/components/account/topFilterBar/filterGroups/TimePeriodFYFilterGroup.jsx b/src/js/components/account/topFilterBar/filterGroups/TimePeriodFYFilterGroup.jsx index c66664b267..4188e103a3 100644 --- a/src/js/components/account/topFilterBar/filterGroups/TimePeriodFYFilterGroup.jsx +++ b/src/js/components/account/topFilterBar/filterGroups/TimePeriodFYFilterGroup.jsx @@ -8,7 +8,7 @@ import PropTypes from 'prop-types'; import * as FiscalYearHelper from 'helpers/fiscalYearHelper'; -import BaseTopFilterGroup from 'components/search/topFilterBar/filterGroups/BaseTopFilterGroup'; +import LegacyBaseTopFilterGroup from './LegacyBaseTopFilterGroup'; const propTypes = { filter: PropTypes.object, @@ -86,7 +86,7 @@ export default class TimePeriodFYFilterGroup extends React.Component { render() { const tags = this.generateTags(); - return (); diff --git a/src/js/components/award/contract/ContractDetails.jsx b/src/js/components/award/contract/ContractDetails.jsx index 29602ecf11..287468db88 100644 --- a/src/js/components/award/contract/ContractDetails.jsx +++ b/src/js/components/award/contract/ContractDetails.jsx @@ -46,7 +46,9 @@ export default class ContractDetails extends React.Component { } componentWillReceiveProps(nextProps) { - this.prepareValues(nextProps.selectedAward); + if (!Object.is(nextProps.selectedAward, this.props.selectedAward)) { + this.prepareValues(nextProps.selectedAward); + } } parsePlaceOfPerformance(award) { diff --git a/src/js/components/award/financialAssistance/FinancialAssistanceDetails.jsx b/src/js/components/award/financialAssistance/FinancialAssistanceDetails.jsx index 6e429cca2d..8b949e79a1 100644 --- a/src/js/components/award/financialAssistance/FinancialAssistanceDetails.jsx +++ b/src/js/components/award/financialAssistance/FinancialAssistanceDetails.jsx @@ -43,10 +43,16 @@ export default class FinancialAssistanceDetails extends React.Component { }; } - componentWillReceiveProps() { + componentDidMount() { this.prepareValues(this.props.selectedAward); } + componentWillReceiveProps(nextProps) { + if (!Object.is(nextProps.selectedAward, this.props.selectedAward)) { + this.prepareValues(nextProps.selectedAward); + } + } + parsePlaceOfPerformance(award) { // Location let popPlace = ''; @@ -94,11 +100,10 @@ export default class FinancialAssistanceDetails extends React.Component { return popPlace; } - prepareValues() { + prepareValues(award) { let yearRangeTotal = ""; let monthRangeTotal = ""; let description = null; - const award = this.props.selectedAward; const latestTransaction = award.latest_transaction; // Date Range diff --git a/src/js/components/bulkDownload/BulkDownloadPage.jsx b/src/js/components/bulkDownload/BulkDownloadPage.jsx index dfee592fd9..9e945ad412 100644 --- a/src/js/components/bulkDownload/BulkDownloadPage.jsx +++ b/src/js/components/bulkDownload/BulkDownloadPage.jsx @@ -67,6 +67,14 @@ export default class BulkDownloadPage extends React.Component { this.clickedDownload = this.clickedDownload.bind(this); } + componentWillReceiveProps(nextProps) { + // Need to close the modal once the download is completed + if (this.state.showModal && nextProps.bulkDownload.download.expectedUrl === "" + && !nextProps.bulkDownload.download.showCollapsedProgress) { + this.hideModal(); + } + } + changeDataType(dataType) { this.props.setDataType(dataType); Router.history.replace('/bulk_download'); diff --git a/src/js/components/bulkDownload/archive/table/TableRow.jsx b/src/js/components/bulkDownload/archive/table/TableRow.jsx index 44c7b299a0..5bfaeb1841 100644 --- a/src/js/components/bulkDownload/archive/table/TableRow.jsx +++ b/src/js/components/bulkDownload/archive/table/TableRow.jsx @@ -19,7 +19,7 @@ export default class TableRow extends React.PureComponent { rowClass = 'row-odd'; } const cells = this.props.columns.map((column) => { - if (column.columnName === 'url') { + if (column.columnName === 'fileName') { // link to the file return ( - {this.props.file.url} + {this.props.file.fileName} ); diff --git a/src/js/components/bulkDownload/awards/filters/AgencyFilter.jsx b/src/js/components/bulkDownload/awards/filters/AgencyFilter.jsx index bfb5efe817..68b46f99e9 100644 --- a/src/js/components/bulkDownload/awards/filters/AgencyFilter.jsx +++ b/src/js/components/bulkDownload/awards/filters/AgencyFilter.jsx @@ -70,8 +70,7 @@ export default class AgencyFilter extends React.Component { e.preventDefault(); const target = e.target; this.props.updateFilter('subAgency', { - id: target.value, - name: target.name + name: target.value }); this.setState({ @@ -132,13 +131,12 @@ export default class AgencyFilter extends React.Component { const subAgencies = this.props.subAgencies.map((subAgency) => (
  • + key={`field-${subAgency.subtier_agency_name}`}> diff --git a/src/js/components/bulkDownload/modal/ModalContent.jsx b/src/js/components/bulkDownload/modal/ModalContent.jsx index d947daedae..75cadaf862 100644 --- a/src/js/components/bulkDownload/modal/ModalContent.jsx +++ b/src/js/components/bulkDownload/modal/ModalContent.jsx @@ -25,9 +25,9 @@ export default class ModalContent extends React.Component { this.onCopy = this.onCopy.bind(this); } + componentDidMount() { this.props.setDownloadCollapsed(true); - window.setTimeout(this.props.hideModal, 8000); // close the modal after 8 seconds } onCopy() { @@ -45,21 +45,27 @@ export default class ModalContent extends React.Component { return (
    -

    Your download is being generated.

    +

    We’re preparing your download.

    This may take a little while — wait times vary based on site traffic and file size.
    -

    Use this link to download your file anytime once it’s ready.

    +

    Once your download is ready, you can use this link to access it anytime

    {this.props.expectedFile}
    + - + - {this.state.copied ? {icon} : null}
    - +
    + To keep browsing, close this box; your download status will appear at the bottom of the screen. +
    +
    ); diff --git a/src/js/components/search/SearchPage.jsx b/src/js/components/search/SearchPage.jsx index e0c2e30713..7b027052be 100644 --- a/src/js/components/search/SearchPage.jsx +++ b/src/js/components/search/SearchPage.jsx @@ -21,10 +21,13 @@ import SearchResults from './SearchResults'; const propTypes = { + download: PropTypes.object, clearAllFilters: PropTypes.func, filters: PropTypes.object, lastUpdate: PropTypes.string, - downloadAvailable: PropTypes.bool + downloadAvailable: PropTypes.bool, + requestsComplete: PropTypes.bool, + noFiltersApplied: PropTypes.bool }; export default class SearchPage extends React.Component { @@ -117,7 +120,10 @@ export default class SearchPage extends React.Component { } render() { - let fullSidebar = (); + let fullSidebar = ( + + ); if (this.state.isMobile) { fullSidebar = null; } @@ -145,10 +151,12 @@ export default class SearchPage extends React.Component { showMobileFilters={this.state.showMobileFilters} updateFilterCount={this.updateFilterCount} toggleMobileFilters={this.toggleMobileFilters} - clearAllFilters={this.props.clearAllFilters} - lastUpdate={this.props.lastUpdate} /> + lastUpdate={this.props.lastUpdate} + requestsComplete={this.props.requestsComplete} + noFiltersApplied={this.props.noFiltersApplied} /> diff --git a/src/js/components/search/SearchResults.jsx b/src/js/components/search/SearchResults.jsx index 337af64b5c..02f646649e 100644 --- a/src/js/components/search/SearchResults.jsx +++ b/src/js/components/search/SearchResults.jsx @@ -6,7 +6,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { AddFilter, CloseCircle } from 'components/sharedComponents/icons/Icons'; +import { AddFilter } from 'components/sharedComponents/icons/Icons'; import TopFilterBarContainer from 'containers/search/topFilterBar/TopFilterBarContainer'; @@ -18,6 +18,8 @@ const propTypes = { isMobile: PropTypes.bool, filterCount: PropTypes.number, showMobileFilters: PropTypes.bool, + requestsComplete: PropTypes.bool, + noFiltersApplied: PropTypes.bool, toggleMobileFilters: PropTypes.func, clearAllFilters: PropTypes.func }; @@ -36,11 +38,6 @@ export default class SearchResults extends React.Component { mobileFilters = 'behind-filters'; } - let showClearButton = 'hide'; - if (this.props.filterCount > 0) { - showClearButton = ''; - } - let showCountBadge = ''; if (this.props.filterCount === 0) { showCountBadge = 'hide'; @@ -64,18 +61,6 @@ export default class SearchResults extends React.Component { -
    - +
    diff --git a/src/js/components/search/SearchSidebar.jsx b/src/js/components/search/SearchSidebar.jsx index 4626d9a44b..6932278987 100644 --- a/src/js/components/search/SearchSidebar.jsx +++ b/src/js/components/search/SearchSidebar.jsx @@ -6,6 +6,8 @@ import React from 'react'; import PropTypes from 'prop-types'; +import SearchSidebarSubmitContainer from 'containers/search/SearchSidebarSubmitContainer'; + import AwardTypeContainer from 'containers/search/filters/AwardTypeContainer'; import TimePeriodContainer from 'containers/search/filters/TimePeriodContainer'; import AgencyContainer from 'containers/search/filters/AgencyContainer'; @@ -85,7 +87,8 @@ const filters = { const propTypes = { mobile: PropTypes.bool, - filters: PropTypes.object + filters: PropTypes.object, + requestsComplete: PropTypes.bool }; const defaultProps = { @@ -97,7 +100,13 @@ export default class SearchSidebar extends React.Component { const expanded = []; filters.options.forEach((filter) => { // Collapse all by default, unless the filter has a selection made - expanded.push(SidebarHelper.filterHasSelections(this.props.filters, filter)); + if (filter === 'Time Period') { + // time period is always expanded + expanded.push(true); + } + else { + expanded.push(SidebarHelper.filterHasSelections(this.props.filters, filter)); + } }); return ( @@ -106,9 +115,15 @@ export default class SearchSidebar extends React.Component { -
    Filter by:
    +
    Filters
    + +
    +
    +
    + +
    ); } diff --git a/src/js/components/search/SearchSidebarSubmit.jsx b/src/js/components/search/SearchSidebarSubmit.jsx new file mode 100644 index 0000000000..39c7abf9e0 --- /dev/null +++ b/src/js/components/search/SearchSidebarSubmit.jsx @@ -0,0 +1,47 @@ +/** + * SearchSidebarSubmit.jsx + * Created by Kevin Li 12/19/17 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +const propTypes = { + requestsComplete: PropTypes.bool, + filtersChanged: PropTypes.bool, + applyStagedFilters: PropTypes.func, + resetFilters: PropTypes.func +}; + +const SearchSidebarSubmit = (props) => { + let disabled = false; + let title = 'Click to submit your search.'; + if (!props.requestsComplete || !props.filtersChanged) { + title = 'Add or update a filter to submit.'; + disabled = true; + } + + return ( +
    + + +
    + ); +}; + +SearchSidebarSubmit.propTypes = propTypes; + +export default SearchSidebarSubmit; diff --git a/src/js/components/search/filters/keyword/Keyword.jsx b/src/js/components/search/filters/keyword/Keyword.jsx index b1801db33e..fb4d88450d 100644 --- a/src/js/components/search/filters/keyword/Keyword.jsx +++ b/src/js/components/search/filters/keyword/Keyword.jsx @@ -6,11 +6,15 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { Close } from 'components/sharedComponents/icons/Icons'; + import IndividualSubmit from 'components/search/filters/IndividualSubmit'; const propTypes = { + selectedKeyword: PropTypes.string, submitText: PropTypes.func, changedInput: PropTypes.func, + removeKeyword: PropTypes.func, value: PropTypes.string }; @@ -27,21 +31,38 @@ export default class Keyword extends React.Component { } render() { + let hideTags = 'hide'; + if (this.props.selectedKeyword !== '') { + hideTags = ''; + } + return (
    - - +
    + + +
    +
    + +
    diff --git a/src/js/components/search/filters/timePeriod/DateRange.jsx b/src/js/components/search/filters/timePeriod/DateRange.jsx index b50bd5e6be..948a5f8462 100644 --- a/src/js/components/search/filters/timePeriod/DateRange.jsx +++ b/src/js/components/search/filters/timePeriod/DateRange.jsx @@ -7,9 +7,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import moment from 'moment'; import DatePicker from 'components/sharedComponents/DatePicker'; -import { AngleRight } from 'components/sharedComponents/icons/Icons'; +import { Close } from 'components/sharedComponents/icons/Icons'; import * as FiscalYearHelper from 'helpers/fiscalYearHelper'; +import IndividualSubmit from 'components/search/filters/IndividualSubmit'; + const defaultProps = { startDate: '01/01/2016', endDate: '12/31/2016', @@ -21,9 +23,12 @@ const propTypes = { onDateChange: PropTypes.func, startDate: PropTypes.object, endDate: PropTypes.object, + selectedStart: PropTypes.string, + selectedEnd: PropTypes.string, showError: PropTypes.func, hideError: PropTypes.func, - applyDateRange: PropTypes.func + applyDateRange: PropTypes.func, + removeDateRange: PropTypes.func }; export default class DateRange extends React.Component { @@ -59,6 +64,30 @@ export default class DateRange extends React.Component { const earliestDateString = FiscalYearHelper.convertFYToDateRange(FiscalYearHelper.earliestFiscalYear)[0]; const earliestDate = moment(earliestDateString, 'YYYY-MM-DD').toDate(); + + let dateLabel = ''; + let hideTags = 'hide'; + if (this.props.selectedStart || this.props.selectedEnd) { + hideTags = ''; + let start = null; + let end = null; + if (this.props.selectedStart) { + start = moment(this.props.selectedStart, 'YYYY-MM-DD').format('MM/DD/YYYY'); + } + if (this.props.selectedEnd) { + end = moment(this.props.selectedEnd, 'YYYY-MM-DD').format('MM/DD/YYYY'); + } + if (start && end) { + dateLabel = `${start} to ${end}`; + } + else if (start) { + dateLabel = `${start} to present`; + } + else { + dateLabel = `... to ${end}`; + } + } + return (
    + + +
    - +
    ); } diff --git a/src/js/components/search/filters/timePeriod/TimePeriod.jsx b/src/js/components/search/filters/timePeriod/TimePeriod.jsx index 9d101f66a0..10248b15fb 100644 --- a/src/js/components/search/filters/timePeriod/TimePeriod.jsx +++ b/src/js/components/search/filters/timePeriod/TimePeriod.jsx @@ -69,6 +69,7 @@ export default class TimePeriod extends React.Component { this.hideError = this.hideError.bind(this); this.toggleFilters = this.toggleFilters.bind(this); this.validateDates = this.validateDates.bind(this); + this.removeDateRange = this.removeDateRange.bind(this); } componentDidMount() { @@ -214,6 +215,14 @@ export default class TimePeriod extends React.Component { } } + removeDateRange() { + this.props.updateFilter({ + dateType: 'dr', + startDate: null, + endDate: null + }); + } + showError(error, message) { this.setState({ showError: true, @@ -267,10 +276,13 @@ export default class TimePeriod extends React.Component { startingTab={1} startDate={this.state.startDateUI} endDate={this.state.endDateUI} + selectedStart={this.props.filterTimePeriodStart} + selectedEnd={this.props.filterTimePeriodEnd} onDateChange={this.handleDateChange} showError={this.showError} hideError={this.hideError} - applyDateRange={this.validateDates} />); + applyDateRange={this.validateDates} + removeDateRange={this.removeDateRange} />); activeClassFY = 'inactive'; activeClassDR = ''; } diff --git a/src/js/components/search/header/DownloadButton.jsx b/src/js/components/search/header/DownloadButton.jsx index d165bc8073..5d5751836c 100644 --- a/src/js/components/search/header/DownloadButton.jsx +++ b/src/js/components/search/header/DownloadButton.jsx @@ -6,8 +6,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { ExclamationTriangle } from 'components/sharedComponents/icons/Icons'; - import NoDownloadHover from './NoDownloadHover'; const propTypes = { @@ -56,14 +54,8 @@ export default class DownloadButton extends React.Component { } let disabled = ''; - let downloadIcon = null; if (!this.props.downloadAvailable) { disabled = 'disabled'; - downloadIcon = ( -
    - -
    - ); } return ( @@ -77,7 +69,6 @@ export default class DownloadButton extends React.Component { title="Download your data" aria-label="Download your data" onClick={this.onClick}> - {downloadIcon}
    Download
    diff --git a/src/js/components/search/modals/fullDownload/DownloadBottomBar.jsx b/src/js/components/search/modals/fullDownload/DownloadBottomBar.jsx index 518861a8e6..1f86ef0252 100644 --- a/src/js/components/search/modals/fullDownload/DownloadBottomBar.jsx +++ b/src/js/components/search/modals/fullDownload/DownloadBottomBar.jsx @@ -7,12 +7,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import { ExclamationCircle, CheckCircle } from 'components/sharedComponents/icons/Icons'; +import { CopyToClipboard } from 'react-copy-to-clipboard'; const propTypes = { showError: PropTypes.bool, showSuccess: PropTypes.bool, title: PropTypes.string, - description: PropTypes.string + description: PropTypes.string, + download: PropTypes.object }; const defaultProps = { @@ -27,35 +29,63 @@ const Spinner = () => ( ); -const DownloadBottomBar = (props) => { - let leftIcon = ; - if (props.showError) { - leftIcon = ; +export default class DownloadBottomBar extends React.Component { + constructor(props) { + super(props); + + this.onCopy = this.onCopy.bind(this); + + this.state = { + copied: false + }; } - else if (props.showSuccess) { - leftIcon = ; + + onCopy() { + this.setState({ + copied: true + }); } - return ( -
    -
    -
    - {leftIcon} -
    -
    -
    - {props.title} + render() { + let leftIcon = ; + if (this.props.showError) { + leftIcon = ; + } + else if (this.props.showSuccess) { + leftIcon = ; + } + + const icon = ( +
    + +
    + ); + + return ( +
    +
    +
    + {leftIcon} +
    +
    +
    + {this.props.title} +
    +

    + {this.props.description} +

    + {this.state.copied ? {icon} : null} + + +
    -

    - {props.description} -

    -
    - ); + ); + } }; DownloadBottomBar.propTypes = propTypes; DownloadBottomBar.defaultProps = defaultProps; - -export default DownloadBottomBar; diff --git a/src/js/components/search/modals/fullDownload/FullDownloadModal.jsx b/src/js/components/search/modals/fullDownload/FullDownloadModal.jsx index 8980e2f352..c3058d98a5 100644 --- a/src/js/components/search/modals/fullDownload/FullDownloadModal.jsx +++ b/src/js/components/search/modals/fullDownload/FullDownloadModal.jsx @@ -21,6 +21,7 @@ import DownloadProgress from './screens/DownloadProgress'; const propTypes = { mounted: PropTypes.bool, + download: PropTypes.object, hideModal: PropTypes.func, setDownloadCollapsed: PropTypes.func, pendingDownload: PropTypes.bool @@ -87,10 +88,11 @@ export default class FullDownloadModal extends React.Component { else if (this.state.downloadStep === 3) { content = (); + download={this.props.download} + setDownloadCollapsed={this.props.setDownloadCollapsed} + expectedUrl={this.props.download.expectedUrl} />); } - return ( + +
    + ); return (
    -

    Your download is being prepared.

    +

    We’re preparing your download.

    - You can see its progress in the bar below. + This may take a little while — wait times vary based on site traffic and file size. +
    +
    +

    Once your download is ready, you can use this link to access it anytime.

    +
    {this.props.expectedUrl}
    + + + + +
    +
    + To keep browsing, close this box; your download status will appear at the bottom of the screen.
    +
    ); diff --git a/src/js/components/search/table/ResultsTableErrorMessage.jsx b/src/js/components/search/table/ResultsTableErrorMessage.jsx new file mode 100644 index 0000000000..e87da1bec4 --- /dev/null +++ b/src/js/components/search/table/ResultsTableErrorMessage.jsx @@ -0,0 +1,24 @@ +/** + * ResultsTableErrorMessage.jsx + * Created by Kevin Li 12/21/17 + **/ + +import React from 'react'; + +import { ExclamationTriangle } from 'components/sharedComponents/icons/Icons'; + +const ResultsTableErrorMessage = () => ( +
    +
    + +
    +
    + An error occurred. +
    +
    + Something went wrong while gathering your data. +
    +
    +); + +export default ResultsTableErrorMessage; diff --git a/src/js/components/search/table/ResultsTableLoadingMessage.jsx b/src/js/components/search/table/ResultsTableLoadingMessage.jsx new file mode 100644 index 0000000000..551022d6b2 --- /dev/null +++ b/src/js/components/search/table/ResultsTableLoadingMessage.jsx @@ -0,0 +1,19 @@ +/** + * ResultsTableLoadingMessage.jsx + * Created by Kevin Li 12/21/17 + **/ + +import React from 'react'; + +import LoadingSpinner from 'components/sharedComponents/LoadingSpinner'; + +const ResultsTableLoadingMessage = () => ( +
    + +
    + Gathering your data... +
    +
    +); + +export default ResultsTableLoadingMessage; diff --git a/src/js/components/search/table/ResultsTableNoResults.jsx b/src/js/components/search/table/ResultsTableNoResults.jsx new file mode 100644 index 0000000000..1a519c57af --- /dev/null +++ b/src/js/components/search/table/ResultsTableNoResults.jsx @@ -0,0 +1,20 @@ +/** + * ResultsTableNoResults.jsx + * Created by Kevin Li 12/21/17 + **/ + +import React from 'react'; + +const ResultsTableNoResults = () => ( +
    +
    +
    + No results found. +
    +
    + Try again using different filters. +
    +
    +); + +export default ResultsTableNoResults; diff --git a/src/js/components/search/table/ResultsTableSection.jsx b/src/js/components/search/table/ResultsTableSection.jsx index 1b76dd1d26..57cc163bff 100644 --- a/src/js/components/search/table/ResultsTableSection.jsx +++ b/src/js/components/search/table/ResultsTableSection.jsx @@ -6,12 +6,17 @@ import React from 'react'; import PropTypes from 'prop-types'; +import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'; + import ResultsTable from './ResultsTable'; import ResultsTableTabs from './ResultsTableTabs'; -import ResultsTableMessage from './ResultsTableMessage'; +import ResultsTableLoadingMessage from './ResultsTableLoadingMessage'; +import ResultsTableNoResults from './ResultsTableNoResults'; +import ResultsTableErrorMessage from './ResultsTableErrorMessage'; const propTypes = { inFlight: PropTypes.bool, + error: PropTypes.bool, tableTypes: PropTypes.array, currentType: PropTypes.string, switchTab: PropTypes.func, @@ -50,15 +55,36 @@ export default class ResultsTableSection extends React.Component { } render() { - let loadingWrapper = 'loaded-table'; let message = null; + let table = ( + + ); + if (this.props.inFlight) { - loadingWrapper = 'loading-table'; - message = ; + message = ( +
    + +
    + ); + } + else if (this.props.error) { + table = null; + message = ( +
    + +
    + ); } else if (this.props.results.length === 0) { // no results - message = ; + table = null; + message = ( +
    + +
    + ); } return ( @@ -73,8 +99,16 @@ export default class ResultsTableSection extends React.Component { types={this.props.tableTypes} active={this.props.currentType} counts={this.props.counts} - switchTab={this.props.switchTab} /> -
    + switchTab={this.props.switchTab} + disabled={this.props.inFlight} /> +
    + + {message} +
    { @@ -82,11 +116,8 @@ export default class ResultsTableSection extends React.Component { // the results table width will follow this div's width this.tableWidthController = div; }} /> - + {table}
    - {message}
    ); } diff --git a/src/js/components/search/table/ResultsTableTabItem.jsx b/src/js/components/search/table/ResultsTableTabItem.jsx index cb739276de..05b2eb1bf1 100644 --- a/src/js/components/search/table/ResultsTableTabItem.jsx +++ b/src/js/components/search/table/ResultsTableTabItem.jsx @@ -6,7 +6,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import ComingSoonLabel from 'components/sharedComponents/ComingSoonLabel'; import { formatNumber } from 'helpers/moneyFormatter'; const propTypes = { @@ -30,27 +29,21 @@ export default class ResultsTableTabItem extends React.Component { } render() { - const comingSoonModule = (); let activeClass = ''; - let comingSoon = ''; let disabledStatus = ''; - let status = ''; if (this.props.active) { activeClass = ' active'; } if (this.props.enabled === false) { - comingSoon = comingSoonModule; - status = ' coming-soon'; disabledStatus = true; } else { - status = ''; disabledStatus = false; } return (
    - {comingSoon} ); } diff --git a/src/js/components/search/table/ResultsTableTabs.jsx b/src/js/components/search/table/ResultsTableTabs.jsx index f1ea1f4b53..340c2e8c51 100644 --- a/src/js/components/search/table/ResultsTableTabs.jsx +++ b/src/js/components/search/table/ResultsTableTabs.jsx @@ -12,7 +12,8 @@ const propTypes = { types: PropTypes.array, counts: PropTypes.object, active: PropTypes.string, - switchTab: PropTypes.func + switchTab: PropTypes.func, + disabled: PropTypes.bool }; export default class ResultsTableTabs extends React.Component { @@ -23,7 +24,8 @@ export default class ResultsTableTabs extends React.Component { count={this.props.counts[type.internal]} active={this.props.active === type.internal} switchTab={this.props.switchTab} - key={`table-type-item-${type.internal}`} /> + key={`table-type-item-${type.internal}`} + enabled={!this.props.disabled} /> )); return ( diff --git a/src/js/components/search/topFilterBar/TopFilterBar.jsx b/src/js/components/search/topFilterBar/TopFilterBar.jsx index a3c38a3282..382661ca49 100644 --- a/src/js/components/search/topFilterBar/TopFilterBar.jsx +++ b/src/js/components/search/topFilterBar/TopFilterBar.jsx @@ -11,78 +11,43 @@ import React from 'react'; import PropTypes from 'prop-types'; -import * as Icons from 'components/sharedComponents/icons/Icons'; - const propTypes = { filters: PropTypes.array, filterCount: PropTypes.number, - clearAllFilters: PropTypes.func, - groupGenerator: PropTypes.func, - compressed: PropTypes.bool + groupGenerator: PropTypes.func }; -export default class TopFilterBar extends React.Component { - constructor(props) { - super(props); - - this.pressedClearAll = this.pressedClearAll.bind(this); - } - +const TopFilterBar = (props) => { + const filters = props.filters.map((filter) => + props.groupGenerator({ + filter, + redux: props + })); - pressedClearAll() { - this.props.clearAllFilters(); + let filterBarHeader = `${props.filterCount} Active Filter`; + if (props.filterCount !== 1) { + filterBarHeader += 's'; } - - render() { - const filters = this.props.filters.map((filter) => - this.props.groupGenerator({ - filter, - redux: this.props, - compressed: this.props.compressed - })); - - let filterBarHeader = `${this.props.filterCount} Current Filter`; - if (this.props.filterCount !== 1) { - filterBarHeader += 's'; - } - filterBarHeader += ':'; - - let hideCompressed = ''; - if (this.props.compressed) { - hideCompressed = 'hide'; - } - - return ( -
    -
    -
    -
    - {filterBarHeader} -
    -
    - -
    + filterBarHeader += ':'; + + return ( +
    +
    +
    +
    + {filterBarHeader}
    -
    -
    - {filters} -
    +
    +
    +
    + {filters}
    - ); - } -} +
    + ); +}; TopFilterBar.propTypes = propTypes; + +export default TopFilterBar; diff --git a/src/js/components/search/topFilterBar/TopFilterItem.jsx b/src/js/components/search/topFilterBar/TopFilterItem.jsx index c3e073ac9f..77893f06c4 100644 --- a/src/js/components/search/topFilterBar/TopFilterItem.jsx +++ b/src/js/components/search/topFilterBar/TopFilterItem.jsx @@ -5,68 +5,27 @@ import React from 'react'; import PropTypes from 'prop-types'; -import * as Icons from 'components/sharedComponents/icons/Icons'; const propTypes = { - title: PropTypes.string.isRequired, - value: PropTypes.any, - removeFilter: PropTypes.func, - compressed: PropTypes.bool + title: PropTypes.string.isRequired }; const defaultProps = { - title: 'Filter', - compressed: false + title: 'Filter' }; -export default class TopFilterItem extends React.Component { - constructor(props) { - super(props); - - this.clickedButton = this.clickedButton.bind(this); - } - - clickedButton() { - if (this.props.compressed) { - return; - } - this.props.removeFilter(this.props.value); - } - - render() { - const accessibleLabel = `Remove filter for ${this.props.title}`; - - let hideCompressed = ''; - if (this.props.compressed) { - hideCompressed = 'hide'; - } - - return ( -
    - +const TopFilterItem = (props) => ( +
    +
    +
    + {props.title}
    - ); - } -} +
    +
    +); TopFilterItem.propTypes = propTypes; TopFilterItem.defaultProps = defaultProps; + +export default TopFilterItem; diff --git a/src/js/components/search/topFilterBar/filterGroups/BaseTopFilterGroup.jsx b/src/js/components/search/topFilterBar/filterGroups/BaseTopFilterGroup.jsx index cd95682fd9..aef43514bd 100644 --- a/src/js/components/search/topFilterBar/filterGroups/BaseTopFilterGroup.jsx +++ b/src/js/components/search/topFilterBar/filterGroups/BaseTopFilterGroup.jsx @@ -6,7 +6,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import * as Icons from 'components/sharedComponents/icons/Icons'; import TopFilterItem from '../TopFilterItem'; const propTypes = { @@ -22,16 +21,6 @@ const defaultProps = { }; export default class BaseTopFilterGroup extends React.Component { - constructor(props) { - super(props); - - this.clearFilterGroup = this.clearFilterGroup.bind(this); - } - - clearFilterGroup() { - this.props.clearFilterGroup(); - } - render() { const tags = this.props.tags.map((tag) => ( )); - let showClose = ''; - if (tags.length < 2) { - showClose = ' hide'; - } - - let hideCompressed = ''; - if (this.props.compressed) { - hideCompressed = 'hide'; - } - return (
    -
    +
    - {this.props.filter.name}: -
    -
    - + {this.props.filter.name}
    diff --git a/src/js/components/search/visualizations/ChartError.jsx b/src/js/components/search/visualizations/ChartError.jsx new file mode 100644 index 0000000000..14ed641a21 --- /dev/null +++ b/src/js/components/search/visualizations/ChartError.jsx @@ -0,0 +1,26 @@ +/** + * ChartError.jsx + * Created by Kevin Li 12/26/17 + */ + +import React from 'react'; + +import { ExclamationTriangle } from 'components/sharedComponents/icons/Icons'; + +const ChartError = () => ( +
    +
    +
    + +
    +
    + An error occurred. +
    +
    + Something went wrong while gathering your data. +
    +
    +
    +); + +export default ChartError; diff --git a/src/js/components/search/visualizations/ChartLoadingMessage.jsx b/src/js/components/search/visualizations/ChartLoadingMessage.jsx new file mode 100644 index 0000000000..89ec546199 --- /dev/null +++ b/src/js/components/search/visualizations/ChartLoadingMessage.jsx @@ -0,0 +1,20 @@ +/** + * ChartLoadingMessage.jsx + * Created by Kevin Li 12/26/17 + */ + +import React from 'react'; +import LoadingSpinner from 'components/sharedComponents/LoadingSpinner'; + +const ChartLoadingMessage = () => ( +
    +
    + +
    + Gathering your data... +
    +
    +
    +); + +export default ChartLoadingMessage; diff --git a/src/js/components/search/visualizations/ChartNoResults.jsx b/src/js/components/search/visualizations/ChartNoResults.jsx new file mode 100644 index 0000000000..aa743decad --- /dev/null +++ b/src/js/components/search/visualizations/ChartNoResults.jsx @@ -0,0 +1,22 @@ +/** + * ChartNoResults.jsx + * Created by Kevin Li 12/26/17 + */ + +import React from 'react'; + +const ChartNoResults = () => ( +
    +
    +
    +
    + No results found. +
    +
    + Try again using different filters. +
    +
    +
    +); + +export default ChartNoResults; diff --git a/src/js/components/search/visualizations/VisualizationTabItem.jsx b/src/js/components/search/visualizations/VisualizationTabItem.jsx index b99755ea69..aa74872a8a 100644 --- a/src/js/components/search/visualizations/VisualizationTabItem.jsx +++ b/src/js/components/search/visualizations/VisualizationTabItem.jsx @@ -13,7 +13,8 @@ const propTypes = { code: PropTypes.string, label: PropTypes.string, icon: PropTypes.string, - active: PropTypes.bool + active: PropTypes.bool, + disabled: PropTypes.bool }; const VisualizationTabItem = (props) => { @@ -33,7 +34,8 @@ const VisualizationTabItem = (props) => { className={`visualization-type-tab ${active}`} aria-label={props.label} title={props.label} - onClick={clickedTab}> + onClick={clickedTab} + disabled={props.disabled}>
    diff --git a/src/js/components/search/visualizations/VisualizationWrapper.jsx b/src/js/components/search/visualizations/VisualizationWrapper.jsx index 6ca94d2a40..e5e00347c1 100644 --- a/src/js/components/search/visualizations/VisualizationWrapper.jsx +++ b/src/js/components/search/visualizations/VisualizationWrapper.jsx @@ -14,6 +14,8 @@ import GeoVisualizationSectionContainer from import RankVisualizationWrapperContainer from 'containers/search/visualizations/rank/RankVisualizationWrapperContainer'; +import NoFiltersScreen from './screens/NoFiltersScreen'; + import VisualizationTabItem from './VisualizationTabItem'; const tabOptions = [ @@ -35,7 +37,9 @@ const tabOptions = [ ]; const propTypes = { - isMobile: PropTypes.bool + isMobile: PropTypes.bool, + requestsComplete: PropTypes.bool, + noFiltersApplied: PropTypes.bool }; export default class VisualizationWrapper extends React.Component { @@ -61,25 +65,28 @@ export default class VisualizationWrapper extends React.Component { {...tab} key={tab.code} active={this.state.active === tab.code} - clickedTab={this.clickedTab} /> + clickedTab={this.clickedTab} + disabled={!this.props.requestsComplete} /> )); - let content = null; - switch (this.state.active) { - case 'table': - content = ; - break; - case 'time': - content = ; - break; - case 'map': - content = ; - break; - case 'rank': - content = ; - break; - default: - content = null; + let content = ; + if (!this.props.noFiltersApplied) { + switch (this.state.active) { + case 'table': + content = ; + break; + case 'time': + content = ; + break; + case 'map': + content = ; + break; + case 'rank': + content = ; + break; + default: + content = null; + } } return ( diff --git a/src/js/components/search/visualizations/geo/GeoVisualizationSection.jsx b/src/js/components/search/visualizations/geo/GeoVisualizationSection.jsx index 5606c35f21..b3c9b79263 100644 --- a/src/js/components/search/visualizations/geo/GeoVisualizationSection.jsx +++ b/src/js/components/search/visualizations/geo/GeoVisualizationSection.jsx @@ -7,6 +7,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import Cookies from 'js-cookie'; +import LoadingSpinner from 'components/sharedComponents/LoadingSpinner'; +import { ExclamationTriangle } from 'components/sharedComponents/icons/Icons'; + import GeoVisualizationScopeButton from './GeoVisualizationScopeButton'; import MapWrapper from './MapWrapper'; import GeoVisualizationTooltip from './GeoVisualizationTooltip'; @@ -22,7 +25,9 @@ const propTypes = { renderHash: PropTypes.string, data: PropTypes.object, total: PropTypes.number, - message: PropTypes.string + loading: PropTypes.bool, + error: PropTypes.bool, + noResults: PropTypes.bool }; const availableLayers = ['state', 'county', 'congressionalDistrict']; @@ -89,9 +94,47 @@ export default class GeoVisualizationSection extends React.Component { closeDisclaimer={this.closeDisclaimer} />); } - let loadingMessage = null; - if (this.props.message !== '') { - loadingMessage = (); + let message = null; + if (this.props.loading) { + message = ( + +
    + +
    + Gathering your data... +
    +
    +
    + ); + } + else if (this.props.error) { + message = ( + +
    +
    + +
    +
    + An error occurred. +
    +
    + Something went wrong while gathering your data. +
    +
    +
    + ); + } + else if (this.props.noResults) { + message = ( + +
    +
    +
    + No results found in the current map area. +
    +
    + + ); } return ( @@ -149,7 +192,7 @@ export default class GeoVisualizationSection extends React.Component { availableLayers={availableLayers} showLayerToggle> {disclaimer} - {loadingMessage} + {message}
    diff --git a/src/js/components/search/visualizations/geo/MapMessage.jsx b/src/js/components/search/visualizations/geo/MapMessage.jsx index cd2f0b70b0..842a22f430 100644 --- a/src/js/components/search/visualizations/geo/MapMessage.jsx +++ b/src/js/components/search/visualizations/geo/MapMessage.jsx @@ -7,13 +7,13 @@ import React from 'react'; import PropTypes from 'prop-types'; const propTypes = { - message: PropTypes.string + children: PropTypes.node }; const MapMessage = (props) => (
    - {props.message} + {props.children}
    ); diff --git a/src/js/components/search/visualizations/screens/NoFiltersScreen.jsx b/src/js/components/search/visualizations/screens/NoFiltersScreen.jsx new file mode 100644 index 0000000000..6e11ecfcf9 --- /dev/null +++ b/src/js/components/search/visualizations/screens/NoFiltersScreen.jsx @@ -0,0 +1,23 @@ +/** + * NoFiltersScreen.jsx + * Created by Kevin Li 12/26/17 + */ + +import React from 'react'; + +import { CircleArrowLeft } from 'components/sharedComponents/icons/Icons'; + +const NoFiltersScreen = () => ( +
    +
    +
    + +
    +
    + Choose your filters and submit your search to begin. +
    +
    +
    +); + +export default NoFiltersScreen; diff --git a/src/js/components/search/visualizations/time/TimeVisualization.jsx b/src/js/components/search/visualizations/time/TimeVisualization.jsx index a211236c4b..faed7f6c84 100644 --- a/src/js/components/search/visualizations/time/TimeVisualization.jsx +++ b/src/js/components/search/visualizations/time/TimeVisualization.jsx @@ -6,9 +6,13 @@ import React from 'react'; import PropTypes from 'prop-types'; +import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'; + import BarChart from './chart/BarChart'; import Tooltip from './TimeVisualizationTooltip'; -import ChartMessage from './TimeVisualizationChartMessage'; +import ChartLoadingMessage from '../ChartLoadingMessage'; +import ChartNoResults from '../ChartNoResults'; +import ChartError from '../ChartError'; const defaultProps = { groups: [], @@ -43,7 +47,9 @@ const propTypes = { xSeries: PropTypes.array, ySeries: PropTypes.array, loading: PropTypes.bool, - legend: PropTypes.array + legend: PropTypes.array, + visualizationPeriod: PropTypes.string, + error: PropTypes.bool }; /* eslint-enable react/no-unused-prop-types */ @@ -81,10 +87,13 @@ export default class TimeVisualization extends React.Component { barWidth={this.state.barWidth} />); } - let chart = (); + let chart = (); if (this.props.loading) { // API request is still pending - chart = (); + chart = (); + } + else if (this.props.error) { + chart = (); } else if (this.props.groups.length > 0) { // only mount the chart component if there is data to display @@ -96,7 +105,13 @@ export default class TimeVisualization extends React.Component { return (
    - {chart} + + {chart} + {tooltip}
    ); diff --git a/src/js/components/search/visualizations/time/TimeVisualizationSection.jsx b/src/js/components/search/visualizations/time/TimeVisualizationSection.jsx index 22593597d5..16cca363f2 100644 --- a/src/js/components/search/visualizations/time/TimeVisualizationSection.jsx +++ b/src/js/components/search/visualizations/time/TimeVisualizationSection.jsx @@ -12,7 +12,8 @@ import TimeVisualizationPeriodButton from './TimeVisualizationPeriodButton'; const propTypes = { data: PropTypes.object, - updateVisualizationPeriod: PropTypes.func + updateVisualizationPeriod: PropTypes.func, + visualizationPeriod: PropTypes.string }; export default class TimeVisualizationSection extends React.Component { diff --git a/src/js/components/search/visualizations/time/chart/BarChart.jsx b/src/js/components/search/visualizations/time/chart/BarChart.jsx index 068b3aefca..9770a753e4 100644 --- a/src/js/components/search/visualizations/time/chart/BarChart.jsx +++ b/src/js/components/search/visualizations/time/chart/BarChart.jsx @@ -22,12 +22,14 @@ const propTypes = { width: PropTypes.number, height: PropTypes.number, xSeries: PropTypes.array, + rawLabels: PropTypes.array, ySeries: PropTypes.array, showTooltip: PropTypes.func, enableHighlight: PropTypes.bool, padding: PropTypes.object, legend: PropTypes.array, - activeLabel: PropTypes.object + activeLabel: PropTypes.object, + visualizationPeriod: PropTypes.string }; /* eslint-enable react/no-unused-prop-types */ @@ -241,6 +243,7 @@ export default class BarChart extends React.Component { graphHeight, yValues: allY, xValues: props.groups, + rawLabels: props.rawLabels, yAverage: mean(allY), yTicks: yScale.ticks(7) }); @@ -408,9 +411,11 @@ export default class BarChart extends React.Component { width={this.props.width - this.props.padding.left} padding={this.props.padding} data={this.state.xValues} + rawLabels={this.state.rawLabels} scale={this.state.xScale} axisPos={this.state.xAxisPos} - activeLabel={this.props.activeLabel} /> + activeLabel={this.props.activeLabel} + visualizationPeriod={this.props.visualizationPeriod} /> { - // offset the D3 calculated position by the left padding - // and put the label in the middle - // of the each tick's width to center the text - if (item !== props.activeLabel.xValue) { - return null; - } - const xPos = props.scale(item) + (props.scale.bandwidth() / 2); - return (); - }) - ); + const xPos = props.scale(props.activeLabel.xValue) + (props.scale.bandwidth() / 2); + return ([]); + } + + // Figure out which labels to show depending on type + let labelIterator = 1; + let labelOffset = 0; + // Year has 4 quarters + if (props.visualizationPeriod === "quarter") { + labelIterator = 4; } - else if (ref.length === 1) { - return ( - props.data.map((item) => { - // offset the D3 calculated position by the left padding and put the label in - // the middle - // of the each tick's width to center the text - const xPos = props.scale(item) + (props.scale.bandwidth() / 2); - return (); - }) - ); + else if (props.visualizationPeriod === "month") { + labelIterator = 12; } - else if (ref[0][0] === 'Q') { - // Quarterly - return ( - props.data.map((item, index) => { - // offset the D3 calculated position by the left padding and put the label in the middle - // of the each tick's width to center the text - if (index % 4 !== 0) { - return null; - } - - const endIndex = index + 3 > props.data.length ? props.data.length - 1 : index + 3; - - const xPos = (props.scale(item) + props.scale(props.data[endIndex]) + props.scale.bandwidth()) / 2; - - return (); - }) - ); + + // Get offset in case of first period + if (props.visualizationPeriod !== "fiscal_year" && props.rawLabels) { + labelOffset = this.calculateDateOffset(props.rawLabels[0], props.visualizationPeriod); } - // Monthly View + return ( - props.data.map((item, index) => { - // offset the D3 calculated position by the left padding and put the label in the middle + props.rawLabels.map((item, index) => { + // offset the D3 calculated position by the left padding and put the label in + // the middle // of the each tick's width to center the text - if (index % 12 !== 0) { + if ((index - labelOffset) % labelIterator !== 0 && index !== 0) { return null; } - const endIndex = index + 11 > props.data.length ? props.data.length - 1 : index + 11; - - const xPos = (props.scale(item) + props.scale(props.data[endIndex]) + props.scale.bandwidth()) / 2; - - const label = (parseInt(item.split(" ")[1], 10) + 1).toString(); + // Figure out what to call the label and where to place it + const label = this.calculateLabel(item, props); + const xPos = this.calculateXPos(item, index, labelOffset, props); return (); + key={`label-x-${item}-${index}`} />); }) ); } + // Finds the position of the label, under bar for years or + // average start and end for monthly/quartlery + calculateXPos(item, index, labelOffset, props) { + if (props.visualizationPeriod === 'fiscal_year') { + return props.scale(item.year) + (props.scale.bandwidth() / 2); + } + const endIndex = this.calculateEndIndex( + index, + props.data, + props.visualizationPeriod, + labelOffset); + + // Need to use props.data because you cant scale by objects + return (props.scale(props.data[index]) + props.scale(props.data[endIndex]) + props.scale.bandwidth()) / 2; + } + + // Gets the content of the label, year, break apart the quarter, or + // Fiscal year increments if the date range started between oct-dec + calculateLabel(item, props) { + if (props.visualizationPeriod === 'fiscal_year') { + return item.year; + } + const year = item.year; + if (props.visualizationPeriod === 'quarter') { + return year; + } + const months = ['Oct', 'Nov', 'Dec']; + const increment = months.indexOf(item.period) !== -1 ? 1 : 0; + return (parseInt(year, 10) + increment).toString(); + } + + // Calcuate how many periods until the end of the FY so that the year label + // is placed correctly + calculateDateOffset(item, type) { + const period = item.period; + if (type === 'month') { + // Fiscal year starts in October, so calculate how many months until the + // end of the year + // Mod 12 because 12 month offset == to 0 month offset + const months = ['Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May', + 'Jun', 'Jul', 'Aug', 'Sep']; + return (12 - months.indexOf(period)) % 12; + } + // Calculate how many quarters left in the year + // Mod 4 because 4 quarter offset == 0 quarter offset + const quarters = ['Q1', 'Q2', 'Q3', 'Q4']; + return (4 - quarters.indexOf(period)) % 4; + } + + // Finds the end of the year for a range of dates + // Only matters for the first section and last since the date range can start + // in the middle or not be finished yet. Every other date should be a full range + calculateEndIndex(index, data, type, offset) { + // Blocks of 4 for quarters (0-3) + if (type === 'quarter') { + let endIndex = index + 3; + if (index < offset) { + endIndex = offset - 1; + } + if (endIndex >= data.length) { + endIndex = data.length - 1; + } + return endIndex; + } + + // Blocks of 12 for monthly (0-11) + let endIndex = index + 11; + if (index < offset) { + endIndex = offset - 1; + } + if (endIndex >= data.length) { + endIndex = data.length - 1; + } + return endIndex; + } + drawAxis(props) { if (!props.scale) { return; diff --git a/src/js/components/sharedComponents/LoadingSpinner.jsx b/src/js/components/sharedComponents/LoadingSpinner.jsx new file mode 100644 index 0000000000..90e9dcd8fb --- /dev/null +++ b/src/js/components/sharedComponents/LoadingSpinner.jsx @@ -0,0 +1,48 @@ +/** + * LoadingScreen.jsx + * Created by Kevin Li 12/20/17 + */ + +import React from 'react'; + +const LoadingSpinner = () => ( +
    + + + + + + + +
    +); + +export default LoadingSpinner; diff --git a/src/js/components/sharedComponents/icons/Icons.jsx b/src/js/components/sharedComponents/icons/Icons.jsx index b48d99f92a..f7c677dcc6 100644 --- a/src/js/components/sharedComponents/icons/Icons.jsx +++ b/src/js/components/sharedComponents/icons/Icons.jsx @@ -397,3 +397,9 @@ Recipient.defaultProps = { alt: 'Icon Depicting a Person Representing Recipients' }; +export class CircleArrowLeft extends BaseIcon {} +CircleArrowLeft.defaultProps = { + iconName: 'usa-da-circle-arrow-left', + iconClass: 'usa-da-circle-arrow-left', + alt: 'Icon Depicting an Arrow in a Circle Pointing Left' +}; diff --git a/src/js/containers/account/topFilterBar/AccountTopFilterBarContainer.jsx b/src/js/containers/account/topFilterBar/AccountTopFilterBarContainer.jsx index 8cce818479..96de37450d 100644 --- a/src/js/containers/account/topFilterBar/AccountTopFilterBarContainer.jsx +++ b/src/js/containers/account/topFilterBar/AccountTopFilterBarContainer.jsx @@ -10,7 +10,7 @@ import { connect } from 'react-redux'; import { orderBy } from 'lodash'; import moment from 'moment'; -import TopFilterBar from 'components/search/topFilterBar/TopFilterBar'; +import LegacyTopFilterBar from 'components/account/topFilterBar/LegacyTopFilterBar'; import { topFilterGroupGenerator } from 'components/account/topFilterBar/filterGroups/AccountTopFilterGroupGenerator'; @@ -161,7 +161,7 @@ export class AccountTopFilterBarContainer extends React.Component { count += filter.values.length; }); - output = ( { diff --git a/src/js/containers/bulkDownload/archive/AwardDataArchiveContainer.jsx b/src/js/containers/bulkDownload/archive/AwardDataArchiveContainer.jsx index 5179c764e3..5f8b9ad9e3 100644 --- a/src/js/containers/bulkDownload/archive/AwardDataArchiveContainer.jsx +++ b/src/js/containers/bulkDownload/archive/AwardDataArchiveContainer.jsx @@ -17,7 +17,7 @@ const columns = [ displayName: 'Agency' }, { - columnName: 'url', + columnName: 'fileName', displayName: 'Archive File' }, { @@ -155,7 +155,8 @@ export default class AwardDataArchiveContainer extends React.Component { const file = { agency: formattedAgency, - url: item.file_name, + fileName: item.file_name, + url: item.url, fy: formattedFY, date: formattedDate }; diff --git a/src/js/containers/bulkDownload/modal/BulkDownloadBottomBarContainer.jsx b/src/js/containers/bulkDownload/modal/BulkDownloadBottomBarContainer.jsx index 63b98168cc..f027a30bfc 100644 --- a/src/js/containers/bulkDownload/modal/BulkDownloadBottomBarContainer.jsx +++ b/src/js/containers/bulkDownload/modal/BulkDownloadBottomBarContainer.jsx @@ -32,8 +32,8 @@ export class BulkDownloadBottomBarContainer extends React.Component { visible: false, showError: false, showSuccess: false, - title: 'Your file is being generated...', - description: 'Warning: In order to complete your download, please remain on this site.' + title: 'We\'re preparing your download(s)...', + description: 'If you plan to leave the site, copy the download link before you go - you\'ll need it to access your file.' }; this.request = null; @@ -75,8 +75,8 @@ export class BulkDownloadBottomBarContainer extends React.Component { visible: true, showError: false, showSuccess: false, - title: 'Your file is being generated...', - description: 'Warning: In order to complete your download, please remain on this site.' + title: 'We\'re preparing your download(s)...', + description: 'If you plan to leave the site, copy the download link before you go - you\'ll need it to access your file.' }, this.checkStatus); } @@ -193,6 +193,7 @@ will no longer download to your computer. Are you sure you want to do this?`; if (this.state.visible) { content = ( { this.request = null; + this.props.setAppliedFilterEmptiness(false); this.applyFilters(res.data.filter); }) .catch((err) => { @@ -181,6 +194,8 @@ export class SearchContainer extends React.Component { hash: '', hashState: 'ready' }, () => { + this.props.setAppliedFilterEmptiness(true); + this.props.setAppliedFilterCompletion(true); Router.history.replace('/search'); }); } @@ -214,6 +229,8 @@ export class SearchContainer extends React.Component { }); this.props.populateAllSearchFilters(reduxValues); + // also overwrite the staged filters with the same values + this.props.applyStagedFilters(reduxValues); this.setState({ hashState: 'ready' @@ -265,13 +282,9 @@ export class SearchContainer extends React.Component { const unfiltered = this.determineIfUnfiltered(filters); if (unfiltered) { // all the filters were cleared, reset to a blank hash - this.setState({ - hash: '', - hashState: 'ready' - }, () => { - Router.history.replace('/search'); - }); - + this.props.setAppliedFilterEmptiness(true); + this.props.setAppliedFilterCompletion(true); + Router.history.replace('/search'); return; } @@ -327,6 +340,14 @@ export class SearchContainer extends React.Component { } requestDownloadAvailability(filters) { + if (this.determineIfUnfiltered(filters)) { + // don't make an API call when it's a blank state + this.setState({ + downloadAvailable: false + }); + return; + } + const operation = new SearchAwardsOperation(); operation.fromState(filters); const searchParams = operation.toParams(); @@ -364,20 +385,27 @@ export class SearchContainer extends React.Component { return ( + downloadAvailable={this.state.downloadAvailable} + download={this.props.download} + requestsComplete={this.props.appliedFilters._complete} /> ); } } export default connect( (state) => ({ - filters: state.filters + filters: state.filters, + download: state.download, + appliedFilters: state.appliedFilters }), (dispatch) => bindActionCreators(Object.assign({}, searchHashActions, { - clearAllFilters + clearAllFilters, + applyStagedFilters, + setAppliedFilterEmptiness, + setAppliedFilterCompletion }), dispatch) )(SearchContainer); diff --git a/src/js/containers/search/SearchSidebarSubmitContainer.jsx b/src/js/containers/search/SearchSidebarSubmitContainer.jsx new file mode 100644 index 0000000000..9124272aa9 --- /dev/null +++ b/src/js/containers/search/SearchSidebarSubmitContainer.jsx @@ -0,0 +1,124 @@ +/** + * SearchSidebarSubmitContainer.jsx + * Created by Kevin Li 12/21/17 + */ + +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'; +import { clearAllFilters as clearStagedFilters } from 'redux/actions/search/searchFilterActions'; + +import SearchSidebarSubmit from 'components/search/SearchSidebarSubmit'; + +const combinedActions = Object.assign({}, appliedFilterActions, { + clearStagedFilters +}); + +const propTypes = { + stagedFilters: PropTypes.object, + appliedFilters: PropTypes.object, + requestsComplete: PropTypes.bool, + applyStagedFilters: PropTypes.func, + clearStagedFilters: PropTypes.func, + setAppliedFilterCompletion: PropTypes.func, + resetAppliedFilters: PropTypes.func +}; + +export class SearchSidebarSubmitContainer extends React.Component { + constructor(props) { + super(props); + + this.state = { + filtersChanged: false + }; + + this.resetFilters = this.resetFilters.bind(this); + this.applyStagedFilters = this.applyStagedFilters.bind(this); + } + + componentDidUpdate(prevProps) { + if (prevProps.stagedFilters !== this.props.stagedFilters) { + this.stagingChanged(); + } + else if (prevProps.appliedFilters !== this.props.appliedFilters) { + this.stagingChanged(); + } + } + + compareStores() { + // we need to do a deep equality check by comparing every store key + const storeKeys = Object.keys(this.props.stagedFilters); + if (storeKeys.length !== Object.keys(this.props.appliedFilters).length) { + // key lengths do not match, there's a difference so fail immediately + return false; + } + + for (const key of storeKeys) { + if (!{}.hasOwnProperty.call(this.props.appliedFilters, key)) { + // no such key, immediately fail + return false; + } + + if (!is(this.props.appliedFilters[key], this.props.stagedFilters[key])) { + // use immutable to check equality of nested objects + return false; + } + } + + return true; + } + + stagingChanged() { + // do a deep equality check between the staged filters and applied filters + if (!this.compareStores()) { + this.setState({ + filtersChanged: true + }); + } + else if (this.state.filtersChanged) { + this.setState({ + filtersChanged: false + }); + } + } + + applyStagedFilters() { + this.props.setAppliedFilterCompletion(false); + this.props.applyStagedFilters(this.props.stagedFilters); + this.setState({ + filtersChanged: false + }); + } + + resetFilters() { + this.props.clearStagedFilters(); + this.props.resetAppliedFilters(); + } + + render() { + return ( + + ); + } +} + +export default connect( + (state) => ({ + requestsComplete: state.appliedFilters._complete, + isEmpty: state.appliedFilters._empty, + stagedFilters: state.filters, + appliedFilters: state.appliedFilters.filters + }), + (dispatch) => bindActionCreators(combinedActions, dispatch) +)(SearchSidebarSubmitContainer); + +SearchSidebarSubmitContainer.propTypes = propTypes; diff --git a/src/js/containers/search/filters/KeywordContainer.jsx b/src/js/containers/search/filters/KeywordContainer.jsx index 8d780af35f..daebd7c507 100644 --- a/src/js/containers/search/filters/KeywordContainer.jsx +++ b/src/js/containers/search/filters/KeywordContainer.jsx @@ -37,6 +37,7 @@ export class KeywordContainer extends React.Component { this.submitText = this.submitText.bind(this); this.changedInput = this.changedInput.bind(this); + this.removeKeyword = this.removeKeyword.bind(this); } componentWillMount() { @@ -73,12 +74,22 @@ export class KeywordContainer extends React.Component { } } + removeKeyword() { + this.setState({ + value: '' + }, () => { + this.submitText(); + }); + } + render() { return ( + submitText={this.submitText} + removeKeyword={this.removeKeyword} /> ); } } diff --git a/src/js/containers/search/filters/SearchSidebarContainer.jsx b/src/js/containers/search/filters/SearchSidebarContainer.jsx new file mode 100644 index 0000000000..6323013320 --- /dev/null +++ b/src/js/containers/search/filters/SearchSidebarContainer.jsx @@ -0,0 +1,34 @@ +/** + * SearchSidebarContainer.jsx + * Created by Kevin Li 12/19/17 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; + +import SearchSidebar from 'components/search/SearchSidebar'; + +import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; + +const propTypes = { + filters: PropTypes.object +}; + +export class SearchSidebarContainer extends React.Component { + render() { + return ( + + ); + } +} + +SearchSidebarContainer.propTypes = propTypes; + +export default connect( + (state) => ({ + filters: state.filters + }), + (dispatch) => bindActionCreators(searchFilterActions, dispatch) +)(SearchSidebarContainer); diff --git a/src/js/containers/search/modals/fullDownload/DownloadBottomBarContainer.jsx b/src/js/containers/search/modals/fullDownload/DownloadBottomBarContainer.jsx index 4b12a0da7a..8485ffe14e 100644 --- a/src/js/containers/search/modals/fullDownload/DownloadBottomBarContainer.jsx +++ b/src/js/containers/search/modals/fullDownload/DownloadBottomBarContainer.jsx @@ -22,6 +22,7 @@ const propTypes = { setDownloadPending: PropTypes.func, setDownloadCollapsed: PropTypes.func, setDownloadExpectedFile: PropTypes.func, + setDownloadExpectedUrl: PropTypes.func, resetDownload: PropTypes.func, filters: PropTypes.object }; @@ -34,8 +35,8 @@ export class DownloadBottomBarContainer extends React.Component { visible: false, showError: false, showSuccess: false, - title: 'Your file is being generated...', - description: 'Warning: In order to complete your download, please remain on this site.' + title: 'We\'re preparing your download(s)...', + description: 'If you plan to leave the site, copy the download link before you go - you\'ll need it to access your file.' }; this.request = null; @@ -79,8 +80,8 @@ export class DownloadBottomBarContainer extends React.Component { visible: true, showError: false, showSuccess: false, - title: 'Your file is being generated...', - description: 'Warning: In order to complete your download, please remain on this site.' + title: 'We\'re preparing your download(s)...', + description: 'If you plan to leave the site, copy the download link before you go - you\'ll need it to access your file.' }, this.checkStatus); } @@ -108,6 +109,7 @@ export class DownloadBottomBarContainer extends React.Component { this.request.promise .then((res) => { this.props.setDownloadExpectedFile(res.data.file_name); + this.props.setDownloadExpectedUrl(res.data.url); this.checkStatus(); }) .catch((err) => { diff --git a/src/js/containers/search/modals/fullDownload/FullDownloadModalContainer.jsx b/src/js/containers/search/modals/fullDownload/FullDownloadModalContainer.jsx index b65559d262..c81bf982ca 100644 --- a/src/js/containers/search/modals/fullDownload/FullDownloadModalContainer.jsx +++ b/src/js/containers/search/modals/fullDownload/FullDownloadModalContainer.jsx @@ -16,7 +16,8 @@ const propTypes = { mounted: PropTypes.bool, hideModal: PropTypes.func, setDownloadCollapsed: PropTypes.func, - pendingDownload: PropTypes.bool + pendingDownload: PropTypes.bool, + download: PropTypes.object }; export class FullDownloadModalContainer extends React.Component { @@ -25,6 +26,7 @@ export class FullDownloadModalContainer extends React.Component { ); @@ -34,6 +36,9 @@ export class FullDownloadModalContainer extends React.Component { FullDownloadModalContainer.propTypes = propTypes; export default connect( - (state) => ({ pendingDownload: state.download.pendingDownload }), + (state) => ({ + pendingDownload: state.download.pendingDownload, + download: state.download + }), (dispatch) => bindActionCreators(downloadActions, dispatch) )(FullDownloadModalContainer); diff --git a/src/js/containers/search/table/ResultsTableContainer.jsx b/src/js/containers/search/table/ResultsTableContainer.jsx index c2fac545e4..b727306dd7 100644 --- a/src/js/containers/search/table/ResultsTableContainer.jsx +++ b/src/js/containers/search/table/ResultsTableContainer.jsx @@ -23,13 +23,16 @@ import { measureTableHeader } from 'helpers/textMeasurement'; import ResultsTableSection from 'components/search/table/ResultsTableSection'; import SearchActions from 'redux/actions/searchActions'; +import * as appliedFilterActions from 'redux/actions/search/appliedFilterActions'; const propTypes = { filters: PropTypes.object, columnVisibility: PropTypes.object, toggleColumnVisibility: PropTypes.func, reorderColumns: PropTypes.func, - populateAvailableColumns: PropTypes.func + populateAvailableColumns: PropTypes.func, + setAppliedFilterCompletion: PropTypes.func, + noApplied: PropTypes.bool }; const tableTypes = [ @@ -76,6 +79,7 @@ export class ResultsTableContainer extends React.Component { direction: 'desc' }, inFlight: true, + error: false, results: [], tableInstance: `${uniqueId()}` // this will stay constant during pagination but will change when the filters or table type changes }; @@ -99,7 +103,7 @@ export class ResultsTableContainer extends React.Component { } componentDidUpdate(prevProps) { - if (prevProps.filters !== this.props.filters) { + if (prevProps.filters !== this.props.filters && !this.props.noApplied) { // filters changed, update the search object this.pickDefaultTab(); } @@ -123,8 +127,11 @@ export class ResultsTableContainer extends React.Component { this.tabCountRequest.cancel(); } + this.props.setAppliedFilterCompletion(false); + this.setState({ - inFlight: true + inFlight: true, + error: false }); const searchParams = new SearchAwardsOperation(); @@ -140,7 +147,15 @@ export class ResultsTableContainer extends React.Component { this.parseTabCounts(res.data); }) .catch((err) => { - console.log(err); + if (!isCancel(err)) { + this.setState({ + inFlight: false, + error: true + }); + this.props.setAppliedFilterCompletion(true); + + console.log(err); + } }); } @@ -240,6 +255,8 @@ export class ResultsTableContainer extends React.Component { this.searchRequest.cancel(); } + this.props.setAppliedFilterCompletion(false); + const tableType = this.state.tableType; // Append the current tab's award types to the search params if the Award Type filter @@ -263,7 +280,8 @@ export class ResultsTableContainer extends React.Component { // indicate the request is about to start this.setState({ - inFlight: true + inFlight: true, + error: false }); let pageNumber = this.state.page; @@ -325,20 +343,18 @@ export class ResultsTableContainer extends React.Component { newState.lastPage = !res.data.page_metadata.hasNext; this.setState(newState); + + this.props.setAppliedFilterCompletion(true); }) .catch((err) => { - if (isCancel(err)) { - // the request was cancelled - } - else if (err.response) { - // server responded with something - console.log(err); - this.searchRequest = null; - } - else { - // request never made it out + if (!isCancel(err)) { + this.setState({ + inFlight: false, + error: true + }); + this.props.setAppliedFilterCompletion(true); + console.log(err); - this.searchRequest = null; } }); } @@ -422,6 +438,7 @@ export class ResultsTableContainer extends React.Component { const tableType = this.state.tableType; return ( ({ - filters: state.filters, + filters: state.appliedFilters.filters, + noApplied: state.appliedFilters._empty, columnVisibility: state.columnVisibility }), - (dispatch) => bindActionCreators(SearchActions, dispatch) + (dispatch) => bindActionCreators(Object.assign({}, SearchActions, appliedFilterActions), dispatch) )(ResultsTableContainer); diff --git a/src/js/containers/search/topFilterBar/TopFilterBarContainer.jsx b/src/js/containers/search/topFilterBar/TopFilterBarContainer.jsx index bc7015b314..8ecc1080da 100644 --- a/src/js/containers/search/topFilterBar/TopFilterBarContainer.jsx +++ b/src/js/containers/search/topFilterBar/TopFilterBarContainer.jsx @@ -290,7 +290,7 @@ export class TopFilterBarContainer extends React.Component { if (selected) { filter.code = 'selectedLocations'; - filter.name = 'Place of Performance Location'; + filter.name = 'Place of Performance'; return filter; } return null; @@ -639,6 +639,6 @@ TopFilterBarContainer.propTypes = propTypes; TopFilterBarContainer.defaultProps = defaultProps; export default connect( - (state) => ({ reduxFilters: state.filters }), + (state) => ({ reduxFilters: state.appliedFilters.filters }), (dispatch) => bindActionCreators(searchFilterActions, dispatch) )(TopFilterBarContainer); diff --git a/src/js/containers/search/visualizations/geo/GeoVisualizationSectionContainer.jsx b/src/js/containers/search/visualizations/geo/GeoVisualizationSectionContainer.jsx index 52a2c3affa..b3172ff3f0 100644 --- a/src/js/containers/search/visualizations/geo/GeoVisualizationSectionContainer.jsx +++ b/src/js/containers/search/visualizations/geo/GeoVisualizationSectionContainer.jsx @@ -14,6 +14,7 @@ import GeoVisualizationSection from 'components/search/visualizations/geo/GeoVisualizationSection'; import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; +import { setAppliedFilterCompletion } from 'redux/actions/search/appliedFilterActions'; import * as SearchHelper from 'helpers/searchHelper'; import MapBroadcaster from 'helpers/mapBroadcaster'; @@ -22,7 +23,9 @@ import SearchAwardsOperation from 'models/search/SearchAwardsOperation'; const propTypes = { reduxFilters: PropTypes.object, - resultsMeta: PropTypes.object + resultsMeta: PropTypes.object, + setAppliedFilterCompletion: PropTypes.func, + noApplied: PropTypes.bool }; const apiScopes = { @@ -46,7 +49,7 @@ export class GeoVisualizationSectionContainer extends React.Component { renderHash: `geo-${uniqueId()}`, loading: true, loadingTiles: true, - message: '' + error: false }; this.apiRequest = null; @@ -70,7 +73,7 @@ export class GeoVisualizationSectionContainer extends React.Component { } componentDidUpdate(prevProps) { - if (!isEqual(prevProps.reduxFilters, this.props.reduxFilters)) { + if (!isEqual(prevProps.reduxFilters, this.props.reduxFilters) && !this.props.noApplied) { this.prepareFetch(true); } } @@ -162,6 +165,19 @@ export class GeoVisualizationSectionContainer extends React.Component { const operation = new SearchAwardsOperation(); operation.fromState(this.props.reduxFilters); + // if no entities are visible, don't make an API rquest because nothing in the US is visible + if (this.state.visibleEntities.length === 0) { + this.setState({ + loading: false, + error: false, + data: { + values: [], + locations: [] + } + }); + return; + } + const searchParams = operation.toParams(); // generate the API parameters @@ -179,9 +195,11 @@ export class GeoVisualizationSectionContainer extends React.Component { this.setState({ loading: true, - message: 'Loading data...' + error: false }); + this.props.setAppliedFilterCompletion(false); + this.apiRequest = SearchHelper.performSpendingByGeographySearch(apiParams); this.apiRequest.promise .then((res) => { @@ -195,8 +213,10 @@ export class GeoVisualizationSectionContainer extends React.Component { this.setState({ loading: false, - message: 'An error occurred while loading map data.' + error: true }); + + this.props.setAppliedFilterCompletion(true); } }); } @@ -218,20 +238,17 @@ export class GeoVisualizationSectionContainer extends React.Component { } }); - let message = ''; - if (data.results.length === 0) { - message = 'No results in the current map area.'; - } + this.props.setAppliedFilterCompletion(true); this.setState({ - message, data: { values: spendingValues, locations: spendingShapes, labels: spendingLabels }, renderHash: `geo-${uniqueId()}`, - loading: false + loading: false, + error: false }); } @@ -249,6 +266,7 @@ export class GeoVisualizationSectionContainer extends React.Component { return ( ); @@ -258,6 +276,11 @@ export class GeoVisualizationSectionContainer extends React.Component { GeoVisualizationSectionContainer.propTypes = propTypes; export default connect( - (state) => ({ reduxFilters: state.filters }), - (dispatch) => bindActionCreators(searchFilterActions, dispatch) + (state) => ({ + reduxFilters: state.appliedFilters.filters, + noApplied: state.appliedFilters._empty + }), + (dispatch) => bindActionCreators(Object.assign({}, searchFilterActions, { + setAppliedFilterCompletion + }), dispatch) )(GeoVisualizationSectionContainer); diff --git a/src/js/containers/search/visualizations/time/TimeVisualizationSectionContainer.jsx b/src/js/containers/search/visualizations/time/TimeVisualizationSectionContainer.jsx index 3d4bc1c759..f9daa3914e 100644 --- a/src/js/containers/search/visualizations/time/TimeVisualizationSectionContainer.jsx +++ b/src/js/containers/search/visualizations/time/TimeVisualizationSectionContainer.jsx @@ -8,22 +8,27 @@ import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { isEqual } from 'lodash'; +import { isCancel } from 'axios'; import TimeVisualizationSection from 'components/search/visualizations/time/TimeVisualizationSection'; import * as searchFilterActions from 'redux/actions/search/searchFilterActions'; +import { setAppliedFilterCompletion } from 'redux/actions/search/appliedFilterActions'; import * as SearchHelper from 'helpers/searchHelper'; import * as MonthHelper from 'helpers/monthHelper'; import SearchAwardsOperation from 'models/search/SearchAwardsOperation'; -const combinedActions = Object.assign({}, searchFilterActions); +const combinedActions = Object.assign({}, searchFilterActions, { + setAppliedFilterCompletion +}); const propTypes = { reduxFilters: PropTypes.object, - setVizTxnSum: PropTypes.func + setAppliedFilterCompletion: PropTypes.func, + noApplied: PropTypes.bool }; export class TimeVisualizationSectionContainer extends React.Component { @@ -33,6 +38,7 @@ export class TimeVisualizationSectionContainer extends React.Component { this.state = { visualizationPeriod: 'fiscal_year', loading: true, + error: false, groups: [], xSeries: [], ySeries: [] @@ -47,7 +53,7 @@ export class TimeVisualizationSectionContainer extends React.Component { } componentDidUpdate(prevProps) { - if (!isEqual(prevProps.reduxFilters, this.props.reduxFilters)) { + if (!isEqual(prevProps.reduxFilters, this.props.reduxFilters) && !this.props.noApplied) { this.fetchData(); } } @@ -61,8 +67,10 @@ export class TimeVisualizationSectionContainer extends React.Component { } fetchData() { + this.props.setAppliedFilterCompletion(false); this.setState({ - loading: true + loading: true, + error: false }); // Cancel API request if it exists @@ -96,8 +104,18 @@ export class TimeVisualizationSectionContainer extends React.Component { this.parseData(res.data, this.state.visualizationPeriod); this.apiRequest = null; }) - .catch(() => { + .catch((err) => { + if (isCancel(err)) { + return; + } + + this.props.setAppliedFilterCompletion(true); this.apiRequest = null; + console.log(err); + this.setState({ + loading: false, + error: true + }); }); } @@ -115,31 +133,52 @@ export class TimeVisualizationSectionContainer extends React.Component { return `${month} ${year}`; } + generateTimeRaw(group, timePeriod) { + if (group === 'fiscal_year') { + return { + period: null, + year: timePeriod.fiscal_year + }; + } + else if (group === 'quarter') { + return { + period: `Q${timePeriod.quarter}`, + year: `${timePeriod.fiscal_year}` + }; + } + + const month = MonthHelper.convertNumToShortMonth(timePeriod.month); + const year = MonthHelper.convertMonthToFY(timePeriod.month, timePeriod.fiscal_year); + + return { + period: `${month}`, + year: `${year}` + }; + } + parseData(data, group) { const groups = []; const xSeries = []; const ySeries = []; - - let totalSpending = 0; + const rawLabels = []; // iterate through each response object and break it up into groups, x series, and y series data.results.forEach((item) => { groups.push(this.generateTimeLabel(group, item.time_period)); + rawLabels.push(this.generateTimeRaw(group, item.time_period)); xSeries.push([this.generateTimeLabel(group, item.time_period)]); ySeries.push([parseFloat(item.aggregated_amount)]); - - totalSpending += parseFloat(item.aggregated_amount); }); this.setState({ groups, xSeries, ySeries, - loading: false + rawLabels, + loading: false, + error: false }, () => { - // save the total spending amount to Redux so all visualizations have access to this - // data - this.props.setVizTxnSum(totalSpending); + this.props.setAppliedFilterCompletion(true); }); } @@ -147,7 +186,8 @@ export class TimeVisualizationSectionContainer extends React.Component { return ( + updateVisualizationPeriod={this.updateVisualizationPeriod} + visualizationPeriod={this.state.visualizationPeriod} /> ); } } @@ -155,6 +195,9 @@ export class TimeVisualizationSectionContainer extends React.Component { TimeVisualizationSectionContainer.propTypes = propTypes; export default connect( - (state) => ({ reduxFilters: state.filters }), + (state) => ({ + reduxFilters: state.appliedFilters.filters, + noApplied: state.appliedFilters._empty + }), (dispatch) => bindActionCreators(combinedActions, dispatch) )(TimeVisualizationSectionContainer); diff --git a/src/js/models/search/SearchAwardsOperation.js b/src/js/models/search/SearchAwardsOperation.js index e1b9ed5172..305105987d 100644 --- a/src/js/models/search/SearchAwardsOperation.js +++ b/src/js/models/search/SearchAwardsOperation.js @@ -177,17 +177,20 @@ class SearchAwardsOperation { filters[rootKeys.recipients] = this.selectedRecipients; } - if (this.recipientDomesticForeign !== '' && this.recipientDomesticForeign !== 'all') { - filters[rootKeys.recipientLocationScope] = this.recipientDomesticForeign; - } - if (this.selectedRecipientLocations.length > 0) { const locationSet = []; this.selectedRecipientLocations.forEach((location) => { - locationSet.push(location.filter); + if (location.filter.country && location.filter.country === 'FOREIGN') { + filters[rootKeys.recipientLocationScope] = 'foreign'; + } + else { + locationSet.push(location.filter); + } }); - filters[rootKeys.recipientLocation] = locationSet; + if (locationSet.length > 0) { + filters[rootKeys.recipientLocation] = locationSet; + } } if (this.recipientType.length > 0) { @@ -198,14 +201,17 @@ class SearchAwardsOperation { if (this.selectedLocations.length > 0) { const locationSet = []; this.selectedLocations.forEach((location) => { - locationSet.push(location.filter); + if (location.filter.country && location.filter.country === 'FOREIGN') { + filters[rootKeys.placeOfPerformanceScope] = 'foreign'; + } + else { + locationSet.push(location.filter); + } }); - filters[rootKeys.placeOfPerformance] = locationSet; - } - - if (this.locationDomesticForeign !== '' && this.locationDomesticForeign !== 'all') { - filters[rootKeys.placeOfPerformanceScope] = this.locationDomesticForeign; + if (locationSet.length > 0) { + filters[rootKeys.placeOfPerformance] = locationSet; + } } // Add Award Amounts diff --git a/src/js/redux/actions/search/appliedFilterActions.js b/src/js/redux/actions/search/appliedFilterActions.js new file mode 100644 index 0000000000..5ca67ce8d7 --- /dev/null +++ b/src/js/redux/actions/search/appliedFilterActions.js @@ -0,0 +1,23 @@ +/** + * appliedFilterActions.js + * Created by Kevin Li 12/21/17 + */ + +export const setAppliedFilterCompletion = (complete) => ({ + complete, + type: 'SET_APPLIED_FILTER_COMPLETION' +}); + +export const setAppliedFilterEmptiness = (empty) => ({ + empty, + type: 'SET_APPLIED_FILTER_EMPTINESS' +}); + +export const applyStagedFilters = (filters) => ({ + filters, + type: 'APPLY_STAGED_FILTERS' +}); + +export const resetAppliedFilters = () => ({ + type: 'CLEAR_APPLIED_FILTERS' +}); diff --git a/src/js/redux/actions/search/downloadActions.js b/src/js/redux/actions/search/downloadActions.js index 741718be11..53e7a196d3 100644 --- a/src/js/redux/actions/search/downloadActions.js +++ b/src/js/redux/actions/search/downloadActions.js @@ -18,6 +18,11 @@ export const setDownloadExpectedFile = (state) => ({ file: state }); +export const setDownloadExpectedUrl = (state) => ({ + type: 'SET_DOWNLOAD_EXPECTED_URL', + url: state +}); + export const setDownloadPending = (state) => ({ state, type: 'SET_DOWNLOAD_PENDING' diff --git a/src/js/redux/reducers/index.js b/src/js/redux/reducers/index.js index 537d62ba32..ae587715f9 100644 --- a/src/js/redux/reducers/index.js +++ b/src/js/redux/reducers/index.js @@ -6,6 +6,7 @@ import { combineReducers } from 'redux'; import filtersReducer from './search/searchFiltersReducer'; +import appliedFiltersReducer from './search/appliedFiltersReducer'; import columnVisibilityReducer from './search/columnVisibilityReducer'; import awardReducer from './award/awardReducer'; import accountReducer from './account/accountReducer'; @@ -18,6 +19,7 @@ import bulkDownloadReducer from './bulkDownload/bulkDownloadReducer'; const appReducer = combineReducers({ filters: filtersReducer, + appliedFilters: appliedFiltersReducer, columnVisibility: columnVisibilityReducer, download: downloadReducer, award: awardReducer, diff --git a/src/js/redux/reducers/search/appliedFiltersReducer.js b/src/js/redux/reducers/search/appliedFiltersReducer.js new file mode 100644 index 0000000000..5bd346e36e --- /dev/null +++ b/src/js/redux/reducers/search/appliedFiltersReducer.js @@ -0,0 +1,35 @@ +/** + * appliedFiltersReducer.js + * Created by Kevin Li 12/20/17 + */ + +import { initialState as defaultFilters } from './searchFiltersReducer'; + +export const initialState = { + filters: defaultFilters, + _empty: true, + _complete: true +}; + +const appliedFiltersReducer = (state = initialState, action) => { + switch (action.type) { + case 'APPLY_STAGED_FILTERS': + return Object.assign({}, state, { + filters: action.filters + }); + case 'CLEAR_APPLIED_FILTERS': + return Object.assign({}, initialState); + case 'SET_APPLIED_FILTER_EMPTINESS': + return Object.assign({}, state, { + _empty: action.empty + }); + case 'SET_APPLIED_FILTER_COMPLETION': + return Object.assign({}, state, { + _complete: action.complete + }); + default: + return state; + } +}; + +export default appliedFiltersReducer; diff --git a/src/js/redux/reducers/search/downloadReducer.js b/src/js/redux/reducers/search/downloadReducer.js index 79b7582d2f..367d08e8c8 100644 --- a/src/js/redux/reducers/search/downloadReducer.js +++ b/src/js/redux/reducers/search/downloadReducer.js @@ -9,6 +9,7 @@ export const initialState = { type: 'award', columns: new List(), expectedFile: '', + expectedUrl: '', pendingDownload: false, showCollapsedProgress: false }; @@ -30,6 +31,11 @@ const downloadReducer = (state = initialState, action) => { expectedFile: action.file }); } + case 'SET_DOWNLOAD_EXPECTED_URL': { + return Object.assign({}, state, { + expectedUrl: action.url + }); + } case 'SET_DOWNLOAD_PENDING': { return Object.assign({}, state, { pendingDownload: action.state diff --git a/src/js/redux/reducers/search/searchFiltersReducer.js b/src/js/redux/reducers/search/searchFiltersReducer.js index 1bdcb1a282..e33f6dd968 100644 --- a/src/js/redux/reducers/search/searchFiltersReducer.js +++ b/src/js/redux/reducers/search/searchFiltersReducer.js @@ -11,7 +11,6 @@ import * as AgencyFilterFunctions from './filters/agencyFilterFunctions'; import * as RecipientFilterFunctions from './filters/recipientFilterFunctions'; import * as AwardAmountFilterFunctions from './filters/awardAmountFilterFunctions'; import * as OtherFilterFunctions from './filters/OtherFilterFunctions'; -import * as FiscalYearHelper from '../../../helpers/fiscalYearHelper'; import * as ContractFilterFunctions from './filters/contractFilterFunctions'; // update this version when changes to the reducer structure are made @@ -41,7 +40,7 @@ export const requiredTypes = { export const initialState = { keyword: '', timePeriodType: 'fy', - timePeriodFY: new Set([`${FiscalYearHelper.currentFiscalYear()}`]), + timePeriodFY: new Set(), timePeriodStart: null, timePeriodEnd: null, selectedLocations: new OrderedMap(), diff --git a/tests/containers/account/topFilterBar/AccountTopFilterBarContainer-test.jsx b/tests/containers/account/topFilterBar/AccountTopFilterBarContainer-test.jsx index 12bf0ab25d..8737359814 100644 --- a/tests/containers/account/topFilterBar/AccountTopFilterBarContainer-test.jsx +++ b/tests/containers/account/topFilterBar/AccountTopFilterBarContainer-test.jsx @@ -16,7 +16,7 @@ import { defaultFilters } from '../defaultFilters'; const prepareFiltersSpy = sinon.spy(AccountTopFilterBarContainer.prototype, 'prepareFilters'); // mock the child component by replacing it with a function that returns a null element -jest.mock('components/search/topFilterBar/TopFilterBar', () => +jest.mock('components/account/topFilterBar/LegacyTopFilterBar', () => jest.fn(() => null)); describe('AccountTopFilterBarContainer', () => { diff --git a/tests/containers/bulkDownload/archive/AwardDataArchiveContainer-test.jsx b/tests/containers/bulkDownload/archive/AwardDataArchiveContainer-test.jsx index 186144bc23..b594989177 100644 --- a/tests/containers/bulkDownload/archive/AwardDataArchiveContainer-test.jsx +++ b/tests/containers/bulkDownload/archive/AwardDataArchiveContainer-test.jsx @@ -83,13 +83,15 @@ describe('AwardDataArchiveContainer', () => { const formattedResults = [ { agency: "Mock Agency 1 (ABC)", - url: "mockFile1.zip", + fileName: "mockFile1.zip", + url: "http://mockFile_full.zip", fy: "FY 1988", date: "12/12/1987" }, { agency: "Mock Agency 2 (DEF)", - url: "mockFile2.zip", + fileName: "mockFile2.zip", + url: "http://mockFile_delta.zip", fy: "FY 1988", date: "12/18/1987" } diff --git a/tests/containers/bulkDownload/mockData.js b/tests/containers/bulkDownload/mockData.js index ed73c01fb0..e7b618346c 100644 --- a/tests/containers/bulkDownload/mockData.js +++ b/tests/containers/bulkDownload/mockData.js @@ -40,12 +40,10 @@ export const mockAgencies = { export const mockSubAgencies = [ { - subtier_agency_name: "Subtier Agency 1", - subtier_agency_id: 5 + subtier_agency_name: "Subtier Agency 1" }, { - subtier_agency_name: "Subtier Agency 2", - subtier_agency_id: 6 + subtier_agency_name: "Subtier Agency 2" } ]; @@ -69,7 +67,6 @@ export const mockRedux = { name: 'Mock Agency' }, subAgency: { - id: '456', name: 'Mock Sub-Agency' }, dateType: 'action_date', diff --git a/tests/containers/search/SearchContainer-test.jsx b/tests/containers/search/SearchContainer-test.jsx index 283ada76e3..5bed7a43ba 100644 --- a/tests/containers/search/SearchContainer-test.jsx +++ b/tests/containers/search/SearchContainer-test.jsx @@ -5,23 +5,20 @@ import React from 'react'; import { shallow } from 'enzyme'; -import sinon from 'sinon'; import { Set } from 'immutable'; import { SearchContainer } from 'containers/search/SearchContainer'; import * as SearchHelper from 'helpers/searchHelper'; import { initialState } from 'redux/reducers/search/searchFiltersReducer'; -import Router from 'containers/router/Router'; +import { initialState as initialApplied } from 'redux/reducers/search/appliedFiltersReducer'; import { mockHash, mockFilters, mockRedux, mockActions } from './mockSearchHashes'; +import Router from './mockRouter'; // force Jest to use native Node promises // see: https://facebook.github.io/jest/docs/troubleshooting.html#unresolved-promises global.Promise = require.requireActual('promise'); -// spy on specific functions inside the component -const routerReplaceSpy = sinon.spy(Router.history, 'replace'); - // mock the child component by replacing it with a function that returns a null element jest.mock('components/search/SearchPage', () => jest.fn(() => null)); @@ -29,10 +26,10 @@ jest.mock('components/search/SearchPage', () => jest.mock('helpers/searchHelper', () => require('./filters/searchHelper')); jest.mock('helpers/fiscalYearHelper', () => require('./filters/fiscalYearHelper')); jest.mock('helpers/downloadHelper', () => require('./modals/fullDownload/downloadHelper')); +jest.mock('containers/router/Router', () => require('./mockRouter')); jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; - describe('SearchContainer', () => { it('should try to resolve the current URL hash on mount', () => { const container = shallow( { const receiveHash = jest.fn(); container.instance().receiveHash = receiveHash; - container.instance().componentWillReceiveProps(Object.assign({}, container.props(), { + container.instance().componentWillReceiveProps(Object.assign({}, mockActions, mockRedux, { params: { hash: '11111' } @@ -104,7 +101,9 @@ describe('SearchContainer', () => { }); const nextProps = Object.assign({}, mockRedux, mockActions, { - filters: nextFilters + appliedFilters: Object.assign({}, initialApplied, { + filters: nextFilters + }) }); const generateHash = jest.fn(); @@ -151,11 +150,8 @@ describe('SearchContainer', () => { const generateHash = jest.fn(); container.instance().generateHash = generateHash; - routerReplaceSpy.reset(); container.instance().generateInitialHash(); - expect(routerReplaceSpy.callCount).toEqual(1); - expect(routerReplaceSpy.calledWith('/search')).toBeTruthy(); - routerReplaceSpy.reset(); + expect(Router.history.replace).toHaveBeenLastCalledWith('/search'); expect(generateHash).toHaveBeenCalledTimes(0); }); @@ -166,7 +162,9 @@ describe('SearchContainer', () => { }); const redux = Object.assign({}, mockRedux, { - filters + appliedFilters: { + filters + } }); const container = shallow( { {...mockActions} {...mockRedux} />); - routerReplaceSpy.reset(); container.instance().provideHash('12345'); - expect(routerReplaceSpy.callCount).toEqual(1); - expect(routerReplaceSpy.calledWith('/search/12345')).toBeTruthy(); - routerReplaceSpy.reset(); + expect(Router.history.replace).toHaveBeenLastCalledWith('/search/12345'); expect(container.state().hash).toEqual('12345'); }); @@ -249,9 +244,9 @@ describe('SearchContainer', () => { it('should trigger a Redux action to apply the filters', () => { const populateAction = jest.fn(); - const actions = { + const actions = Object.assign({}, mockActions, { populateAllSearchFilters: populateAction - }; + }); const container = shallow( { }); describe('requestDownloadAvailability', () => { + it('should not make an API requets if the applied filter state equals the initial blank filter state', () => { + const blankFilters = Object.assign({}, initialState); + const container = shallow(); + + const mockParse = jest.fn(); + container.instance().parseDownloadAvailability = mockParse; + + container.setState({ + downloadAvailable: true + }); + + container.instance().requestDownloadAvailability(blankFilters); + + expect(mockParse).toHaveBeenCalledTimes(0); + expect(container.state().downloadAvailable).toBeFalsy(); + + }); it('should make an API request for how many transaction rows will be returned', async () => { + const newFilters = Object.assign({}, initialState, { + timePeriodFY: new Set(['1990']) + }); + const container = shallow(); @@ -315,7 +333,7 @@ describe('SearchContainer', () => { const mockParse = jest.fn(); container.instance().parseDownloadAvailability = mockParse; - container.instance().requestDownloadAvailability(mockRedux.filters); + container.instance().requestDownloadAvailability(newFilters); await container.instance().downloadRequest.promise; expect(mockParse).toHaveBeenCalledWith({ diff --git a/tests/containers/search/SearchSidebarSubmitContainer-test.jsx b/tests/containers/search/SearchSidebarSubmitContainer-test.jsx new file mode 100644 index 0000000000..57ac5b67b8 --- /dev/null +++ b/tests/containers/search/SearchSidebarSubmitContainer-test.jsx @@ -0,0 +1,169 @@ +/** + * SearchSidebarSubmitContainer-test.jsx + * Created by Kevin Li 12/28/17 + */ + +import React from 'react'; +import { mount, shallow } from 'enzyme'; +import { Set } from 'immutable'; + +import { initialState as initialApplied } from 'redux/reducers/search/appliedFiltersReducer' +import { initialState as initialStaged } from 'redux/reducers/search/searchFiltersReducer' + +import { SearchSidebarSubmitContainer } from 'containers/search/SearchSidebarSubmitContainer'; + +import { mockActions, mockRedux } from './mockSubmit'; + +// mock the child component by replacing it with a function that returns a null element +jest.mock('components/search/SearchSidebarSubmit', () => + jest.fn(() => null)); + +describe('SearchSidebarSubmitContainer', () => { + describe('compareStores', () => { + it('should return false if the length of enumerable properties on the applied filter object is different from the length of enumerable properties on the staged filter object', () => { + const changedStage = Object.assign({}, initialStaged, { + bonusFilter: 'hello' + }); + + const redux = Object.assign({}, mockRedux, { + stagedFilters: Object.assign({}, mockRedux.stagedFilters, changedStage) + }); + + const container = shallow( + + ); + const compare = container.instance().compareStores(); + expect(compare).toBeFalsy(); + }); + + it('should return false if any item in the staged filter object does not equal the same key value in the applied filter object', () => { + const changedStage = Object.assign({}, initialStaged, { + timePeriodFY: new Set(['1995']) + }); + + const redux = Object.assign({}, mockRedux, { + stagedFilters: Object.assign({}, mockRedux.stagedFilters, changedStage) + }); + + const container = shallow( + + ); + const compare = container.instance().compareStores(); + expect(compare).toBeFalsy(); + }); + + it('should return true if all key values are equal in both the staged and applied filter objects', () => { + const changedStage = Object.assign({}, initialStaged, { + timePeriodFY: new Set(['1995']) + }); + const changedApplied = Object.assign({}, initialApplied.filters, { + timePeriodFY: new Set(['1995']) + }); + + const redux = Object.assign({}, mockRedux, { + stagedFilters: Object.assign({}, mockRedux.stagedFilters, changedStage), + appliedFilters: Object.assign({}, mockRedux.appliedFilters, changedApplied), + }); + + const container = shallow( + + ); + const compare = container.instance().compareStores(); + expect(compare).toBeTruthy(); + }); + }); + describe('stagingChanged', () => { + it('should set the filtersChanged state to true when the stores are not equal', () => { + const container = shallow( + + ); + container.instance().compareStores = jest.fn(() => false); + + container.instance().stagingChanged(); + expect(container.state().filtersChanged).toBeTruthy(); + }); + it('should set the filtersChanged state to false when the stores are equal and the filtersChanged state was previously true', () => { + const container = shallow( + + ); + container.instance().compareStores = jest.fn(() => true); + container.setState({ + filtersChanged: true + }); + + container.instance().stagingChanged(); + expect(container.state().filtersChanged).toBeFalsy(); + }); + }); + describe('applyStagedFilters', () => { + it('should tell Redux to copy the staged filter set to the applied filter set', () => { + const actions = Object.assign({}, mockActions, { + applyStagedFilters: jest.fn() + }); + + const container = shallow( + + ); + container.instance().applyStagedFilters(); + + expect(actions.applyStagedFilters).toHaveBeenCalledTimes(1); + }); + + it('should reset the filtersChanged state to false', () => { + const container = shallow( + + ); + container.setState({ + filtersChanged: true + }); + + container.instance().applyStagedFilters(); + + expect(container.state().filtersChanged).toBeFalsy(); + }); + }); + describe('resetFilters', () => { + it('should reset all the staged filters to their initial states', () => { + const actions = Object.assign({}, mockActions, { + clearStagedFilters: jest.fn() + }); + + const container = shallow( + + ); + + container.instance().resetFilters(); + expect(actions.clearStagedFilters).toHaveBeenCalledTimes(1); + }); + it('should reset all the applied filters to their initial states', () => { + const actions = Object.assign({}, mockActions, { + resetAppliedFilters: jest.fn() + }); + + const container = shallow( + + ); + + container.instance().resetFilters(); + expect(actions.resetAppliedFilters).toHaveBeenCalledTimes(1); + }); + }); +}); \ No newline at end of file diff --git a/tests/containers/search/mockRouter.js b/tests/containers/search/mockRouter.js new file mode 100644 index 0000000000..8f68fa7e07 --- /dev/null +++ b/tests/containers/search/mockRouter.js @@ -0,0 +1,7 @@ +const Router = { + history: { + replace: jest.fn() + } +}; + +export default Router; diff --git a/tests/containers/search/mockSearchHashes.js b/tests/containers/search/mockSearchHashes.js index dc41555ab2..3936c273a8 100644 --- a/tests/containers/search/mockSearchHashes.js +++ b/tests/containers/search/mockSearchHashes.js @@ -1,4 +1,5 @@ import { initialState, filterStoreVersion } from 'redux/reducers/search/searchFiltersReducer'; +import { initialState as initialApplied } from 'redux/reducers/search/appliedFiltersReducer'; import * as FiscalYearHelper from 'helpers/fiscalYearHelper'; export const mockHash = { @@ -37,11 +38,15 @@ export const mockFilters = { export const mockRedux = { filters: initialState, + appliedFilters: initialApplied, params: { hash: '' } }; export const mockActions = { - populateAllSearchFilters: jest.fn() + populateAllSearchFilters: jest.fn(), + applyStagedFilters: jest.fn(), + setAppliedFilterEmptiness: jest.fn(), + setAppliedFilterCompletion: jest.fn() }; diff --git a/tests/containers/search/mockSubmit.js b/tests/containers/search/mockSubmit.js new file mode 100644 index 0000000000..631483ce4d --- /dev/null +++ b/tests/containers/search/mockSubmit.js @@ -0,0 +1,15 @@ +import { initialState as initialApplied } from 'redux/reducers/search/appliedFiltersReducer' +import { initialState as initialStaged } from 'redux/reducers/search/searchFiltersReducer' + +export const mockRedux = { + requestsComplete: true, + stagedFilters: initialStaged, + appliedFilters: initialApplied.filters +}; + +export const mockActions = { + applyStagedFilters: jest.fn(), + clearStagedFilters: jest.fn(), + setAppliedFilterCompletion: jest.fn(), + resetAppliedFilters: jest.fn() +}; \ No newline at end of file diff --git a/tests/containers/search/modals/fullDownload/mockFullDownload.js b/tests/containers/search/modals/fullDownload/mockFullDownload.js index 98025d8de0..4c80f1b9e5 100644 --- a/tests/containers/search/modals/fullDownload/mockFullDownload.js +++ b/tests/containers/search/modals/fullDownload/mockFullDownload.js @@ -5,7 +5,8 @@ export const mockRedux = { download: Object.assign({}, initialState, { pendingDownload: false, showCollapsedProgress: false, - expectedFile: '' + expectedFile: '', + expectedUrl: '' }), filters: initialFilter }; @@ -14,6 +15,7 @@ export const mockActions = { setDownloadPending: jest.fn(), setDownloadCollapsed: jest.fn(), setDownloadExpectedFile: jest.fn(), + setDownloadExpectedUrl: jest.fn(), resetDownload: jest.fn() }; diff --git a/tests/containers/search/table/mockAwards.js b/tests/containers/search/table/mockAwards.js index bf365d5fe7..d2bbb55cd6 100644 --- a/tests/containers/search/table/mockAwards.js +++ b/tests/containers/search/table/mockAwards.js @@ -6,11 +6,13 @@ import { initialState } from 'redux/reducers/search/searchFiltersReducer'; export const mockActions = { toggleColumnVisibility: jest.fn(), reorderColumns: jest.fn(), - populateAvailableColumns: jest.fn() + populateAvailableColumns: jest.fn(), + setAppliedFilterCompletion: jest.fn() }; export const mockRedux = { filters: initialState, + noApplied: false, columnVisibility: new VisibilityRecord() }; diff --git a/tests/containers/search/topFilterBar/TopFilterBarContainer-test.jsx b/tests/containers/search/topFilterBar/TopFilterBarContainer-test.jsx index 5da35df753..9c5cc3e0d1 100644 --- a/tests/containers/search/topFilterBar/TopFilterBarContainer-test.jsx +++ b/tests/containers/search/topFilterBar/TopFilterBarContainer-test.jsx @@ -5,7 +5,6 @@ import React from 'react'; import { mount } from 'enzyme'; -import sinon from 'sinon'; import { Set, OrderedMap } from 'immutable'; @@ -34,16 +33,14 @@ const defaultProps = { const setup = (props) => mount(); -const prepareFiltersSpy = sinon.spy(TopFilterBarContainer.prototype, 'prepareFilters'); - describe('TopFilterBarContainer', () => { - it('should return a TopFilterBar child component with FY17 selected on load', () => { + it('should return a TopFilterBar child component with no filters selected by default', () => { const topBarContainer = setup({ reduxFilters: initialState, updateFilterCount: jest.fn() }); - expect(topBarContainer.find(TopFilterBar)).toHaveLength(1); + expect(topBarContainer.find(TopFilterBar)).toHaveLength(0); }); it('should return a TopFilterBar child component when there are active filters', () => { @@ -85,12 +82,13 @@ describe('TopFilterBarContainer', () => { // mount the container const topBarContainer = setup(initialProps); + topBarContainer.instance().prepareFilters = jest.fn(); // change the props topBarContainer.setProps(updatedProps); // the prepareFilters function should have been called - expect(prepareFiltersSpy.called).toBeTruthy(); + expect(topBarContainer.instance().prepareFilters).toHaveBeenCalledTimes(1); }); it('should update component state with Redux keyword filter when available', () => { @@ -208,7 +206,7 @@ describe('TopFilterBarContainer', () => { const filterItem = topBarContainer.state().filters[0]; const expectedFilterState = { code: 'selectedLocations', - name: 'Place of Performance Location', + name: 'Place of Performance', scope: 'all', values: [{ filter: { diff --git a/tests/containers/search/visualizations/geo/GeoVisualizationSectionContainer-test.jsx b/tests/containers/search/visualizations/geo/GeoVisualizationSectionContainer-test.jsx index 0c9d644a5d..6f492fd21e 100644 --- a/tests/containers/search/visualizations/geo/GeoVisualizationSectionContainer-test.jsx +++ b/tests/containers/search/visualizations/geo/GeoVisualizationSectionContainer-test.jsx @@ -4,7 +4,7 @@ */ import React from 'react'; -import { mount } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import sinon from 'sinon'; import { Set } from 'immutable'; @@ -14,6 +14,7 @@ import { GeoVisualizationSectionContainer } from import MapBroadcaster from 'helpers/mapBroadcaster'; import { defaultFilters } from '../../../../testResources/defaultReduxFilters'; +import { geo as mockApi } from '../mockVisualizations'; jest.mock('helpers/searchHelper', () => require('./mocks/geoHelper')); jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; @@ -38,6 +39,7 @@ describe('GeoVisualizationSectionContainer', () => { // mount the container const container = mount(); expect(fetchDataSpy.callCount).toEqual(0); @@ -64,6 +66,7 @@ describe('GeoVisualizationSectionContainer', () => { // mount the container const container = mount(); @@ -86,6 +89,7 @@ describe('GeoVisualizationSectionContainer', () => { it('should set the scope to place of performance when requested', () => { // mount the container const container = mount(); @@ -98,6 +102,7 @@ describe('GeoVisualizationSectionContainer', () => { it('should set the scope to recipient when requested', () => { // mount the container const container = mount(); @@ -109,6 +114,7 @@ describe('GeoVisualizationSectionContainer', () => { it('should request a map measurement operation if the scope has changed', () => { const container = mount(); const mockPrepare = jest.fn(); @@ -123,6 +129,7 @@ describe('GeoVisualizationSectionContainer', () => { it('should not request a map measurement operation if the scope has not changed', () => { const container = mount(); const mockPrepare = jest.fn(); @@ -139,6 +146,7 @@ describe('GeoVisualizationSectionContainer', () => { describe('mapLoaded', () => { it('should set the loadingTiles state to false', () => { const container = mount(); container.instance().mapLoaded(); @@ -148,6 +156,7 @@ describe('GeoVisualizationSectionContainer', () => { it('should call the prepareFetch method', () => { jest.useFakeTimers(); const container = mount(); const mockPrepare = jest.fn(); @@ -166,6 +175,7 @@ describe('GeoVisualizationSectionContainer', () => { const attached = MapBroadcaster.on('measureMap', mockListener); const container = mount(); container.setState({ @@ -186,6 +196,7 @@ describe('GeoVisualizationSectionContainer', () => { const attached = MapBroadcaster.on('measureMap', mockListener); const container = mount(); container.setState({ @@ -203,6 +214,7 @@ describe('GeoVisualizationSectionContainer', () => { describe('receivedEntities', () => { it('should set the state to the returned entities', () => { const container = mount(); container.setState({ @@ -216,6 +228,7 @@ describe('GeoVisualizationSectionContainer', () => { it('should make an API call using the returned entities', () => { const container = mount(); const mockFetch = jest.fn(); @@ -232,14 +245,14 @@ describe('GeoVisualizationSectionContainer', () => { }); describe('parseData', () => { - it('should properly resture the API response for the map visualization', async () => { + it('should properly parse the API response for the map visualization', () => { // mount the container - const container = mount(); - container.instance().fetchData(); - await container.instance().apiRequest.promise; + container.instance().parseData(mockApi); const expectedState = { values: [123.12, 345.56], @@ -265,6 +278,7 @@ describe('GeoVisualizationSectionContainer', () => { describe('changeMapLayer', () => { it('should update the mapLayer state when a new map tileset is requested', () => { const container = mount(); expect(container.state().mapLayer).toEqual('state'); @@ -274,6 +288,7 @@ describe('GeoVisualizationSectionContainer', () => { }); it('should make a new renderHash when a new map tileset is requested', () => { const container = mount(); const originalHash = `${container.state().renderHash}`; @@ -283,6 +298,7 @@ describe('GeoVisualizationSectionContainer', () => { }); it('should update the mapLayer state when a new map tileset is requested', () => { const container = mount(); container.setState({ @@ -296,6 +312,7 @@ describe('GeoVisualizationSectionContainer', () => { }); it('should request a map measurement operation', () => { const container = mount(); const mockPrepare = jest.fn(); diff --git a/tests/containers/search/visualizations/time/TimeVisualizationSectionContainer-test.jsx b/tests/containers/search/visualizations/time/TimeVisualizationSectionContainer-test.jsx index f7bbca404f..a26c82da7b 100644 --- a/tests/containers/search/visualizations/time/TimeVisualizationSectionContainer-test.jsx +++ b/tests/containers/search/visualizations/time/TimeVisualizationSectionContainer-test.jsx @@ -5,7 +5,6 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; -import sinon from 'sinon'; import { Set } from 'immutable'; @@ -14,304 +13,127 @@ import { TimeVisualizationSectionContainer } from import * as SearchHelper from 'helpers/searchHelper'; import { defaultFilters } from '../../../../testResources/defaultReduxFilters'; +import { mockActions, mockApi, mockQuarters, mockMonths } from './mockData'; -// force Jest to use native Node promises -// see: https://facebook.github.io/jest/docs/troubleshooting.html#unresolved-promises -global.Promise = require.requireActual('promise'); +jest.mock('helpers/searchHelper', () => require('./mockSearchHelper')); -// spy on specific functions inside the component -const fetchDataSpy = sinon.spy(TimeVisualizationSectionContainer.prototype, 'fetchData'); - -// we don't want to actually hit the API because tests should be fully controlled, so we will mock -// the SearchHelper functions -const mockSearchHelper = (functionName, event, expectedResponse) => { - jest.useFakeTimers(); - // override the specified function - SearchHelper[functionName] = jest.fn(() => { - // Axios normally returns a promise, replicate this, but return the expected result - const networkCall = new Promise((resolve, reject) => { - process.nextTick(() => { - if (event === 'resolve') { - resolve({ - data: expectedResponse - }); - } - else { - reject({ - data: expectedResponse - }); - } - }); - }); - - return { - promise: networkCall, - cancel: jest.fn() - }; - }); -}; - -const unmockSearchHelper = () => { - jest.useRealTimers(); - jest.unmock('helpers/searchHelper'); -}; +jest.mock('components/search/visualizations/time/TimeVisualizationSection', () => + jest.fn(() => null)); describe('TimeVisualizationSectionContainer', () => { it('should make an API request on mount', () => { - // create a mock API response - const apiResponse = { - page_metadata: { - has_next_page: false, - has_previous_page: false, - next: null, - page: 1, - previous: null - }, - results: [{ - item: '2013', - aggregate: '1234' - }], - total_metadata: { - count: 1 - } - }; - - // mock the search helper to resolve with the mocked response - mockSearchHelper('performSpendingOverTimeSearch', 'resolve', apiResponse); - // mount the container - mount(); + const container = shallow(); + container.instance().fetchAwards = jest.fn(); - // the mocked SearchHelper waits 1 tick to resolve the promise, so wait for the tick - jest.runAllTicks(); + container.instance().componentDidMount(); // everything should be updated now - expect(fetchDataSpy.callCount).toEqual(1); - - // reset the mocks and spies - unmockSearchHelper(); - fetchDataSpy.reset(); + expect(container.instance().fetchAwards).toHaveBeenCalledTimes(1); }); it('should make an API request when the Redux filters change', () => { - // create a mock API response - const apiResponse = { - page_metadata: { - has_next_page: false, - has_previous_page: false, - next: null, - page: 1, - previous: null - }, - results: [{ - item: '2013', - aggregate: '1234' - }], - total_metadata: { - count: 1 - } - }; - - // mock the search helper to resolve with the mocked response - mockSearchHelper('performSpendingOverTimeSearch', 'resolve', apiResponse); - - const initialFilters = Object.assign({}, defaultFilters); - const secondFilters = Object.assign({}, defaultFilters, { + const mockFilters = Object.assign({}, defaultFilters, { timePeriodType: 'fy', timePeriodFY: new Set(['2014', '2015']) }); // mount the container - const timeVisualizationContainer = - mount(); + const container = + mount(); + container.instance().fetchAwards = jest.fn(); - // wait for the first SearchHelper call to finish - jest.runAllTicks(); + expect(container.instance().fetchAwards).toHaveBeenCalledTimes(0); - // the first API call should have been called - expect(fetchDataSpy.callCount).toEqual(1); - - // now update the props - timeVisualizationContainer.setProps({ - reduxFilters: secondFilters + container.setProps({ + reduxFilters: mockFilters }); - // wait for the second SearchHelper call to finish - jest.runAllTicks(); - // the first API call should have been called - expect(fetchDataSpy.callCount).toEqual(2); - - // reset the mocks and spies - unmockSearchHelper(); - fetchDataSpy.reset(); + expect(container.instance().fetchAwards).toHaveBeenCalledTimes(1); }); describe('parseData', () => { it('should properly restructure the API data for the spending over time chart for fiscal year series', () => { - // create a mock API response - const apiResponse = { - page_metadata: { - has_next_page: false, - has_previous_page: false, - next: null, - page: 1, - previous: null - }, - results: [{ - time_period: { - fiscal_year: "2016" - }, - aggregated_amount: "1234" - }, - { - time_period: { - fiscal_year: "2017" - }, - aggregated_amount: "5555" - }] - }; - - const mockReduxActions = { - setVizTxnSum: jest.fn() - }; - - // mock the search helper to resolve with the mocked response - mockSearchHelper('performSpendingOverTimeSearch', 'resolve', apiResponse); // mount the container - const timeVisualizationContainer = + const container = shallow(); - timeVisualizationContainer.instance().parseData(apiResponse, 'fiscal_year'); + container.instance().parseData(mockApi, 'fiscal_year'); - // wait for the SearchHelper promises to resolve - jest.runAllTicks(); // validate the state contains the correctly parsed values const expectedState = { loading: false, + error: false, visualizationPeriod: "fiscal_year", - groups: ['2016', '2017'], - xSeries: [['2016'], ['2017']], - ySeries: [[1234], [5555]] + groups: ['1979', '1980'], + xSeries: [['1979'], ['1980']], + ySeries: [[123], [234]], + rawLabels:[{period: null, year:'1979'},{period: null, year:'1980'}] }; - expect(timeVisualizationContainer.state()).toEqual(expectedState); + expect(container.state()).toEqual(expectedState); }); - it('should properly restructure the API data for the spending over time chart for quarterly series', () => { - // create a mock API response - const apiResponse = { - page_metadata: { - has_next_page: false, - has_previous_page: false, - next: null, - page: 1, - previous: null - }, - results: [{ - time_period: { - fiscal_year: "2017", - quarter: "1" - }, - aggregated_amount: "1234" - }, - { - time_period: { - fiscal_year: "2017", - quarter: "2" - }, - aggregated_amount: "5555" - }] - }; - - const mockReduxActions = { - setVizTxnSum: jest.fn() - }; - - // mock the search helper to resolve with the mocked response - mockSearchHelper('performSpendingOverTimeSearch', 'resolve', apiResponse); - // mount the container - const timeVisualizationContainer = + const container = shallow(); - timeVisualizationContainer.instance().updateVisualizationPeriod('quarter'); + container.setState({ + visualizationPeriod: 'quarter' + }); - timeVisualizationContainer.instance().parseData(apiResponse, 'quarter'); + container.instance().parseData(mockQuarters, 'quarter'); - // wait for the SearchHelper promises to resolve - jest.runAllTicks(); // validate the state contains the correctly parsed values - const expectedState = { + const expectedState = { loading: false, + error: false, visualizationPeriod: "quarter", - groups: ['Q1 2017', 'Q2 2017'], - xSeries: [['Q1 2017'], ['Q2 2017']], - ySeries: [[1234], [5555]] + groups: ['Q1 1979', 'Q2 1979'], + xSeries: [['Q1 1979'], ['Q2 1979']], + ySeries: [[1234], [5555]], + rawLabels:[{period: 'Q1', year:'1979'},{period: 'Q2', year:'1979'}] }; - expect(timeVisualizationContainer.state()).toEqual(expectedState); + expect(container.state()).toEqual(expectedState); }); it('should properly restructure the API data for the spending over time chart for monthly series', () => { - // create a mock API response - const apiResponse = { - page_metadata: { - has_next_page: false, - has_previous_page: false, - next: null, - page: 1, - previous: null - }, - results: [{ - time_period: { - fiscal_year: "2017", - month: "1" - }, - aggregated_amount: "1234" - }, - { - time_period: { - fiscal_year: "2017", - month: "2" - }, - aggregated_amount: "5555" - }] - }; - - const mockReduxActions = { - setVizTxnSum: jest.fn() - }; - - // mock the search helper to resolve with the mocked response - mockSearchHelper('performSpendingOverTimeSearch', 'resolve', apiResponse); - // mount the container - const timeVisualizationContainer = + const container = shallow(); - timeVisualizationContainer.instance().updateVisualizationPeriod('month'); + container.setState({ + visualizationPeriod: 'month' + }); + + container.instance().parseData(mockMonths, 'month'); + - timeVisualizationContainer.instance().parseData(apiResponse, 'month'); - // wait for the SearchHelper promises to resolve - jest.runAllTicks(); // validate the state contains the correctly parsed values - const expectedState = { + const expectedState = { loading: false, + error: false, visualizationPeriod: "month", - groups: ['Oct 2016', 'Nov 2016'], - xSeries: [['Oct 2016'], ['Nov 2016']], - ySeries: [[1234], [5555]] + groups: ['Oct 1978', 'Nov 1978'], + xSeries: [['Oct 1978'], ['Nov 1978']], + ySeries: [[1234], [5555]], + rawLabels:[{period: 'Oct', year:'1978'},{period: 'Nov', year:'1978'}] }; - expect(timeVisualizationContainer.state()).toEqual(expectedState); + expect(container.state()).toEqual(expectedState); }); }); }); diff --git a/tests/containers/search/visualizations/time/mockData.js b/tests/containers/search/visualizations/time/mockData.js new file mode 100644 index 0000000000..87499a136f --- /dev/null +++ b/tests/containers/search/visualizations/time/mockData.js @@ -0,0 +1,56 @@ +export const mockApi = { + results: [ + { + time_period: { + fiscal_year: '1979' + }, + aggregated_amount: 123, + group: 'fiscal_Year' + }, + { + time_period: { + fiscal_year: '1980' + }, + aggregated_amount: 234, + group: 'fiscal_Year' + } + ] +}; + +export const mockQuarters = { + results: [{ + time_period: { + fiscal_year: "1979", + quarter: "1" + }, + aggregated_amount: "1234" + }, + { + time_period: { + fiscal_year: "1979", + quarter: "2" + }, + aggregated_amount: "5555" + }] +}; + +export const mockMonths = { + results: [{ + time_period: { + fiscal_year: "1979", + month: "1" + }, + aggregated_amount: "1234" + }, + { + time_period: { + fiscal_year: "1979", + month: "2" + }, + aggregated_amount: "5555" + }] +}; + +export const mockActions = { + setAppliedFilterCompletion: jest.fn() +}; \ No newline at end of file diff --git a/tests/containers/search/visualizations/time/mockSearchHelper.js b/tests/containers/search/visualizations/time/mockSearchHelper.js new file mode 100644 index 0000000000..42cd44edfe --- /dev/null +++ b/tests/containers/search/visualizations/time/mockSearchHelper.js @@ -0,0 +1,14 @@ +import { mockApi } from './mockData'; + +export const performSpendingOverTimeSearch = () => ( + { + promise: new Promise((resolve) => { + process.nextTick(() => { + resolve({ + data: mockApi + }); + }) + }), + cancel: jest.fn() + } +); diff --git a/tests/redux/reducers/search/appliedFiltersReducer-test.js b/tests/redux/reducers/search/appliedFiltersReducer-test.js new file mode 100644 index 0000000000..1e333404e4 --- /dev/null +++ b/tests/redux/reducers/search/appliedFiltersReducer-test.js @@ -0,0 +1,79 @@ + +import { Set } from 'immutable'; + +import appliedFiltersReducer, { initialState } from 'redux/reducers/search/appliedFiltersReducer'; + +describe('appliedFiltersReducer', () => { + it('should return the initial state by default', () => { + expect( + appliedFiltersReducer(undefined, {}) + ).toEqual(initialState); + }); + + describe('APPLY_STAGED_FILTERS', () => { + it('should set the filter object to the provided object', () => { + const newFilters = Object.assign({}, initialState.filters, { + timePeriodFY: new Set(['1990']) + }); + + const action = { + type: 'APPLY_STAGED_FILTERS', + filters: newFilters + }; + + const newState = appliedFiltersReducer(undefined, action); + expect(newState.filters.timePeriodFY).toEqual(new Set(['1990'])); + }); + }); + + describe('CLEAR_APPLIED_FILTERS', () => { + it('should should return the initial state', () => { + const newFilters = Object.assign({}, initialState.filters, { + timePeriodFY: new Set(['1990']) + }); + + const modifiedState = { + filters: newFilters, + _empty: false, + _complete: false + }; + + expect(modifiedState).not.toEqual(initialState); + + const action = { + type: 'CLEAR_APPLIED_FILTERS' + }; + + const restoredState = appliedFiltersReducer(modifiedState, action); + expect(restoredState).toEqual(initialState); + }); + }); + + describe('SET_APPLIED_FILTER_EMPTINESS', () => { + it('should set the _empty value', () => { + let state = appliedFiltersReducer(undefined, {}); + expect(state._empty).toBeTruthy(); + + const action = { + type: 'SET_APPLIED_FILTER_EMPTINESS', + empty: false + }; + state = appliedFiltersReducer(state, action); + expect(state._empty).toBeFalsy(); + }); + }); + + describe('SET_APPLIED_FILTER_COMPLETION', () => { + it('should set the _complete value', () => { + let state = appliedFiltersReducer(undefined, {}); + expect(state._complete).toBeTruthy(); + + const action = { + type: 'SET_APPLIED_FILTER_COMPLETION', + complete: false + }; + state = appliedFiltersReducer(state, action); + expect(state._complete).toBeFalsy(); + }); + }); +}); \ No newline at end of file diff --git a/tests/redux/reducers/search/searchFiltersReducer-test.js b/tests/redux/reducers/search/searchFiltersReducer-test.js index 4a65d6bd35..f7c02a2bee 100644 --- a/tests/redux/reducers/search/searchFiltersReducer-test.js +++ b/tests/redux/reducers/search/searchFiltersReducer-test.js @@ -773,7 +773,7 @@ describe('searchFiltersReducer', () => { const expectedSecond = { timePeriodType: 'fy', - timePeriodFY: new Set(['1991']), + timePeriodFY: new Set(), timePeriodStart: null, timePeriodEnd: null }; @@ -817,7 +817,7 @@ describe('searchFiltersReducer', () => { const expectedSecond = { timePeriodType: 'fy', - timePeriodFY: new Set(['1991']), + timePeriodFY: new Set(), timePeriodStart: null, timePeriodEnd: null };