From c285e56b108ecc4921ba3e1ce9bd420e16ff5b46 Mon Sep 17 00:00:00 2001 From: Andrew Joseph Date: Wed, 21 May 2025 17:35:59 -0500 Subject: [PATCH 1/6] WCMS-25177: copy DataTableHeader and it's dependencies to repo --- src/api/axiosInstance.js | 4 + src/assets/icons/archive.svg | 1 + src/assets/icons/arrow-left.svg | 1 + src/assets/icons/arrow-right.svg | 1 + src/assets/icons/arrow-to-bottom.svg | 1 + src/assets/icons/arrow-to-left.svg | 1 + src/assets/icons/bars.svg | 1 + src/assets/icons/book.svg | 1 + src/assets/icons/bracket.svg | 13 + src/assets/icons/building.svg | 1 + src/assets/icons/caret-down.svg | 1 + src/assets/icons/caret-up.svg | 1 + src/assets/icons/check.svg | 1 + src/assets/icons/chevron-double-left.svg | 1 + src/assets/icons/chevron-double-right.svg | 1 + src/assets/icons/chevron-down-solid.svg | 1 + src/assets/icons/chevron-down.svg | 1 + src/assets/icons/chevron-left.svg | 1 + src/assets/icons/chevron-right-solid.svg | 1 + src/assets/icons/chevron-right.svg | 1 + src/assets/icons/chevron-up.svg | 1 + src/assets/icons/circle.svg | 1 + src/assets/icons/code.svg | 1 + src/assets/icons/columns.svg | 1 + src/assets/icons/copy.svg | 1 + src/assets/icons/density-1.svg | 1 + src/assets/icons/density-2.svg | 2 + src/assets/icons/density-3.svg | 1 + src/assets/icons/download.svg | 1 + src/assets/icons/envelope.svg | 1 + src/assets/icons/equals.svg | 1 + src/assets/icons/expand.svg | 1 + src/assets/icons/external-link-square.svg | 1 + src/assets/icons/facebook.svg | 1 + src/assets/icons/filter-solid.svg | 1 + src/assets/icons/filter.svg | 1 + src/assets/icons/folder-open.svg | 1 + src/assets/icons/greater-than-equal.svg | 1 + src/assets/icons/greater-than.svg | 1 + src/assets/icons/hospital.svg | 1 + src/assets/icons/index.js | 137 + src/assets/icons/info-circle.svg | 1 + src/assets/icons/less-than-equal.svg | 1 + src/assets/icons/less-than.svg | 1 + src/assets/icons/link.svg | 1 + src/assets/icons/linkedin.svg | 1 + src/assets/icons/long-arrow-left.svg | 1 + src/assets/icons/long-arrow-right.svg | 1 + src/assets/icons/minus-square.svg | 1 + src/assets/icons/not-equal.svg | 1 + src/assets/icons/plus-circle.svg | 1 + src/assets/icons/plus-square.svg | 1 + src/assets/icons/plus.svg | 1 + src/assets/icons/search.svg | 1 + src/assets/icons/sliders-h.svg | 1 + src/assets/icons/solid-thumbtack.svg | 1 + src/assets/icons/sort-down.svg | 1 + src/assets/icons/sort-up.svg | 1 + src/assets/icons/sort.svg | 1 + src/assets/icons/table.svg | 1 + src/assets/icons/table2.svg | 13 + src/assets/icons/tag.svg | 18 + src/assets/icons/times-circle.svg | 1 + src/assets/icons/times.svg | 1 + src/assets/icons/trash.svg | 1 + src/assets/icons/twitter.svg | 1 + src/assets/icons/undo.svg | 1 + .../common/DatasetDate/DatasetDate.jsx | 61 + .../common/DatasetDate/DatasetDate.scss | 46 + .../common/DatasetDate/DatasetDate.test.jsx | 15 + .../DatasetDateItem/DatasetDateItem.jsx | 55 + .../__snapshots__/DatasetDate.test.js.snap | 31 + .../__snapshots__/DatasetDate.test.jsx.snap | 187 + .../common/FileDownload/FileDownload.jsx | 117 + .../common/FileDownload/FileDownload.test.jsx | 111 + .../common/FileDownload/FilteredDownload.jsx | 86 + .../FileDownload/FilteredDownload.test.jsx | 52 + .../__snapshots__/FileDownload.test.js.snap | 156 + .../__snapshots__/FileDownload.test.jsx.snap | 156 + .../FilteredDownload.test.js.snap | 146 + .../FilteredDownload.test.jsx.snap | 146 + .../common/FontAwesomePro/FontAwesomePro.jsx | 81 + .../FontAwesomePro/FontAwesomePro.test.jsx | 19 + .../__snapshots__/FontAwesomePro.test.js.snap | 18 + .../FontAwesomePro.test.jsx.snap | 18 + src/components/common/Text/Text.jsx | 52 + .../common/ToggleBlock/ToggleBlock.jsx | 123 + .../common/ToggleBlock/ToggleBlock.test.jsx | 96 + .../__snapshots__/ToggleBlock.test.js.snap | 46 + .../__snapshots__/ToggleBlock.test.jsx.snap | 46 + src/components/common/Tooltip/Tooltip.jsx | 107 + src/components/common/Tooltip/Tooltip.scss | 108 + .../common/Tooltip/Tooltip.test.jsx | 67 + .../__snapshots__/Tooltip.test.js.snap | 23 + .../__snapshots__/Tooltip.test.jsx.snap | 23 + .../dataset/DataTable/DataTable.jsx | 435 ++ .../dataset/DataTable/DataTable.scss | 411 ++ .../dataset/DataTable/DataTable.test.jsx | 593 +++ .../__snapshots__/DataTable.test.js.snap | 1142 +++++ .../__snapshots__/DataTable.test.jsx.snap | 1150 +++++ .../DataTableHeader/DataHeaderButton.jsx | 47 + .../DataTableHeader/DataHeaderButton.test.jsx | 46 + .../dataset/DataTableHeader/DataIcon.jsx | 141 + .../dataset/DataTableHeader/DataIcon.test.jsx | 75 + .../DataTableHeader/DataTableDensity.jsx | 84 + .../DataTableHeader/DataTableDensity.test.jsx | 93 + .../DataTableHeader/DataTableHeader.jsx | 316 ++ .../DataTableHeader/DataTableHeader.scss | 348 ++ .../DataTableHeader/DataTableHeader.test.jsx | 587 +++ .../DataTableHeader/PopOverContent.jsx | 101 + .../DataTableHeader/PopOverContent.test.jsx | 65 + .../DataHeaderButton.test.js.snap | 29 + .../DataHeaderButton.test.jsx.snap | 29 + .../__snapshots__/DataIcon.test.js.snap | 15 + .../__snapshots__/DataIcon.test.jsx.snap | 15 + .../DataTableDensity.test.js.snap | 107 + .../DataTableDensity.test.jsx.snap | 107 + .../DataTableHeader.test.js.snap | 1590 +++++++ .../DataTableHeader.test.jsx.snap | 1768 +++++++ .../__snapshots__/PopOverContent.test.js.snap | 143 + .../PopOverContent.test.jsx.snap | 143 + .../DataTablePageResults.jsx | 52 + .../DataTablePageResults.test.jsx | 45 + .../DataTablePageResults.test.js.snap | 30 + .../DataTablePageResults.test.jsx.snap | 30 + .../DatasetContentLoading.jsx | 80 + .../DatasetContentLoading.scss | 66 + .../DatasetContentLoading.test.jsx | 28 + .../DatasetContentLoading.test.js.snap | 768 +++ .../DatasetContentLoading.test.jsx.snap | 768 +++ .../DatasetDownloadLink.jsx | 131 + .../DatasetDownloadLink.scss | 139 + .../DatasetDownloadLink.test.jsx | 162 + .../DatasetDownloadLink.test.js.snap | 113 + .../DatasetDownloadLink.test.jsx.snap | 113 + .../DatasetExplorerDownloadLink.jsx | 135 + .../DatasetExplorerDownloadLink.scss | 37 + .../DatasetHeaderLoading.jsx | 16 + .../DatasetHeaderLoading.scss | 41 + .../DatasetHeaderLoading.test.jsx | 12 + .../DatasetHeaderLoading.test.js.snap | 25 + .../DatasetHeaderLoading.test.jsx.snap | 25 + .../DatasetResource/DatasetResource.jsx | 60 + .../DatasetResource/DatasetResource.test.jsx | 233 + .../FilteredDatasetContext.jsx | 23 + .../FilteredDatasetResource.jsx | 184 + .../FilteredDatasetResource.test.jsx | 346 ++ .../DatasetResource.test.js.snap | 3841 +++++++++++++++ .../DatasetResource.test.jsx.snap | 4115 +++++++++++++++++ .../FilteredDatasetResource.test.js.snap | 3 + .../FilteredDatasetResource.test.jsx.snap | 3 + .../dataset/FilterDataset/AddFilter.jsx | 22 + .../dataset/FilterDataset/AddFilter.test.jsx | 25 + .../dataset/FilterDataset/DeleteFilter.jsx | 35 + .../FilterDataset/DeleteFilter.test.jsx | 35 + .../dataset/FilterDataset/FilterChip.jsx | 140 + .../dataset/FilterDataset/FilterChip.scss | 51 + .../dataset/FilterDataset/FilterChip.test.jsx | 90 + .../dataset/FilterDataset/FilterChipList.jsx | 95 + .../FilterDataset/FilterChipList.test.jsx | 118 + .../dataset/FilterDataset/FilterDataset.jsx | 183 + .../dataset/FilterDataset/FilterDataset.scss | 176 + .../FilterDataset/FilterDataset.test.jsx | 275 ++ .../dataset/FilterDataset/FilterItem.jsx | 117 + .../__snapshots__/AddFilter.test.js.snap | 31 + .../__snapshots__/AddFilter.test.jsx.snap | 31 + .../__snapshots__/DeleteFilter.test.js.snap | 29 + .../__snapshots__/DeleteFilter.test.jsx.snap | 29 + .../__snapshots__/FilterChip.test.js.snap | 43 + .../__snapshots__/FilterChip.test.jsx.snap | 43 + .../__snapshots__/FilterChipList.test.js.snap | 93 + .../FilterChipList.test.jsx.snap | 93 + .../__snapshots__/FilterDataset.test.js.snap | 258 ++ .../__snapshots__/FilterDataset.test.jsx.snap | 249 + .../FullScreenResource/FullScreenResource.jsx | 88 + .../FullScreenResource.scss | 203 + .../FullScreenResource.test.jsx | 184 + .../FullScreenResource.test.js.snap | 1209 +++++ .../FullScreenResource.test.jsx.snap | 1366 ++++++ .../dataset/ManageColumns/ManageColumns.jsx | 385 ++ .../dataset/ManageColumns/ManageColumns.scss | 300 ++ .../ManageColumns/ManageColumns.test.jsx | 637 +++ .../ManageColumns/ManageColumnsCard.jsx | 103 + .../ManageColumns/ManageColumnsCard.test.jsx | 48 + .../__snapshots__/ManageColumns.test.js.snap | 255 + .../__snapshots__/ManageColumns.test.jsx.snap | 276 ++ .../ManageColumnsCard.test.js.snap | 51 + .../ManageColumnsCard.test.jsx.snap | 58 + src/config/defaults.json | 48 + src/config/development.json | 7 + src/config/env.json | 5 + src/config/production.json | 6 + src/config/staging.json | 5 + src/config/testing.json | 6 + src/context/DatasetContext.jsx | 34 + src/hooks/useDataStore.js | 203 + src/log.js | 67 + src/stories/utilities/axios-mock.jsx | 6 + .../data-mocks/api-response-dataset.json | 83 + src/utilities/data-mocks/data-fileDownload.js | 40 + .../data-filteredDatasetResource.js | 33 + src/utilities/displayUtilities.js | 36 + src/utilities/downloadUtilities.js | 36 + src/utilities/getApiBaseUrl.js | 22 + 204 files changed, 31705 insertions(+) create mode 100644 src/api/axiosInstance.js create mode 100644 src/assets/icons/archive.svg create mode 100644 src/assets/icons/arrow-left.svg create mode 100644 src/assets/icons/arrow-right.svg create mode 100644 src/assets/icons/arrow-to-bottom.svg create mode 100644 src/assets/icons/arrow-to-left.svg create mode 100644 src/assets/icons/bars.svg create mode 100644 src/assets/icons/book.svg create mode 100644 src/assets/icons/bracket.svg create mode 100644 src/assets/icons/building.svg create mode 100644 src/assets/icons/caret-down.svg create mode 100644 src/assets/icons/caret-up.svg create mode 100644 src/assets/icons/check.svg create mode 100644 src/assets/icons/chevron-double-left.svg create mode 100644 src/assets/icons/chevron-double-right.svg create mode 100644 src/assets/icons/chevron-down-solid.svg create mode 100644 src/assets/icons/chevron-down.svg create mode 100644 src/assets/icons/chevron-left.svg create mode 100644 src/assets/icons/chevron-right-solid.svg create mode 100644 src/assets/icons/chevron-right.svg create mode 100644 src/assets/icons/chevron-up.svg create mode 100644 src/assets/icons/circle.svg create mode 100644 src/assets/icons/code.svg create mode 100644 src/assets/icons/columns.svg create mode 100644 src/assets/icons/copy.svg create mode 100644 src/assets/icons/density-1.svg create mode 100644 src/assets/icons/density-2.svg create mode 100644 src/assets/icons/density-3.svg create mode 100644 src/assets/icons/download.svg create mode 100644 src/assets/icons/envelope.svg create mode 100644 src/assets/icons/equals.svg create mode 100644 src/assets/icons/expand.svg create mode 100644 src/assets/icons/external-link-square.svg create mode 100644 src/assets/icons/facebook.svg create mode 100644 src/assets/icons/filter-solid.svg create mode 100644 src/assets/icons/filter.svg create mode 100644 src/assets/icons/folder-open.svg create mode 100644 src/assets/icons/greater-than-equal.svg create mode 100644 src/assets/icons/greater-than.svg create mode 100644 src/assets/icons/hospital.svg create mode 100644 src/assets/icons/index.js create mode 100644 src/assets/icons/info-circle.svg create mode 100644 src/assets/icons/less-than-equal.svg create mode 100644 src/assets/icons/less-than.svg create mode 100644 src/assets/icons/link.svg create mode 100644 src/assets/icons/linkedin.svg create mode 100644 src/assets/icons/long-arrow-left.svg create mode 100644 src/assets/icons/long-arrow-right.svg create mode 100644 src/assets/icons/minus-square.svg create mode 100644 src/assets/icons/not-equal.svg create mode 100644 src/assets/icons/plus-circle.svg create mode 100644 src/assets/icons/plus-square.svg create mode 100644 src/assets/icons/plus.svg create mode 100644 src/assets/icons/search.svg create mode 100644 src/assets/icons/sliders-h.svg create mode 100644 src/assets/icons/solid-thumbtack.svg create mode 100644 src/assets/icons/sort-down.svg create mode 100644 src/assets/icons/sort-up.svg create mode 100644 src/assets/icons/sort.svg create mode 100644 src/assets/icons/table.svg create mode 100644 src/assets/icons/table2.svg create mode 100644 src/assets/icons/tag.svg create mode 100644 src/assets/icons/times-circle.svg create mode 100644 src/assets/icons/times.svg create mode 100644 src/assets/icons/trash.svg create mode 100644 src/assets/icons/twitter.svg create mode 100644 src/assets/icons/undo.svg create mode 100644 src/components/common/DatasetDate/DatasetDate.jsx create mode 100644 src/components/common/DatasetDate/DatasetDate.scss create mode 100644 src/components/common/DatasetDate/DatasetDate.test.jsx create mode 100644 src/components/common/DatasetDate/DatasetDateItem/DatasetDateItem.jsx create mode 100644 src/components/common/DatasetDate/__snapshots__/DatasetDate.test.js.snap create mode 100644 src/components/common/DatasetDate/__snapshots__/DatasetDate.test.jsx.snap create mode 100644 src/components/common/FileDownload/FileDownload.jsx create mode 100644 src/components/common/FileDownload/FileDownload.test.jsx create mode 100644 src/components/common/FileDownload/FilteredDownload.jsx create mode 100644 src/components/common/FileDownload/FilteredDownload.test.jsx create mode 100644 src/components/common/FileDownload/__snapshots__/FileDownload.test.js.snap create mode 100644 src/components/common/FileDownload/__snapshots__/FileDownload.test.jsx.snap create mode 100644 src/components/common/FileDownload/__snapshots__/FilteredDownload.test.js.snap create mode 100644 src/components/common/FileDownload/__snapshots__/FilteredDownload.test.jsx.snap create mode 100644 src/components/common/FontAwesomePro/FontAwesomePro.jsx create mode 100644 src/components/common/FontAwesomePro/FontAwesomePro.test.jsx create mode 100644 src/components/common/FontAwesomePro/__snapshots__/FontAwesomePro.test.js.snap create mode 100644 src/components/common/FontAwesomePro/__snapshots__/FontAwesomePro.test.jsx.snap create mode 100644 src/components/common/Text/Text.jsx create mode 100644 src/components/common/ToggleBlock/ToggleBlock.jsx create mode 100644 src/components/common/ToggleBlock/ToggleBlock.test.jsx create mode 100644 src/components/common/ToggleBlock/__snapshots__/ToggleBlock.test.js.snap create mode 100644 src/components/common/ToggleBlock/__snapshots__/ToggleBlock.test.jsx.snap create mode 100644 src/components/common/Tooltip/Tooltip.jsx create mode 100644 src/components/common/Tooltip/Tooltip.scss create mode 100644 src/components/common/Tooltip/Tooltip.test.jsx create mode 100644 src/components/common/Tooltip/__snapshots__/Tooltip.test.js.snap create mode 100644 src/components/common/Tooltip/__snapshots__/Tooltip.test.jsx.snap create mode 100644 src/components/dataset/DataTable/DataTable.jsx create mode 100644 src/components/dataset/DataTable/DataTable.scss create mode 100644 src/components/dataset/DataTable/DataTable.test.jsx create mode 100644 src/components/dataset/DataTable/__snapshots__/DataTable.test.js.snap create mode 100644 src/components/dataset/DataTable/__snapshots__/DataTable.test.jsx.snap create mode 100644 src/components/dataset/DataTableHeader/DataHeaderButton.jsx create mode 100644 src/components/dataset/DataTableHeader/DataHeaderButton.test.jsx create mode 100644 src/components/dataset/DataTableHeader/DataIcon.jsx create mode 100644 src/components/dataset/DataTableHeader/DataIcon.test.jsx create mode 100644 src/components/dataset/DataTableHeader/DataTableDensity.jsx create mode 100644 src/components/dataset/DataTableHeader/DataTableDensity.test.jsx create mode 100644 src/components/dataset/DataTableHeader/DataTableHeader.jsx create mode 100644 src/components/dataset/DataTableHeader/DataTableHeader.scss create mode 100644 src/components/dataset/DataTableHeader/DataTableHeader.test.jsx create mode 100644 src/components/dataset/DataTableHeader/PopOverContent.jsx create mode 100644 src/components/dataset/DataTableHeader/PopOverContent.test.jsx create mode 100644 src/components/dataset/DataTableHeader/__snapshots__/DataHeaderButton.test.js.snap create mode 100644 src/components/dataset/DataTableHeader/__snapshots__/DataHeaderButton.test.jsx.snap create mode 100644 src/components/dataset/DataTableHeader/__snapshots__/DataIcon.test.js.snap create mode 100644 src/components/dataset/DataTableHeader/__snapshots__/DataIcon.test.jsx.snap create mode 100644 src/components/dataset/DataTableHeader/__snapshots__/DataTableDensity.test.js.snap create mode 100644 src/components/dataset/DataTableHeader/__snapshots__/DataTableDensity.test.jsx.snap create mode 100644 src/components/dataset/DataTableHeader/__snapshots__/DataTableHeader.test.js.snap create mode 100644 src/components/dataset/DataTableHeader/__snapshots__/DataTableHeader.test.jsx.snap create mode 100644 src/components/dataset/DataTableHeader/__snapshots__/PopOverContent.test.js.snap create mode 100644 src/components/dataset/DataTableHeader/__snapshots__/PopOverContent.test.jsx.snap create mode 100644 src/components/dataset/DataTablePageResults/DataTablePageResults.jsx create mode 100644 src/components/dataset/DataTablePageResults/DataTablePageResults.test.jsx create mode 100644 src/components/dataset/DataTablePageResults/__snapshots__/DataTablePageResults.test.js.snap create mode 100644 src/components/dataset/DataTablePageResults/__snapshots__/DataTablePageResults.test.jsx.snap create mode 100644 src/components/dataset/DatasetContentLoading/DatasetContentLoading.jsx create mode 100644 src/components/dataset/DatasetContentLoading/DatasetContentLoading.scss create mode 100644 src/components/dataset/DatasetContentLoading/DatasetContentLoading.test.jsx create mode 100644 src/components/dataset/DatasetContentLoading/__snapshots__/DatasetContentLoading.test.js.snap create mode 100644 src/components/dataset/DatasetContentLoading/__snapshots__/DatasetContentLoading.test.jsx.snap create mode 100644 src/components/dataset/DatasetDownloadLink/DatasetDownloadLink.jsx create mode 100644 src/components/dataset/DatasetDownloadLink/DatasetDownloadLink.scss create mode 100644 src/components/dataset/DatasetDownloadLink/DatasetDownloadLink.test.jsx create mode 100644 src/components/dataset/DatasetDownloadLink/__snapshots__/DatasetDownloadLink.test.js.snap create mode 100644 src/components/dataset/DatasetDownloadLink/__snapshots__/DatasetDownloadLink.test.jsx.snap create mode 100644 src/components/dataset/DatasetExplorerDownloadLink/DatasetExplorerDownloadLink.jsx create mode 100644 src/components/dataset/DatasetExplorerDownloadLink/DatasetExplorerDownloadLink.scss create mode 100644 src/components/dataset/DatasetHeaderLoading/DatasetHeaderLoading.jsx create mode 100644 src/components/dataset/DatasetHeaderLoading/DatasetHeaderLoading.scss create mode 100644 src/components/dataset/DatasetHeaderLoading/DatasetHeaderLoading.test.jsx create mode 100644 src/components/dataset/DatasetHeaderLoading/__snapshots__/DatasetHeaderLoading.test.js.snap create mode 100644 src/components/dataset/DatasetHeaderLoading/__snapshots__/DatasetHeaderLoading.test.jsx.snap create mode 100644 src/components/dataset/DatasetResource/DatasetResource.jsx create mode 100644 src/components/dataset/DatasetResource/DatasetResource.test.jsx create mode 100644 src/components/dataset/DatasetResource/FilteredDatasetContext.jsx create mode 100644 src/components/dataset/DatasetResource/FilteredDatasetResource.jsx create mode 100644 src/components/dataset/DatasetResource/FilteredDatasetResource.test.jsx create mode 100644 src/components/dataset/DatasetResource/__snapshots__/DatasetResource.test.js.snap create mode 100644 src/components/dataset/DatasetResource/__snapshots__/DatasetResource.test.jsx.snap create mode 100644 src/components/dataset/DatasetResource/__snapshots__/FilteredDatasetResource.test.js.snap create mode 100644 src/components/dataset/DatasetResource/__snapshots__/FilteredDatasetResource.test.jsx.snap create mode 100644 src/components/dataset/FilterDataset/AddFilter.jsx create mode 100644 src/components/dataset/FilterDataset/AddFilter.test.jsx create mode 100644 src/components/dataset/FilterDataset/DeleteFilter.jsx create mode 100644 src/components/dataset/FilterDataset/DeleteFilter.test.jsx create mode 100644 src/components/dataset/FilterDataset/FilterChip.jsx create mode 100644 src/components/dataset/FilterDataset/FilterChip.scss create mode 100644 src/components/dataset/FilterDataset/FilterChip.test.jsx create mode 100644 src/components/dataset/FilterDataset/FilterChipList.jsx create mode 100644 src/components/dataset/FilterDataset/FilterChipList.test.jsx create mode 100644 src/components/dataset/FilterDataset/FilterDataset.jsx create mode 100644 src/components/dataset/FilterDataset/FilterDataset.scss create mode 100644 src/components/dataset/FilterDataset/FilterDataset.test.jsx create mode 100644 src/components/dataset/FilterDataset/FilterItem.jsx create mode 100644 src/components/dataset/FilterDataset/__snapshots__/AddFilter.test.js.snap create mode 100644 src/components/dataset/FilterDataset/__snapshots__/AddFilter.test.jsx.snap create mode 100644 src/components/dataset/FilterDataset/__snapshots__/DeleteFilter.test.js.snap create mode 100644 src/components/dataset/FilterDataset/__snapshots__/DeleteFilter.test.jsx.snap create mode 100644 src/components/dataset/FilterDataset/__snapshots__/FilterChip.test.js.snap create mode 100644 src/components/dataset/FilterDataset/__snapshots__/FilterChip.test.jsx.snap create mode 100644 src/components/dataset/FilterDataset/__snapshots__/FilterChipList.test.js.snap create mode 100644 src/components/dataset/FilterDataset/__snapshots__/FilterChipList.test.jsx.snap create mode 100644 src/components/dataset/FilterDataset/__snapshots__/FilterDataset.test.js.snap create mode 100644 src/components/dataset/FilterDataset/__snapshots__/FilterDataset.test.jsx.snap create mode 100644 src/components/dataset/FullScreenResource/FullScreenResource.jsx create mode 100644 src/components/dataset/FullScreenResource/FullScreenResource.scss create mode 100644 src/components/dataset/FullScreenResource/FullScreenResource.test.jsx create mode 100644 src/components/dataset/FullScreenResource/__snapshots__/FullScreenResource.test.js.snap create mode 100644 src/components/dataset/FullScreenResource/__snapshots__/FullScreenResource.test.jsx.snap create mode 100644 src/components/dataset/ManageColumns/ManageColumns.jsx create mode 100644 src/components/dataset/ManageColumns/ManageColumns.scss create mode 100644 src/components/dataset/ManageColumns/ManageColumns.test.jsx create mode 100644 src/components/dataset/ManageColumns/ManageColumnsCard.jsx create mode 100644 src/components/dataset/ManageColumns/ManageColumnsCard.test.jsx create mode 100644 src/components/dataset/ManageColumns/__snapshots__/ManageColumns.test.js.snap create mode 100644 src/components/dataset/ManageColumns/__snapshots__/ManageColumns.test.jsx.snap create mode 100644 src/components/dataset/ManageColumns/__snapshots__/ManageColumnsCard.test.js.snap create mode 100644 src/components/dataset/ManageColumns/__snapshots__/ManageColumnsCard.test.jsx.snap create mode 100644 src/config/defaults.json create mode 100644 src/config/development.json create mode 100644 src/config/env.json create mode 100644 src/config/production.json create mode 100644 src/config/staging.json create mode 100644 src/config/testing.json create mode 100644 src/context/DatasetContext.jsx create mode 100644 src/hooks/useDataStore.js create mode 100644 src/log.js create mode 100644 src/stories/utilities/axios-mock.jsx create mode 100644 src/utilities/data-mocks/api-response-dataset.json create mode 100644 src/utilities/data-mocks/data-fileDownload.js create mode 100644 src/utilities/data-mocks/data-filteredDatasetResource.js create mode 100644 src/utilities/displayUtilities.js create mode 100644 src/utilities/downloadUtilities.js create mode 100644 src/utilities/getApiBaseUrl.js diff --git a/src/api/axiosInstance.js b/src/api/axiosInstance.js new file mode 100644 index 00000000..b8008320 --- /dev/null +++ b/src/api/axiosInstance.js @@ -0,0 +1,4 @@ +import axios from 'axios' + +// Creates a 'shared' instance so that it can be easily used and mocked where needed +export const pdcAxiosInstance = axios.create() diff --git a/src/assets/icons/archive.svg b/src/assets/icons/archive.svg new file mode 100644 index 00000000..d9094236 --- /dev/null +++ b/src/assets/icons/archive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/arrow-left.svg b/src/assets/icons/arrow-left.svg new file mode 100644 index 00000000..e0b40223 --- /dev/null +++ b/src/assets/icons/arrow-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/arrow-right.svg b/src/assets/icons/arrow-right.svg new file mode 100644 index 00000000..0b89191d --- /dev/null +++ b/src/assets/icons/arrow-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/arrow-to-bottom.svg b/src/assets/icons/arrow-to-bottom.svg new file mode 100644 index 00000000..d8cb8d18 --- /dev/null +++ b/src/assets/icons/arrow-to-bottom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/arrow-to-left.svg b/src/assets/icons/arrow-to-left.svg new file mode 100644 index 00000000..f15c7652 --- /dev/null +++ b/src/assets/icons/arrow-to-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/bars.svg b/src/assets/icons/bars.svg new file mode 100644 index 00000000..3f28aeda --- /dev/null +++ b/src/assets/icons/bars.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/book.svg b/src/assets/icons/book.svg new file mode 100644 index 00000000..6d37cd85 --- /dev/null +++ b/src/assets/icons/book.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/bracket.svg b/src/assets/icons/bracket.svg new file mode 100644 index 00000000..e5eb8b2e --- /dev/null +++ b/src/assets/icons/bracket.svg @@ -0,0 +1,13 @@ + + + icon_bracket + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/building.svg b/src/assets/icons/building.svg new file mode 100644 index 00000000..4178db46 --- /dev/null +++ b/src/assets/icons/building.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/caret-down.svg b/src/assets/icons/caret-down.svg new file mode 100644 index 00000000..b3ee2ea9 --- /dev/null +++ b/src/assets/icons/caret-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/caret-up.svg b/src/assets/icons/caret-up.svg new file mode 100644 index 00000000..b4c7e548 --- /dev/null +++ b/src/assets/icons/caret-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/check.svg b/src/assets/icons/check.svg new file mode 100644 index 00000000..781daa31 --- /dev/null +++ b/src/assets/icons/check.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/chevron-double-left.svg b/src/assets/icons/chevron-double-left.svg new file mode 100644 index 00000000..70cc0cd6 --- /dev/null +++ b/src/assets/icons/chevron-double-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/chevron-double-right.svg b/src/assets/icons/chevron-double-right.svg new file mode 100644 index 00000000..23169a41 --- /dev/null +++ b/src/assets/icons/chevron-double-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/chevron-down-solid.svg b/src/assets/icons/chevron-down-solid.svg new file mode 100644 index 00000000..5962e893 --- /dev/null +++ b/src/assets/icons/chevron-down-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/chevron-down.svg b/src/assets/icons/chevron-down.svg new file mode 100644 index 00000000..cb9d8fe1 --- /dev/null +++ b/src/assets/icons/chevron-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/chevron-left.svg b/src/assets/icons/chevron-left.svg new file mode 100644 index 00000000..6ce32bf4 --- /dev/null +++ b/src/assets/icons/chevron-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/chevron-right-solid.svg b/src/assets/icons/chevron-right-solid.svg new file mode 100644 index 00000000..6d3e119b --- /dev/null +++ b/src/assets/icons/chevron-right-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/chevron-right.svg b/src/assets/icons/chevron-right.svg new file mode 100644 index 00000000..1ca5f946 --- /dev/null +++ b/src/assets/icons/chevron-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/chevron-up.svg b/src/assets/icons/chevron-up.svg new file mode 100644 index 00000000..643de0cb --- /dev/null +++ b/src/assets/icons/chevron-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/circle.svg b/src/assets/icons/circle.svg new file mode 100644 index 00000000..c2db0b25 --- /dev/null +++ b/src/assets/icons/circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/code.svg b/src/assets/icons/code.svg new file mode 100644 index 00000000..27072b3e --- /dev/null +++ b/src/assets/icons/code.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/columns.svg b/src/assets/icons/columns.svg new file mode 100644 index 00000000..55daa8c4 --- /dev/null +++ b/src/assets/icons/columns.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/copy.svg b/src/assets/icons/copy.svg new file mode 100644 index 00000000..a488ff2c --- /dev/null +++ b/src/assets/icons/copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/density-1.svg b/src/assets/icons/density-1.svg new file mode 100644 index 00000000..3ae0109a --- /dev/null +++ b/src/assets/icons/density-1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/density-2.svg b/src/assets/icons/density-2.svg new file mode 100644 index 00000000..1a7a304a --- /dev/null +++ b/src/assets/icons/density-2.svg @@ -0,0 +1,2 @@ + + diff --git a/src/assets/icons/density-3.svg b/src/assets/icons/density-3.svg new file mode 100644 index 00000000..a7270716 --- /dev/null +++ b/src/assets/icons/density-3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/download.svg b/src/assets/icons/download.svg new file mode 100644 index 00000000..288c1662 --- /dev/null +++ b/src/assets/icons/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/envelope.svg b/src/assets/icons/envelope.svg new file mode 100644 index 00000000..a2557ef2 --- /dev/null +++ b/src/assets/icons/envelope.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/equals.svg b/src/assets/icons/equals.svg new file mode 100644 index 00000000..3670ebb5 --- /dev/null +++ b/src/assets/icons/equals.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/expand.svg b/src/assets/icons/expand.svg new file mode 100644 index 00000000..6b162e19 --- /dev/null +++ b/src/assets/icons/expand.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/external-link-square.svg b/src/assets/icons/external-link-square.svg new file mode 100644 index 00000000..dffd8172 --- /dev/null +++ b/src/assets/icons/external-link-square.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/facebook.svg b/src/assets/icons/facebook.svg new file mode 100644 index 00000000..c7448e01 --- /dev/null +++ b/src/assets/icons/facebook.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/filter-solid.svg b/src/assets/icons/filter-solid.svg new file mode 100644 index 00000000..c3ccb116 --- /dev/null +++ b/src/assets/icons/filter-solid.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/filter.svg b/src/assets/icons/filter.svg new file mode 100644 index 00000000..78269dea --- /dev/null +++ b/src/assets/icons/filter.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/folder-open.svg b/src/assets/icons/folder-open.svg new file mode 100644 index 00000000..37372797 --- /dev/null +++ b/src/assets/icons/folder-open.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/greater-than-equal.svg b/src/assets/icons/greater-than-equal.svg new file mode 100644 index 00000000..170985d5 --- /dev/null +++ b/src/assets/icons/greater-than-equal.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/greater-than.svg b/src/assets/icons/greater-than.svg new file mode 100644 index 00000000..b62e80ac --- /dev/null +++ b/src/assets/icons/greater-than.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/hospital.svg b/src/assets/icons/hospital.svg new file mode 100644 index 00000000..004fb944 --- /dev/null +++ b/src/assets/icons/hospital.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/index.js b/src/assets/icons/index.js new file mode 100644 index 00000000..54316d14 --- /dev/null +++ b/src/assets/icons/index.js @@ -0,0 +1,137 @@ +import { ReactComponent as Archive } from './archive.svg' +import { ReactComponent as ArrowLeft } from './arrow-left.svg' +import { ReactComponent as ArrowRight } from './arrow-right.svg' +import { ReactComponent as ArrowToBottom } from './arrow-to-bottom.svg' +import { ReactComponent as ArrowToLeft } from './arrow-to-left.svg' +import { ReactComponent as Bars } from './bars.svg' +import { ReactComponent as Book } from './book.svg' +import { ReactComponent as Bracket } from './bracket.svg' +import { ReactComponent as Building } from './building.svg' +import { ReactComponent as CaretDown } from './caret-down.svg' +import { ReactComponent as CaretUp } from './caret-up.svg' +import { ReactComponent as ChevronDoubleLeft } from './chevron-double-left.svg' +import { ReactComponent as ChevronDoubleRight } from './chevron-double-right.svg' +import { ReactComponent as ChevronDown } from './chevron-down.svg' +import { ReactComponent as ChevronDownSolid } from './chevron-down-solid.svg' +import { ReactComponent as ChevronLeft } from './chevron-left.svg' +import { ReactComponent as ChevronRight } from './chevron-right.svg' +import { ReactComponent as ChevronRightSolid } from './chevron-right-solid.svg' +import { ReactComponent as ChevronUp } from './chevron-up.svg' +import { ReactComponent as Circle } from './circle.svg' +import { ReactComponent as Code } from './code.svg' +import { ReactComponent as Columns } from './columns.svg' +import { ReactComponent as Copy } from './copy.svg' +import { ReactComponent as Density1 } from './density-1.svg' +import { ReactComponent as Density2 } from './density-2.svg' +import { ReactComponent as Density3 } from './density-3.svg' +import { ReactComponent as Download } from './download.svg' +import { ReactComponent as Envelope } from './envelope.svg' +import { ReactComponent as Equals } from './equals.svg' +import { ReactComponent as Expand } from './expand.svg' +import { ReactComponent as ExternalLinkSquare } from './external-link-square.svg' +import { ReactComponent as Facebook } from './facebook.svg' +import { ReactComponent as Filter } from './filter.svg' +import { ReactComponent as FilterSolid } from './filter-solid.svg' +import { ReactComponent as FolderOpen } from './folder-open.svg' +import { ReactComponent as GreaterThan } from './greater-than.svg' +import { ReactComponent as GreaterThanEqual } from './greater-than-equal.svg' +import { ReactComponent as InfoCircle } from './info-circle.svg' +import { ReactComponent as LessThan } from './less-than.svg' +import { ReactComponent as LessThanEqual } from './less-than-equal.svg' +import { ReactComponent as Link } from './link.svg' +import { ReactComponent as Linkedin } from './linkedin.svg' +import { ReactComponent as LongArrowLeft } from './long-arrow-left.svg' +import { ReactComponent as LongArrowRight } from './long-arrow-right.svg' +import { ReactComponent as MinusSquare } from './minus-square.svg' +import { ReactComponent as NotEqual } from './not-equal.svg' +import { ReactComponent as Plus } from './plus.svg' +import { ReactComponent as PlusCircle } from './plus-circle.svg' +import { ReactComponent as PlusSquare } from './plus-square.svg' +import { ReactComponent as Search } from './search.svg' +import { ReactComponent as SlidersH } from './sliders-h.svg' +import { ReactComponent as SolidThumbtack } from './solid-thumbtack.svg' +import { ReactComponent as Sort } from './sort.svg' +import { ReactComponent as SortDown } from './sort-down.svg' +import { ReactComponent as SortUp } from './sort-up.svg' +import { ReactComponent as Table } from './table.svg' +import { ReactComponent as Table2 } from './table2.svg' +import { ReactComponent as Tag } from './tag.svg' +import { ReactComponent as Times } from './times.svg' +import { ReactComponent as TimesCircle } from './times-circle.svg' +import { ReactComponent as Trash } from './trash.svg' +import { ReactComponent as Twitter } from './twitter.svg' +import { ReactComponent as Undo } from './undo.svg' +import { ReactComponent as Hospital } from './hospital.svg' + +/* + To add an icon: + 1. Add SVG file: '~/assets/icons/' + 2. Hook it up by following 'import' and 'export' syntax +*/ + +export default [ + { archive: Archive }, + { 'arrow-left': ArrowLeft }, + { 'arrow-right': ArrowRight }, + { 'arrow-to-bottom': ArrowToBottom }, + { 'arrow-to-left': ArrowToLeft }, + { bars: Bars }, + { book: Book }, + { bracket: Bracket }, + { building: Building }, + { 'caret-down': CaretDown }, + { 'caret-up': CaretUp }, + { 'chevron-double-left': ChevronDoubleLeft }, + { 'chevron-double-right': ChevronDoubleRight }, + { 'chevron-down-solid': ChevronDownSolid }, + { 'chevron-down': ChevronDown }, + { 'chevron-left': ChevronLeft }, + { 'chevron-right-solid': ChevronRightSolid }, + { 'chevron-right': ChevronRight }, + { 'chevron-up': ChevronUp }, + { circle: Circle }, + { code: Code }, + { columns: Columns }, + { copy: Copy }, + { 'density-1': Density1 }, + { 'density-2': Density2 }, + { 'density-3': Density3 }, + { download: Download }, + { envelope: Envelope }, + { equals: Equals }, + { expand: Expand }, + { 'external-link-square': ExternalLinkSquare }, + { facebook: Facebook }, + { filter: Filter }, + { 'filter-solid': FilterSolid }, + { 'folder-open': FolderOpen }, + { hospital: Hospital }, + { 'info-circle': InfoCircle }, + { linkedin: Linkedin }, + { link: Link }, + { 'greater-than': GreaterThan }, + { 'greater-than-equal': GreaterThanEqual }, + { 'less-than': LessThan }, + { 'less-than-equal': LessThanEqual }, + { 'long-arrow-left': LongArrowLeft }, + { 'long-arrow-right': LongArrowRight }, + { 'minus-square': MinusSquare }, + { 'not-equal': NotEqual }, + { plus: Plus }, + { 'plus-circle': PlusCircle }, + { 'plus-square': PlusSquare }, + { search: Search }, + { 'sliders-h': SlidersH }, + { 'solid-thumbtack': SolidThumbtack }, + { 'sort-down': SortDown }, + { 'sort-up': SortUp }, + { sort: Sort }, + { table: Table }, + { table2: Table2 }, + { tag: Tag }, + { times: Times }, + { 'times-circle': TimesCircle }, + { trash: Trash }, + { twitter: Twitter }, + { undo: Undo } +] diff --git a/src/assets/icons/info-circle.svg b/src/assets/icons/info-circle.svg new file mode 100644 index 00000000..3f0f6426 --- /dev/null +++ b/src/assets/icons/info-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/less-than-equal.svg b/src/assets/icons/less-than-equal.svg new file mode 100644 index 00000000..3190c7ef --- /dev/null +++ b/src/assets/icons/less-than-equal.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/less-than.svg b/src/assets/icons/less-than.svg new file mode 100644 index 00000000..da3dcbd3 --- /dev/null +++ b/src/assets/icons/less-than.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/link.svg b/src/assets/icons/link.svg new file mode 100644 index 00000000..c53d1ada --- /dev/null +++ b/src/assets/icons/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/linkedin.svg b/src/assets/icons/linkedin.svg new file mode 100644 index 00000000..0d5c4fbb --- /dev/null +++ b/src/assets/icons/linkedin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/long-arrow-left.svg b/src/assets/icons/long-arrow-left.svg new file mode 100644 index 00000000..a6c1695f --- /dev/null +++ b/src/assets/icons/long-arrow-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/long-arrow-right.svg b/src/assets/icons/long-arrow-right.svg new file mode 100644 index 00000000..5fcbe4e8 --- /dev/null +++ b/src/assets/icons/long-arrow-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/minus-square.svg b/src/assets/icons/minus-square.svg new file mode 100644 index 00000000..6b72714e --- /dev/null +++ b/src/assets/icons/minus-square.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/not-equal.svg b/src/assets/icons/not-equal.svg new file mode 100644 index 00000000..c17e05bd --- /dev/null +++ b/src/assets/icons/not-equal.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/plus-circle.svg b/src/assets/icons/plus-circle.svg new file mode 100644 index 00000000..d42ac32f --- /dev/null +++ b/src/assets/icons/plus-circle.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/plus-square.svg b/src/assets/icons/plus-square.svg new file mode 100644 index 00000000..dfc22ac6 --- /dev/null +++ b/src/assets/icons/plus-square.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/plus.svg b/src/assets/icons/plus.svg new file mode 100644 index 00000000..ef16c985 --- /dev/null +++ b/src/assets/icons/plus.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/search.svg b/src/assets/icons/search.svg new file mode 100644 index 00000000..acd3282b --- /dev/null +++ b/src/assets/icons/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/sliders-h.svg b/src/assets/icons/sliders-h.svg new file mode 100644 index 00000000..e2aaf7fb --- /dev/null +++ b/src/assets/icons/sliders-h.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/solid-thumbtack.svg b/src/assets/icons/solid-thumbtack.svg new file mode 100644 index 00000000..7b98d281 --- /dev/null +++ b/src/assets/icons/solid-thumbtack.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/sort-down.svg b/src/assets/icons/sort-down.svg new file mode 100644 index 00000000..2644ba2b --- /dev/null +++ b/src/assets/icons/sort-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/sort-up.svg b/src/assets/icons/sort-up.svg new file mode 100644 index 00000000..c6e1001d --- /dev/null +++ b/src/assets/icons/sort-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/sort.svg b/src/assets/icons/sort.svg new file mode 100644 index 00000000..89c08354 --- /dev/null +++ b/src/assets/icons/sort.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/table.svg b/src/assets/icons/table.svg new file mode 100644 index 00000000..f3ba83da --- /dev/null +++ b/src/assets/icons/table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/table2.svg b/src/assets/icons/table2.svg new file mode 100644 index 00000000..f50d33ad --- /dev/null +++ b/src/assets/icons/table2.svg @@ -0,0 +1,13 @@ + + + icon_table + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/tag.svg b/src/assets/icons/tag.svg new file mode 100644 index 00000000..3f137f1b --- /dev/null +++ b/src/assets/icons/tag.svg @@ -0,0 +1,18 @@ + + + icon_tag + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/times-circle.svg b/src/assets/icons/times-circle.svg new file mode 100644 index 00000000..15181d34 --- /dev/null +++ b/src/assets/icons/times-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/times.svg b/src/assets/icons/times.svg new file mode 100644 index 00000000..73bee8c3 --- /dev/null +++ b/src/assets/icons/times.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/trash.svg b/src/assets/icons/trash.svg new file mode 100644 index 00000000..92e895b8 --- /dev/null +++ b/src/assets/icons/trash.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/twitter.svg b/src/assets/icons/twitter.svg new file mode 100644 index 00000000..114b5ca2 --- /dev/null +++ b/src/assets/icons/twitter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/undo.svg b/src/assets/icons/undo.svg new file mode 100644 index 00000000..9731a363 --- /dev/null +++ b/src/assets/icons/undo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/common/DatasetDate/DatasetDate.jsx b/src/components/common/DatasetDate/DatasetDate.jsx new file mode 100644 index 00000000..e4218151 --- /dev/null +++ b/src/components/common/DatasetDate/DatasetDate.jsx @@ -0,0 +1,61 @@ +import React from 'react' +import PropTypes from 'prop-types' +import DatasetDateItem from './DatasetDateItem/DatasetDateItem' + +import './DatasetDate.scss' + +const DatasetDate = (props) => { + const { + date, + updatedBoldLabel = false, + releasedBoldLabel = false, + refreshBoldLabel = false, + displayTooltips = true + } = props + + const { modified, released, refresh } = date + + let count = 0 + released && count++ + modified && count++ + refresh && count++ + + return ( +
+ {modified && } + {count > 1 && } + {released && } + {count > 2 && } + {refresh && } +
+ ) +} + +DatasetDate.propTypes = { + /** + * Released and modified date strings + */ + date: PropTypes.shape({ + modified: PropTypes.string, + released: PropTypes.string, + refresh: PropTypes.string + }), + /** + * Apply bold style to updated label + */ + updatedBoldLabel: PropTypes.bool, + /** + * Apply bold style to released label + */ + releasedBoldLabel: PropTypes.bool, + /** + * Apply bold style to refresh label + */ + refreshBoldLabel: PropTypes.bool, + /** + * Display tooltips or not + */ + displayTooltips: PropTypes.bool +} +DatasetDate.displayName = 'DatasetDate' +export default DatasetDate diff --git a/src/components/common/DatasetDate/DatasetDate.scss b/src/components/common/DatasetDate/DatasetDate.scss new file mode 100644 index 00000000..ba699649 --- /dev/null +++ b/src/components/common/DatasetDate/DatasetDate.scss @@ -0,0 +1,46 @@ +@use "../../../scss/modules/modules"; +@use "../../../scss/modules/colormap"; + +.dataset-date { + display: flex; + + .dataset-date-item { + color: colormap.$charcoal-90; + font-size: 1.4rem; + white-space: nowrap; + + &.bold-label > span { + font-weight: 700; + } + .ds-c-tooltip__container { + position: relative; + bottom: 10px; + width: 20px; + display: inline-block; + } + .ds-c-tooltip__trigger-icon { + padding: 0 + } + .ds-c-tooltip { + max-width: revert !important; + font-size: var(--theme-font-size-md, 16px); + font-style: normal; + font-weight: 700; + line-height: 150%; /* 24px */ + } + } +} + +@media screen and (max-width: 600px) { + .dataset-date { + flex-direction: column; + + .dataset-date-item { + margin-bottom: 0.5rem; + } + + .bullet-point { + display: none; + } + } +} diff --git a/src/components/common/DatasetDate/DatasetDate.test.jsx b/src/components/common/DatasetDate/DatasetDate.test.jsx new file mode 100644 index 00000000..1269906b --- /dev/null +++ b/src/components/common/DatasetDate/DatasetDate.test.jsx @@ -0,0 +1,15 @@ +import renderer from 'react-test-renderer' +import DatasetDate from './DatasetDate' + +describe('DatasetDate component.', () => { + it('Matches snapshot.', () => { + const renderedDatasetDate = renderer.create( + + ).toJSON() + + expect(renderedDatasetDate).toMatchSnapshot() + }) +}) diff --git a/src/components/common/DatasetDate/DatasetDateItem/DatasetDateItem.jsx b/src/components/common/DatasetDate/DatasetDateItem/DatasetDateItem.jsx new file mode 100644 index 00000000..d7dc2b34 --- /dev/null +++ b/src/components/common/DatasetDate/DatasetDateItem/DatasetDateItem.jsx @@ -0,0 +1,55 @@ +import React from 'react' +import PropTypes from 'prop-types' +import moment from 'moment' +import { Tooltip, TooltipIcon } from '@cmsgov/design-system' + +const DatasetDateItem = (props) => { + const { type, date, boldLabel = false, displayTooltips = true } = props + + const dateText = { + modified: 'Last Modified', + released: 'Released', + refresh: 'Planned Update' + } + const tooltipValue = { + modified: 'Last Modified: The date the
dataset was last updated.', + released: 'Released: The date the most
recent dataset was made available
to the public.', + refresh: 'Planned Update: The date the
dataset is scheduled to be updated.' + } + return ( +
+ {dateText[type]}: {moment(date).format('MMMM D, YYYY')} + {displayTooltips === true && + } + placement="top" + > + + + } +
+ ) +} + +DatasetDateItem.propTypes = { + /** + * Date label + */ + type: PropTypes.oneOf(['modified', 'released', 'refresh']), + /** + * Date string + */ + date: PropTypes.string, + /** + * Apply bold style to label + */ + boldLabel: PropTypes.bool, + /** + * Display tooltips or not + */ + displayTooltips: PropTypes.bool +} +DatasetDateItem.displayName = 'DatasetDateItem' +export default DatasetDateItem diff --git a/src/components/common/DatasetDate/__snapshots__/DatasetDate.test.js.snap b/src/components/common/DatasetDate/__snapshots__/DatasetDate.test.js.snap new file mode 100644 index 00000000..cb10c6d4 --- /dev/null +++ b/src/components/common/DatasetDate/__snapshots__/DatasetDate.test.js.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DatasetDate component. Matches snapshot. 1`] = ` +
+
+ + Last Modified + + : + February 1, 2023 +
+ + • + +
+ + Released + + : + January 1, 2023 +
+
+`; diff --git a/src/components/common/DatasetDate/__snapshots__/DatasetDate.test.jsx.snap b/src/components/common/DatasetDate/__snapshots__/DatasetDate.test.jsx.snap new file mode 100644 index 00000000..29ad6f49 --- /dev/null +++ b/src/components/common/DatasetDate/__snapshots__/DatasetDate.test.jsx.snap @@ -0,0 +1,187 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`DatasetDate component. > Matches snapshot. 1`] = ` +
+
+ + + Last Modified + + : + February 1, 2023 + +
+ + +
+
+ + • + +
+ + + Released + + : + January 1, 2023 + +
+ + +
+
+
+`; diff --git a/src/components/common/FileDownload/FileDownload.jsx b/src/components/common/FileDownload/FileDownload.jsx new file mode 100644 index 00000000..6faa2b4d --- /dev/null +++ b/src/components/common/FileDownload/FileDownload.jsx @@ -0,0 +1,117 @@ +import React, { useContext, useEffect } from 'react' +import PropTypes from 'prop-types' +import FontAwesomePro from '../FontAwesomePro/FontAwesomePro' +import { formatBytes } from '../../../utilities/downloadUtilities' +import { pdcAxiosInstance } from '../../../api/axiosInstance' +import log from '../../../log' +import DatasetDownloadLink from '../../dataset/DatasetDownloadLink/DatasetDownloadLink' +import { getApiBaseUrl } from '../../../utilities/getApiBaseUrl' +import { FilteredDispatch } from '../../dataset/DatasetResource/FilteredDatasetContext' + +function FileDownload (props) { + const { resource, isDatasetExplorerDownloadLink = false } = props + let downloadURL = null + + const { + downloadSize, + setDownloadSize + } = useContext(FilteredDispatch) + + if ( + Object.prototype.hasOwnProperty.call(resource, 'data') && + Object.prototype.hasOwnProperty.call(resource.data, 'data') && + Object.prototype.hasOwnProperty.call(resource.data.data, 'downloadURL') + ) { + const fixedURL = getApiBaseUrl(false) + downloadURL = `${fixedURL}${resource.data.data.downloadURL.split('/provider-data')[1]}` + } + + let format = null + + if ( + Object.prototype.hasOwnProperty.call(resource, 'data') && + Object.prototype.hasOwnProperty.call(resource.data, 'format') + ) { + format = resource.data.format + format.toUpperCase() + } + + let title = null + if ( + Object.prototype.hasOwnProperty.call(resource, 'data') && + Object.prototype.hasOwnProperty.call(resource.data, 'data') && + Object.prototype.hasOwnProperty.call(resource.data.data, 'title') + ) { + title = resource.data.data.title + } else { + title = 'This Dataset' + } + + useEffect(() => { + if (downloadURL) { + pdcAxiosInstance.head(downloadURL) + .then(({ headers }) => { + const cLength = headers['content-length'] + setDownloadSize(formatBytes(cLength)) + }) + .catch((e) => { + log.debug('Error retrieving fileSize, FileDownload: ', e) + }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [downloadURL]) + + const item = ( + <> + {downloadURL && title && format && ( + + + {isDatasetExplorerDownloadLink ? ( + Download full dataset ({format.toUpperCase()}) {`${downloadSize.size} ${downloadSize.sizeLabel}`} + ) : ( + Download full dataset - {format} - {`${downloadSize.size} ${downloadSize.sizeLabel}`} + )} + + )} + linkHref={downloadURL} + /> + )} + {!isDatasetExplorerDownloadLink && ( +
+ + {format} + + {downloadSize && ( + <> + + + {`${downloadSize.size} ${downloadSize.sizeLabel}`} + + + )} +
+ )} + + ) + + return isDatasetExplorerDownloadLink ? item :
{item}
+} + +FileDownload.propTypes = { + /** + * Dataset data object + */ + resource: PropTypes.shape({ + data: PropTypes.object, + format: PropTypes.string, + identifier: PropTypes.string + }), + /** + * Dataset explorer download link gets different styling + */ + isDatasetExplorerDownloadLink: PropTypes.bool +} +FileDownload.displayName = 'FileDownload' +export default FileDownload diff --git a/src/components/common/FileDownload/FileDownload.test.jsx b/src/components/common/FileDownload/FileDownload.test.jsx new file mode 100644 index 00000000..a64e879c --- /dev/null +++ b/src/components/common/FileDownload/FileDownload.test.jsx @@ -0,0 +1,111 @@ +import { render, screen } from '@testing-library/react' +import FileDownload from './FileDownload' +import { act } from '@testing-library/react' +import { resourceData, responseHeaders } from '../../../utilities/data-mocks/data-fileDownload' +import axiosMockAdapter from '../../../stories/utilities/axios-mock' +import log from '../../../log' +import { FilteredDispatch } from '../../dataset/DatasetResource/FilteredDatasetContext' +import { MemoryRouter } from 'react-router-dom' + +// Mock the config file +vi.mock('../../../../config', () => ({ + default: { + site: "https://pqdc-dkan.ddev.site/", + domainsNeedProviderDataPath: [] + } +})) + +// Mock window.location +global.window = Object.create(window) +Object.defineProperty(window, 'location', { + value: { + protocol: 'http:', + hostname: 'localhost' + }, + writable: true +}) + +const componentArgs = { + resource: resourceData, + isDatasetExplorerDownloadLink: false +} + + +const renderComponent = (args = componentArgs) => ( + render( + + + + + + ) +) + +describe('FileDownload component.', () => { + it('Matches snapshot.', () => { + const renderedFileDownload = renderComponent() + + expect(renderedFileDownload.asFragment()).toMatchSnapshot() + }) + + it('Does not render download link if resource prop data is not provided.', () => { + renderComponent({ + resource: {} + }) + + expect(screen.queryByText('Download full dataset')).not.toBeInTheDocument() + }) + + it('File download size displays.', async () => { + // Mock api request inside FileDownload + axiosMockAdapter.onHead(resourceData.data.downloadURL).reply(200, '', responseHeaders) + + window.location.protocol = 'https:', + window.location.hostname = 'pqdc-dkan.ddev.site' + + await act(async () => { + renderComponent() + }) + + expect(screen.queryByText('10 KB')).toBeInTheDocument() + }) + + it('Dataset explorer download link is formatted correctly.', async () => { + renderComponent({ + ...componentArgs, + isDatasetExplorerDownloadLink: true + }) + + expect(screen.queryByText('Download full dataset (CSV) 10 KB')).toBeInTheDocument() + }) + + it('Error is caught when api request fails.', async () => { + // Mock failed api request inside FileDownload + axiosMockAdapter.onHead(resourceData.data.downloadURL).reply(500, '', {}, {}) + const logDebug = vi.spyOn(log, 'debug') + + await act(async () => { + renderComponent() + }) + + expect(logDebug).toHaveBeenCalledWith('Error retrieving fileSize, FileDownload: ', expect.objectContaining({})) + }) +}) diff --git a/src/components/common/FileDownload/FilteredDownload.jsx b/src/components/common/FileDownload/FilteredDownload.jsx new file mode 100644 index 00000000..26bd7e0f --- /dev/null +++ b/src/components/common/FileDownload/FilteredDownload.jsx @@ -0,0 +1,86 @@ +import React from 'react' +import PropTypes from 'prop-types' +import FontAwesomePro from '../FontAwesomePro/FontAwesomePro' +import DatasetDownloadLink from '../../dataset/DatasetDownloadLink/DatasetDownloadLink' + +const FilteredDownload = ({ resource, datasetTitle, filteredURL, isDatasetExplorerDownloadLink = false }) => { + let format = null + + if ( + Object.prototype.hasOwnProperty.call(resource, 'data') && + Object.prototype.hasOwnProperty.call(resource.data, 'format') + ) { + format = resource.data.format + format.toUpperCase() + } + + let title = null + + if ( + Object.prototype.hasOwnProperty.call(resource, 'data') && + Object.prototype.hasOwnProperty.call(resource.data, 'data') && + Object.prototype.hasOwnProperty.call(resource.data.data, 'title') + ) { + title = resource.data.data.title + } else { + title = 'This Dataset' + } + + const item = ( + <> + {filteredURL && title && format && ( + + {isDatasetExplorerDownloadLink ? ( + + ) : ( + + )} + {isDatasetExplorerDownloadLink ? ( + Download filtered dataset ({format.toUpperCase()}) + ) : ( + Download filtered dataset - {format} + )} + + )} + linkHref={filteredURL} + /> + )} + {!isDatasetExplorerDownloadLink && ( +
+ + {format} + +
+ )} + + ) + + return isDatasetExplorerDownloadLink ? item :
{item}
+} + +FilteredDownload.propTypes = { + /** + * Dataset data object + */ + resource: PropTypes.shape({ + data: PropTypes.object, + format: PropTypes.string, + identifier: PropTypes.string + }), + /** + * Dataset title + */ + datasetTitle: PropTypes.string, + /** + * Filtered dataset download url + */ + filteredURL: PropTypes.string, + /** + * Dataset explorer download link gets different styling + */ + isDatasetExplorerDownloadLink: PropTypes.bool +} +FilteredDownload.displayName = 'FilteredDownload' +export default FilteredDownload diff --git a/src/components/common/FileDownload/FilteredDownload.test.jsx b/src/components/common/FileDownload/FilteredDownload.test.jsx new file mode 100644 index 00000000..8647c2e3 --- /dev/null +++ b/src/components/common/FileDownload/FilteredDownload.test.jsx @@ -0,0 +1,52 @@ +import { render, screen } from '@testing-library/react' +import FilteredDownload from './FilteredDownload' +import { resourceData } from '../../../utilities/data-mocks/data-fileDownload' +import { MemoryRouter } from 'react-router-dom' + +describe('FilteredDownload component.', () => { + it('Matches snapshot.', () => { + const renderedFilteredDownload = render( + + + + ) + + expect(renderedFilteredDownload.asFragment()).toMatchSnapshot() + }) + + it('Does not render download link if resource prop data is not provided.', () => { + render( + + + + ) + + expect(screen.queryByRole('link')).not.toBeInTheDocument() + }) +}) diff --git a/src/components/common/FileDownload/__snapshots__/FileDownload.test.js.snap b/src/components/common/FileDownload/__snapshots__/FileDownload.test.js.snap new file mode 100644 index 00000000..d09e4f86 --- /dev/null +++ b/src/components/common/FileDownload/__snapshots__/FileDownload.test.js.snap @@ -0,0 +1,156 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FileDownload component. Matches snapshot. 1`] = ` + +
+ + + + Download full dataset + + - csv - 10 KB + + + +
+ +
+
+

+ Notice: Downloading in Excel +

+ +
+
+
+

+ Before downloading these files in Excel, please review the + + Excel Download Instructions + + to ensure accuracy with the display of your data when downloading, opening and saving these files. +

+ +

+ Would you like to proceed? +

+
+
+ +
+
+
+
+
+
+ + csv + + + • + + + 10 KB + +
+
+
+`; diff --git a/src/components/common/FileDownload/__snapshots__/FileDownload.test.jsx.snap b/src/components/common/FileDownload/__snapshots__/FileDownload.test.jsx.snap new file mode 100644 index 00000000..21360382 --- /dev/null +++ b/src/components/common/FileDownload/__snapshots__/FileDownload.test.jsx.snap @@ -0,0 +1,156 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`FileDownload component. > Matches snapshot. 1`] = ` + +
+ + + + Download full dataset + + - csv - 10 KB + + + +
+ +
+
+

+ Notice: Downloading in Excel +

+ +
+
+
+

+ Before downloading these files in Excel, please review the + + Excel Download Instructions + + to ensure accuracy with the display of your data when downloading, opening and saving these files. +

+ +

+ Would you like to proceed? +

+
+
+ +
+
+
+
+
+
+ + csv + + + • + + + 10 KB + +
+
+
+`; diff --git a/src/components/common/FileDownload/__snapshots__/FilteredDownload.test.js.snap b/src/components/common/FileDownload/__snapshots__/FilteredDownload.test.js.snap new file mode 100644 index 00000000..42c0dabc --- /dev/null +++ b/src/components/common/FileDownload/__snapshots__/FilteredDownload.test.js.snap @@ -0,0 +1,146 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FilteredDownload component. Matches snapshot. 1`] = ` + +
+ + + + Download filtered dataset + + - csv + + + +
+ +
+
+

+ Notice: Downloading in Excel +

+ +
+
+
+

+ Before downloading these files in Excel, please review the + + Excel Download Instructions + + to ensure accuracy with the display of your data when downloading, opening and saving these files. +

+ +

+ Would you like to proceed? +

+
+
+ +
+
+
+
+
+
+ + csv + +
+
+
+`; diff --git a/src/components/common/FileDownload/__snapshots__/FilteredDownload.test.jsx.snap b/src/components/common/FileDownload/__snapshots__/FilteredDownload.test.jsx.snap new file mode 100644 index 00000000..385c02a0 --- /dev/null +++ b/src/components/common/FileDownload/__snapshots__/FilteredDownload.test.jsx.snap @@ -0,0 +1,146 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`FilteredDownload component. > Matches snapshot. 1`] = ` + +
+ + + + Download filtered dataset + + - csv + + + +
+ +
+
+

+ Notice: Downloading in Excel +

+ +
+
+
+

+ Before downloading these files in Excel, please review the + + Excel Download Instructions + + to ensure accuracy with the display of your data when downloading, opening and saving these files. +

+ +

+ Would you like to proceed? +

+
+
+ +
+
+
+
+
+
+ + csv + +
+
+
+`; diff --git a/src/components/common/FontAwesomePro/FontAwesomePro.jsx b/src/components/common/FontAwesomePro/FontAwesomePro.jsx new file mode 100644 index 00000000..4a63bfea --- /dev/null +++ b/src/components/common/FontAwesomePro/FontAwesomePro.jsx @@ -0,0 +1,81 @@ +import React, { useRef, useEffect } from 'react' +import PropTypes from 'prop-types' +import svgIndex from '../../../assets/icons' + +const FontAwesomePro = (props) => { + const { + 'aria-hidden': ariaHidden, + fill = '#323A45', + height = 16, + icon = 'long-arrow-right', + size, + width = 16 + } = props + + // Set fill on path + const ref = useRef() + useEffect(() => { + if (fill && ref.current && ref.current.firstElementChild) { + ref.current.firstElementChild.setAttribute('fill', fill) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [icon]) + + // Set default svg attribtues + const iconAttr = { + 'aria-hidden': ariaHidden, + className: `fa fa-${icon}`, + focusable: 'false', + height: size || height, + role: 'img', + width: size || width + } + + // Get svgIndex export array values + const svgList = svgIndex.values() + + // Loop through svgIndex export array + const iconMap = (ico) => { + for (const svg of svgList) { + const key = Object.keys(svg)[0] + if (key === ico) { + // Get value from svgIndex, which is a ReactComponent + const Value = Object.values(svg)[0] + return + } + } + } + + return ( + iconMap(icon) + ) +} + +FontAwesomePro.propTypes = { + /** + * Hide icon from screen reader + */ + 'aria-hidden': PropTypes.bool, + /** + * Icon color code string (hex, rgb, string, etc) + */ + fill: PropTypes.string, + /** + * SVG outer pixel height - Overwritten by 'size' prop if provided + */ + height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + /** + * FontAwesome icon name + */ + icon: PropTypes.string, + /** + * SVG pixel size - Applies to height and width equally + */ + size: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + /** + * SVG outer pixel width - Overwritten by 'size' prop if provided + */ + width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) +} +FontAwesomePro.displayName = 'FontAwesomePro' +export default FontAwesomePro diff --git a/src/components/common/FontAwesomePro/FontAwesomePro.test.jsx b/src/components/common/FontAwesomePro/FontAwesomePro.test.jsx new file mode 100644 index 00000000..31fb4c5d --- /dev/null +++ b/src/components/common/FontAwesomePro/FontAwesomePro.test.jsx @@ -0,0 +1,19 @@ +import { render, screen } from '@testing-library/react' +import renderer from 'react-test-renderer' +import FontAwesomePro from './FontAwesomePro' + +describe('FontAwesomePro component.', () => { + it('Matches snapshot.', () => { + const renderedFontAwesomePro = renderer.create( + + ).toJSON() + + expect(renderedFontAwesomePro).toMatchSnapshot() + }) + + it('Renders the custom fill if provided via props.', () => { + const { container } = render() + + expect(container.querySelector('path').getAttribute('fill')).toEqual('#990000') + }) +}) \ No newline at end of file diff --git a/src/components/common/FontAwesomePro/__snapshots__/FontAwesomePro.test.js.snap b/src/components/common/FontAwesomePro/__snapshots__/FontAwesomePro.test.js.snap new file mode 100644 index 00000000..69f7414c --- /dev/null +++ b/src/components/common/FontAwesomePro/__snapshots__/FontAwesomePro.test.js.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FontAwesomePro component. Matches snapshot. 1`] = ` + + + +`; diff --git a/src/components/common/FontAwesomePro/__snapshots__/FontAwesomePro.test.jsx.snap b/src/components/common/FontAwesomePro/__snapshots__/FontAwesomePro.test.jsx.snap new file mode 100644 index 00000000..ec053fbd --- /dev/null +++ b/src/components/common/FontAwesomePro/__snapshots__/FontAwesomePro.test.jsx.snap @@ -0,0 +1,18 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`FontAwesomePro component. > Matches snapshot. 1`] = ` + + + +`; diff --git a/src/components/common/Text/Text.jsx b/src/components/common/Text/Text.jsx new file mode 100644 index 00000000..f0232401 --- /dev/null +++ b/src/components/common/Text/Text.jsx @@ -0,0 +1,52 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Parser } from 'html-to-react' + +const Text = ({ + label, value, children, wrapper +}) => { + const parser = new Parser() + const { tag, classes } = wrapper + const TextWrapper = () => { + if (tag) { + const TagElement = `${tag}` + return ( + + {label && {`${label}: `}} + {value ? parser.parse(value) : children} + + ) + } + return ( + <> + {label && {`${label}: `}} + {value ? parser.parse(value) : children} + + ) + } + return +} + +Text.defaultProps = { + label: '', + value: '', + children: '', + wrapper: {} +} + +Text.propTypes = { + // Text in strong tag followed by semi colon. + label: PropTypes.string, + // The content of the Text component after the label. + // Will be required in future versions. + children: PropTypes.node, + // If classes are added, will wrap text in div with classes. + wrapper: PropTypes.shape({ + tag: PropTypes.string, + classes: PropTypes.string + }), + // Deprecated way to pass markup to the Text component. + value: PropTypes.string +} + +export default Text diff --git a/src/components/common/ToggleBlock/ToggleBlock.jsx b/src/components/common/ToggleBlock/ToggleBlock.jsx new file mode 100644 index 00000000..e80ad66e --- /dev/null +++ b/src/components/common/ToggleBlock/ToggleBlock.jsx @@ -0,0 +1,123 @@ +import React, { useState } from 'react' +import PropTypes from 'prop-types' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import classNames from 'classnames' + +export default function ToggleBlock ({ + customId = undefined, + className = 'toggle-block', + title, + children, + headingClasses = 'toggle-block-title', + headingLevel = 2, + innerClasses = 'toggle-block-inner', + allowToggle = true, + defaultClosed = false, + toggleCallback = () => {}, + additionalBlockHeadingMarkup = <> +}) { + const [show, toggleShow] = useState(!defaultClosed) + + const ToggleHeading = `h${headingLevel}` + + let toggleBlockHeading = ( + <> + + {title} + + {additionalBlockHeadingMarkup} + + ) + + if (allowToggle) { + toggleBlockHeading = ( + <> + + + + {additionalBlockHeadingMarkup} + + ) + } + + return ( +
+ {toggleBlockHeading} +
+ {children} +
+
+ ) +} + +ToggleBlock.propTypes = { + /** + * Custom id applied to wrapping element + */ + customId: PropTypes.string, + /** + * Button/Header text + */ + title: PropTypes.node.isRequired, + /** + * Panel content + */ + children: PropTypes.node.isRequired, + /** + * Custom classes applied to header element + */ + headingClasses: PropTypes.string, + /** + * Heading level applied to toggle button + */ + headingLevel: PropTypes.number, + /** + * Custom classes applied to panel element + */ + innerClasses: PropTypes.string, + /** + * 'true' makes the component an accordion element + */ + allowToggle: PropTypes.bool, + /** + * Custom classes applied to wrapping element + */ + className: PropTypes.string, + /** + * `true` sets the accordion element closed by default + */ + defaultClosed: PropTypes.bool, + /** + * Callback function used when accordion button element is toggled. + * Passes the `show` state value as an argument + */ + toggleCallback: PropTypes.func, + /** + * Additional markup to be added after the header h2. + * Used for data archive download links + */ + additionalBlockHeadingMarkup: PropTypes.node +} diff --git a/src/components/common/ToggleBlock/ToggleBlock.test.jsx b/src/components/common/ToggleBlock/ToggleBlock.test.jsx new file mode 100644 index 00000000..f11f1ad1 --- /dev/null +++ b/src/components/common/ToggleBlock/ToggleBlock.test.jsx @@ -0,0 +1,96 @@ +import { render } from '@testing-library/react' +import renderer from 'react-test-renderer' +import ToggleBlock from './ToggleBlock' +import userEvent from '@testing-library/user-event' +import { waitFor } from '@testing-library/react' +import ReactDOMServer from 'react-dom/server' + +const componentArgs = { + customId: '', + title: 'Donec eu libero sit amet quam egestas semper?', + children: ( +
+

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus

+
+ ), + headingClasses: 'toggle-block-title', + innerClasses: 'toggle-block-inner', + allowToggle: true, + className: 'toggle-block', + defaultClosed: false +} + +const renderComponent = (args = componentArgs) => render( + +) + +describe('ToggleBlock component.', () => { + it('Matches snapshot.', () => { + const renderedToggleBlock = renderer.create( + + ).toJSON() + + expect(renderedToggleBlock).toMatchSnapshot() + }) + + it('Renders toggle header if \'allowToggle\' prop is \'true\'.', () => { + const { container } = renderComponent() + + expect(container.querySelector('.toggle-block-title')).toBeInTheDocument() + expect(container.querySelector('.toggle-block-title').textContent).toEqual(componentArgs.title) + }) + + it('Does not render toggle header if \'allowToggle\' prop is \'false\'.', () => { + const { container } = renderComponent({ + ...componentArgs, + allowToggle: false + }) + + expect(container.querySelector('.toggle-block-title > button')).toBeNull() + }) + + it('Renders toggle panel open if \'defaultClosed\' prop is \'false\'.', () => { + const { container } = renderComponent() + + expect(container.querySelector('.toggle-block-inner')).toHaveClass('open') + expect(container.querySelector('.toggle-block-inner').innerHTML).toEqual(ReactDOMServer.renderToStaticMarkup(componentArgs.children)) + }) + + it('Renders toggle panel closed if \'defaultClosed\' prop is \'true\'.', () => { + const { container } = renderComponent({ + ...componentArgs, + defaultClosed: true + }) + + expect(container.querySelector('.toggle-block-inner')).toHaveClass('closed') + }) + + it('Toggle panel opens and closes when header button is clicked.', async () => { + const toggleCallback = vi.fn() + + const { container } = renderComponent({ + ...componentArgs, + toggleCallback: toggleCallback() + }) + + // Panel rendered open initially + expect(container.querySelector('.toggle-block-title > button').getAttribute('aria-expanded')).toEqual('true') + + // Click to close panel + await waitFor(() => userEvent.click(container.querySelector('.toggle-block-title > button'))) + + // Make assertions + expect(toggleCallback).toHaveBeenCalled() + expect(container.querySelector('.toggle-block-inner')).toHaveClass('closed') + expect(container.querySelector('.toggle-block-title > button').getAttribute('aria-expanded')).toEqual('false') + + // Click to open panel + await waitFor(() => userEvent.click(container.querySelector('.toggle-block-title > button'))) + + // Make assertions + expect(toggleCallback).toHaveBeenCalled() + expect(container.querySelector('.toggle-block-inner')).toHaveClass('open') + expect(container.querySelector('.toggle-block-inner').innerHTML).toEqual(ReactDOMServer.renderToStaticMarkup(componentArgs.children)) + expect(container.querySelector('.toggle-block-title > button').getAttribute('aria-expanded')).toEqual('true') + }) +}) diff --git a/src/components/common/ToggleBlock/__snapshots__/ToggleBlock.test.js.snap b/src/components/common/ToggleBlock/__snapshots__/ToggleBlock.test.js.snap new file mode 100644 index 00000000..ea147a21 --- /dev/null +++ b/src/components/common/ToggleBlock/__snapshots__/ToggleBlock.test.js.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ToggleBlock component. Matches snapshot. 1`] = ` +
+

+ +

+
+
+

+ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus +

+
+
+
+`; diff --git a/src/components/common/ToggleBlock/__snapshots__/ToggleBlock.test.jsx.snap b/src/components/common/ToggleBlock/__snapshots__/ToggleBlock.test.jsx.snap new file mode 100644 index 00000000..ca576a67 --- /dev/null +++ b/src/components/common/ToggleBlock/__snapshots__/ToggleBlock.test.jsx.snap @@ -0,0 +1,46 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ToggleBlock component. > Matches snapshot. 1`] = ` +
+

+ +

+
+
+

+ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus +

+
+
+
+`; diff --git a/src/components/common/Tooltip/Tooltip.jsx b/src/components/common/Tooltip/Tooltip.jsx new file mode 100644 index 00000000..60251ffe --- /dev/null +++ b/src/components/common/Tooltip/Tooltip.jsx @@ -0,0 +1,107 @@ +import React from 'react' +import cx from 'classnames' +import { usePopperTooltip } from 'react-popper-tooltip' +import PropTypes from 'prop-types' +import './Tooltip.scss' + +const Tooltip = ({ + children, + tooltip, + hideArrow, + type, + customClass, + tooltipClass, + show, + position = 'top' +}) => { + const { + getArrowProps, + getTooltipProps, + setTooltipRef, + setTriggerRef, + visible + } = usePopperTooltip({ + placement: position + }) + + const Trigger = () => ( + + {children} + + ) + + return ( + show ? ( + + {visible && ( + + {!hideArrow && ( + + )} + {tooltip} + + )} + {Trigger()} + + ) : ( + Trigger() + ) + ) +} + +Tooltip.defaultProps = { + delayShow: 500, + delayHide: 100, + hideArrow: true +} + +Tooltip.propTypes = { + /** + * Tooltip trigger content + */ + children: PropTypes.any, + /** + * Tooltip element content + */ + tooltip: PropTypes.any, + /** + * `true` shows the triangle-arrow on the tooltip container. + * The arrow is permanently hidden in component SCSS + */ + hideArrow: PropTypes.bool, + /** + * Custom class(es) added to tooltip and trigger wrapping elements + */ + type: PropTypes.string, + /** + * Custom class(es) added to the tooltip trigger wrapping element + */ + customClass: PropTypes.string, + /** + * Custom class(es) added to the tooltip wrapping element + */ + tooltipClass: PropTypes.string, + /** + * `true` allows the tooltip to be shown when invoked + */ + show: PropTypes.bool, + /** + * Position of the tooltip + */ + position: PropTypes.string +} + +Tooltip.displayName = 'Tooltip' +export default Tooltip diff --git a/src/components/common/Tooltip/Tooltip.scss b/src/components/common/Tooltip/Tooltip.scss new file mode 100644 index 00000000..320940cf --- /dev/null +++ b/src/components/common/Tooltip/Tooltip.scss @@ -0,0 +1,108 @@ +@use "../../../scss/modules/modules"; +@use "../../../scss/modules/colormap"; +@use "../../../scss/modules/functions"; +@use "../../../scss/modules/variables"; + +.tooltip-wrapper { + position: relative; + display: inline-flex; + justify-content: center; +} + +.tooltip-trigger { + + // Tooltip with icon trigger inline with text + &.tooltip-info { + display: inline-block; + vertical-align: middle; + padding: 4px; + margin: 0 4px 2px; + line-height: 0; + border-radius: 100%; + transition: opacity 0.1s linear; + + &:hover { + transition: opacity 0.1s linear; + background-color: colormap.$blueberry-10; + } + + svg.fa-info-circle { + font-size: initial; + + path { + fill: colormap.$blueberry; + } + } + } + + // Tooltip for term definitions in static content + &.definition { + @include functions.link-underline(colormap.$charcoal); + + &:hover { + cursor: help; + } + } +} + +span.tooltip-container { + background: rgba(0, 0, 0, 0.8); + border-radius: 2px; + border: none; + box-shadow: none; + color: white; + padding: 3px 8px 4px; + transition: opacity 0.1s linear; + display: inline-block; + inset: auto auto 100% auto !important; + transform: none !important; + + &.definition { + background: white; + color: colormap.$charcoal; + padding: 16px 24px; + border: 1px solid colormap.$charcoal-20; + box-shadow: variables.$panel-shadow; + max-width: 384px; + + .tooltip-arrow { + display: block; + } + } + + .tooltip-arrow { + display: none; + } + + &.invisible { + display: none; + } + + &.resize-tooltip { + opacity: 1; + left: 0; + // Transition In + transition-property: opacity, left; + transition-timing-function: linear; + transition-duration: 0.1s; + + &.hide-resize-tooltip { + opacity: 0; + left: -9999px !important; + // Transition Out + transition-property: opacity, left; + transition-timing-function: linear; + transition-duration: 0s; + } + } +} + +@media screen and (max-width: 991px) { + span.tooltip-container { + &.definition { + max-width: 256px; + font-size: 1.4rem; + line-height: 1.4; + } + } +} diff --git a/src/components/common/Tooltip/Tooltip.test.jsx b/src/components/common/Tooltip/Tooltip.test.jsx new file mode 100644 index 00000000..38d0d404 --- /dev/null +++ b/src/components/common/Tooltip/Tooltip.test.jsx @@ -0,0 +1,67 @@ +import { render, screen } from '@testing-library/react' +import renderer from 'react-test-renderer' +import Tooltip from './Tooltip' +import userEvent from '@testing-library/user-event' +import { waitFor } from '@testing-library/react' + +const componentArgs = { + tooltip: 'Tooltip', + show: true, + hideArrow: false, + children: ( +
+ Hover over me +
+ ) +} + +const renderComponent = (args = componentArgs) => render( + +) + +describe('Tooltip component.', () => { + it('Matches snapshot.', () => { + const renderedTooltip = renderer.create( + + ).toJSON() + + expect(renderedTooltip).toMatchSnapshot() + }) + + it('Renders tooltip when \'show\' prop is \'true\'.', async () => { + renderComponent() + + // Ensure trigger exists + expect(screen.queryByText('Hover over me')).toBeInTheDocument() + + // Trigger tooltip + await waitFor(() => userEvent.hover(screen.getByText('Hover over me'))) + + // Ensure tooltip shows + expect(screen.queryByText(componentArgs.tooltip)).toBeInTheDocument() + }) + + it('Does not render tooltip when \'show\' prop is \'false\'.', async () => { + renderComponent({ + ...componentArgs, + show: false + }) + + // Ensure trigger exists + expect(screen.queryByText('Hover over me')).toBeInTheDocument() + + // Trigger tooltip + await waitFor(() => userEvent.hover(screen.getByText('Hover over me'))) + + // Ensure tooltip does not show + expect(screen.queryByText(componentArgs.tooltip)).not.toBeInTheDocument() + }) + + it('Tooltip shows when triggered.', async () => { + renderComponent() + + await waitFor(() => userEvent.hover(screen.getByText('Hover over me'))) + + expect(screen.queryByText(componentArgs.tooltip)).toBeInTheDocument() + }) +}) diff --git a/src/components/common/Tooltip/__snapshots__/Tooltip.test.js.snap b/src/components/common/Tooltip/__snapshots__/Tooltip.test.js.snap new file mode 100644 index 00000000..af9700ef --- /dev/null +++ b/src/components/common/Tooltip/__snapshots__/Tooltip.test.js.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Tooltip component. Matches snapshot. 1`] = ` + + +
+ + Hover over me + +
+
+
+`; diff --git a/src/components/common/Tooltip/__snapshots__/Tooltip.test.jsx.snap b/src/components/common/Tooltip/__snapshots__/Tooltip.test.jsx.snap new file mode 100644 index 00000000..69f53837 --- /dev/null +++ b/src/components/common/Tooltip/__snapshots__/Tooltip.test.jsx.snap @@ -0,0 +1,23 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Tooltip component. > Matches snapshot. 1`] = ` + + +
+ + Hover over me + +
+
+
+`; diff --git a/src/components/dataset/DataTable/DataTable.jsx b/src/components/dataset/DataTable/DataTable.jsx new file mode 100644 index 00000000..7b8d9565 --- /dev/null +++ b/src/components/dataset/DataTable/DataTable.jsx @@ -0,0 +1,435 @@ +import React, { useContext, useEffect, useState, useRef } from 'react' +import PerfectScrollbar from 'react-perfect-scrollbar' +import cx from 'classnames' +import PropTypes from 'prop-types' +import Tooltip from '../../common/Tooltip/Tooltip' +import FontAwesomePro from '../../common/FontAwesomePro/FontAwesomePro' +import { FilteredDispatch } from '../DatasetResource/FilteredDatasetContext' +import { DatasetContentLoading } from '../DatasetContentLoading/DatasetContentLoading' +import './DataTable.scss' + +const DataTable = ({ datasetTitle }) => { + const { filteredTable, filteredResource, activeDensity } = useContext(FilteredDispatch) + const [ariaLiveFeedback, setAriaLiveFeedback] = useState('') + const [columnResizing, setColumnResizing] = useState([]) + const resetFeedbackTimer = useRef(undefined) + + const densityMap = { + Expanded: 'density-1', + Normal: 'density-2', + Compact: 'density-3' + } + const density = activeDensity + ? `${densityMap[activeDensity]} -striped -highlight` + : '-striped -highlight' + const { + getTableProps, + getTableBodyProps, + headerGroups, + prepareRow, + page, + state: { sortBy }, + pageOptions, + dispatch + } = filteredTable + + const { + count, + offset, + setOffset, + limit, + loading + } = filteredResource + + // limit is actually a string causing pagination failures since Javascript would concatenate it + const goToNext = () => { + setOffset(offset + Number(limit)) + } + const goToPrev = () => { + setOffset(offset - Number(limit)) + } + + const canNextPage = Number(limit) + offset < count + // Used to disable all tooltip hover actions while resizing + let anyIsResizing = false + const resizeHandler = () => { + anyIsResizing = true + } + useEffect(() => { + if (sortBy.length) { + // useDataStore needs desc or asc + const order = sortBy[0].desc === true ? 'desc' : 'asc' + const sortObject = { + property: sortBy[0].id, + order + } + filteredResource.setSort([sortObject]) + } else { + filteredResource.setSort([]) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sortBy]) + + useEffect(() => { + if (headerGroups.length) { + const columns = [] + + headerGroups.forEach((headerGroup) => { + headerGroup.headers.forEach((column) => { + columns.push({ + id: column.id, + resizing: false + }) + }) + }) + + setColumnResizing(columns) + } + }, [headerGroups]) + + const resetFeedback = () => { + if (resetFeedbackTimer.current) { + clearTimeout(resetFeedbackTimer.current) + } + + resetFeedbackTimer.current = setTimeout(() => { + setAriaLiveFeedback('') + }, 5000) + } + + useEffect(() => { + if (ariaLiveFeedback) { + resetFeedback() + } + }, [ariaLiveFeedback]) + + const currentPage = Number(offset / limit) + if (loading && !count) { + return + } + + // Add comma separators to total page number + const pageSeparator = pageOptions && pageOptions.length.toLocaleString() + + return ( +
+
+ +
+
+ {headerGroups.map((headerGroup, i) => ( +
+ {headerGroup.headers.map((column, i) => { + column.isResizing && resizeHandler() + + return ( +
+ + {column.render('Header')} + + {!column.isSorted ? ( + + + + ) : column.isSortedDesc ? ( + + + + ) : ( + + + + )} + +
+ ) + })} +
+ ))} +
+
+ {headerGroups.map((headerGroup, i) => ( +
+ {headerGroup.headers.map((column, i) => { + return ( +
+
+ {column.canFilter ? column.render('Filter') : null} + {column.filterValue && ( + + + + )} +
+
+ ) + })} +
+ ))} +
+
+ {page.map((row, i) => { + prepareRow(row) + + return ( +
+ {row.cells.map((cell, i) => ( +
+ {cell.render('Cell')} +
+ ))} +
+ ) + })} +
+
+
{ariaLiveFeedback}
+
+
+
+
+
+ +
+
+ + Page{' '} + {currentPage + 1} + {' '}of{' '} + {pageSeparator} + {' '}for {datasetTitle} + +
+
+ +
+
+
+
+ ) +} + +DataTable.propTypes = { + /** + * Dataset title. Used for UTag logic + */ + datasetTitle: PropTypes.string +} +DataTable.displayName = 'DataTable' +export default DataTable diff --git a/src/components/dataset/DataTable/DataTable.scss b/src/components/dataset/DataTable/DataTable.scss new file mode 100644 index 00000000..85136c36 --- /dev/null +++ b/src/components/dataset/DataTable/DataTable.scss @@ -0,0 +1,411 @@ +@use "../../../scss/modules/modules"; +@use "../../../scss/modules/colormap"; +@use "../../../scss/modules/variables"; + +.DataTable { + // Scrollbar + .ps { + // Rail + div[class^="ps__rail-"] { + opacity: 1 !important; + transition-duration: 0.1s; + + &:hover, + &.ps--clicking { + background-color: white; + transition: background-color 0.1s, box-shadow 0.1s; + + &[class*="-y"] { + box-shadow: -1px 0 0 0 colormap.$border-color; + } + + &[class*="-x"] { + box-shadow: 0 -1px 0 0 colormap.$border-color; + } + } + + // Thumb + div[class^="ps__thumb-"] { + background-color: colormap.$charcoal-70; + border-radius: 8px; + transition-duration: 0.1s; + + &:hover { + background-color: colormap.$charcoal-70; + } + } + } + } + + // Expanded + &.density-1 .dc-tbody .dc-td { + padding: 24px 8px; + } + + // Normal + &.density-2 .dc-tbody .dc-td { + padding: 16px 8px; + } + + // Compact + &.density-3 .dc-tbody .dc-td { + padding: 8px; + } + + &.-striped { + .dc-tbody { + .tr:nth-child(odd) { + background: colormap.$charcoal-5; + } + } + } + + .dc-table { + min-width: fit-content !important; + .dc-thead { + // position: -webkit-sticky; // for safari + // position: sticky; + // top: 0; left: 0; + border-bottom: solid 1px rgb(225, 226, 227); + background-color: #fafafa; + .-filters { + display: none; + } + } + + } + + .dc-table-container { + border-radius: 2px; + border: 1px solid colormap.$border-color; + } + + .td { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + border-right: 1px solid colormap.$charcoal-15; + + &.isResizing { + background-color: colormap.$dragonfruit; + } + + &:last-child { + border-right: none; + } + } + + .th { + font-weight: 700; + letter-spacing: 0.25px; + border-right: 1px solid colormap.$charcoal-15; + display: flex; + + &:focus { + outline-offset: -4px !important; + } + + .column-title { + flex: 1 1 auto; + display: block; + padding: 8px; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .sort-icon { + flex: none; + display: flex; + padding: 4px; + + button { + background: none; + border: 0; + margin: 0; + padding: 0; + height: 100%; + } + } + } + + .-header { + div.th { + display: flex; + height: 40px; + background: none; + text-align: left; + border: 0; + color: colormap.$charcoal; + position: static !important; + + .resizer { + width: 15px; + border: 0; + padding: 0; + margin: 0; + background-color: transparent; + outline-offset: -2px; + } + + &:hover { + .resizer { + background-color: colormap.$charcoal-15; + } + } + + .resizer-container { + border-right: 1px solid colormap.$charcoal-15; + } + + &:last-child { + display: flex; + border: 0; + + .resizer-container { + border-right: 0; + + .resizer { + width: 8px; + } + } + } + + // Control cursor style while dragging + &.resizeCursor { + cursor: col-resize !important; + background-color: colormap.$charcoal-5; + text-decoration: underline; + + ~ button { + cursor: col-resize !important; + + &:hover { + cursor: col-resize !important; + text-decoration: none; + + .resizer { + background-color: transparent; + } + } + } + } + + .sort-icon { + display: flex; + align-items: center; + flex: 0 0 auto; + margin-left: 4px; + + svg { + display: flex; + align-items: center; + + path { + fill: colormap.$charcoal-70; + } + } + } + + > span { + min-width: 0; + flex: 1 1 auto; + } + + &.-sort-asc, + &.-sort-desc { + box-shadow: 0 2px 0 0 colormap.$dragonfruit inset; + background-color: colormap.$dragonfruit-2; + + svg path { + fill: colormap.$dragonfruit; + } + } + + $resizer-width: 12px; + .resizer-container { + flex: none; + display: flex; + + .resizer { + display: inline-block; + width: $resizer-width; + height: 100%; + cursor: col-resize; + + &:hover, + &:focus { + background-color: colormap.$charcoal-20; + } + + &.isResizing { + background-color: colormap.$charcoal-40; + cursor: col-resize; + } + } + } + } + } + + .dc-tbody { + .tr.dc-tr { + border-bottom: solid 1px colormap.$charcoal-10; + cursor: default; + &:hover { + background: colormap.$charcoal-10; + } + } + } + + .-filters { + // border-bottom: 1px solid $charcoal-30; + display: none; + .th { + &:last-child { + border: 0; + } + + &.filter-value { + background-color: colormap.$dragonfruit-2; + + input { + padding: 8px 36px 8px 8px; + } + } + + > div { + display: flex; + align-items: center; + } + + button { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 100%; + border: 0; + background: none; + color: colormap.$charcoal; + padding: 0; + + &:hover { + background-color: colormap.$charcoal-10; + } + + svg { + font-size: inherit; + } + } + + input { + border: 1px solid colormap.$border-color; + width: 100%; + height: 40px; + padding: 8px; + margin-right: -32px; + + &:hover { + border-color: colormap.$input-hover; + } + + &:focus { + border-color: colormap.$link-color; + } + + // When input has value... + &:not(:placeholder-shown) { + border-color: colormap.$dragonfruit; + } + + &::placeholder { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + } + } + } + + .pagination-bottom { + width: 100%; + + .-pagination { + display: flex; + align-items: center; + min-height: 64px; + padding: 16px 0; + border-top: 0; + box-shadow: none; + flex: 1; + + .-center { + flex: 1; + text-align: center; + } + + .-next button { + padding: 0 20px 2px 24px; + + svg { + margin-left: 8px; + } + } + + .-previous button { + padding: 0 24px 2px 20px; + + svg { + margin-right: 8px; + } + } + + button { + height: 40px; + border-radius: 20px; + color: colormap.$blueberry; + font: 700 1.6rem variables.$muli; + letter-spacing: 0.25px; + border: 1px solid colormap.$blueberry-20; + background-color: white; + + &:hover:not(:disabled) { + color: colormap.$blueberry; + border-color: colormap.$blueberry-30; + background-color: colormap.$blueberry-5; + } + + &:disabled { + color: colormap.$charcoal-40; + background-color: colormap.$charcoal-5; + cursor: not-allowed; + + svg path { + fill: colormap.$charcoal-40; + } + } + + svg path { + fill: colormap.$blueberry; + } + } + } + } + + span.tooltip-wrapper { + /* These are overriding inline style= attributes set by the Tooltip + component, so they have to use !important: */ + span.tooltip-container { + left: 0 !important; + position: absolute !important; + transform: none !important; + bottom: -80% !important; + background-color: black !important; + color: white !important; + width: auto; + white-space: nowrap; + } + } +} diff --git a/src/components/dataset/DataTable/DataTable.test.jsx b/src/components/dataset/DataTable/DataTable.test.jsx new file mode 100644 index 00000000..e1474fe4 --- /dev/null +++ b/src/components/dataset/DataTable/DataTable.test.jsx @@ -0,0 +1,593 @@ +import renderer from 'react-test-renderer' +import DataTable from './DataTable' +import { render, act, waitFor, screen, fireEvent } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import FilteredDatasetResource from '../DatasetResource/FilteredDatasetResource' +import { filteredDatasetResource } from '../../../utilities/data-mocks/data-filteredDatasetResource' +import axios from 'axios' + +// Mock axios +vi.mock('axios') + +const mockApiResponse = { + 'results': [ + { + measure_name: 'Average Medicare spending associated with an agency\u0027s home health episodes compared with all home health episodes nationally - agency score', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'Average Medicare spending associated with an agency\u0027s home health episodes compared with all home health episodes nationally - count', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'For patients with diabetes, how often the home health team got doctor\u0027s orders, gave foot care, and taught patients about foot care', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often home health patients had new or worsened pressure ulcers', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often home health patients had to be admitted to the hospital', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often home health patients, who have had a recent hospital stay, had a preventable hospital readmission within 30 days of discharge from home health', + measure_date_range: 'January 1, 2016 - December 31, 2018' + }, + { + measure_name: 'How often patients got better at bathing', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients got better at getting in and out of bed', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients got better at taking their drugs correctly by mouth', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients got better at walking or moving around', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients receiving home health care needed any urgent, unplanned care in the hospital emergency room - without being admitted to the hospital', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients remained at home within 31 days of being discharged from home health', + measure_date_range: 'January 1, 2017 - December 31, 2018' + }, + { + measure_name: 'How often patients\u0027 breathing improved', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients\u0027 wounds improved or healed after an operation', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team began their patients\u0027 care in a timely manner', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team checked patients for depression', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team checked patients\u0027 medications and got doctor\u0027s orders for medication issues in a timely manner', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team checked patients\u0027 risk of falling', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team made sure that their patients have received a flu shot for the current flu season', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team made sure that their patients have received a pneumococcal vaccine (pneumonia shot)', + measure_date_range: 'January 1, 2018 - December 31, 2018' + } + ], + count: 21, + schema: { + 'a4eb46a4-fd19-55b0-baf4-de08c3a70abd': { + fields: { + measure_name: { + type: 'text', + mysql_type: 'text', + description: 'Measure Name' + }, + measure_date_range: { + type: 'text', + mysql_type: 'text', + description: 'Measure Date Range' + } + } + } + }, + query: { + keys: true, + limit: 20, + offset: 0, + resources: [ + { + id: 'a4eb46a4-fd19-55b0-baf4-de08c3a70abd', + alias: 't' + } + ], + count: true, + results: true, + schema: true, + format: 'json', + rowIds: false, + properties: [ + 'measure_name', + 'measure_date_range' + ] + } +} + +const renderComponent = async () => render( + + + +) + +describe('DataTable component.', () => { + it('Matches snapshot.', async () => { + let renderedDataTable + + axios.mockResolvedValueOnce({ + data: { + ...mockApiResponse + } + }) + + await act(async () => { + renderedDataTable = renderer.create( + + + + ) + }) + + expect(renderedDataTable.toJSON()).toMatchSnapshot() + }) + + it('The \'Next\' and \'Previous\' buttons change data pages in the table.', async () => { + axios + .mockResolvedValueOnce({ + data: { + ...mockApiResponse + } + }) + .mockResolvedValueOnce({ + data: { + ...mockApiResponse, + results: [ + { + measure_name: 'How often the home health team taught patients (or their family caregivers) about their drugs', + measure_date_range: 'January 1, 2018 - December 31, 2018' + } + ], + offset: 20 + } + }) + .mockResolvedValueOnce({ + data: { + ...mockApiResponse + } + }) + + let element + await act(async () => { + element = await renderComponent({}) + }) + + await waitFor(() => userEvent.click(screen.getByRole('button', { name: 'Next data page'} ))) + expect(element.container.querySelector('.-pageInfo')).toHaveTextContent('Page 2 of 2 for Home Health Care - Measure Date Range') + + await waitFor(() => userEvent.click(screen.getByRole('button', { name: 'Previous data page'} ))) + expect(element.container.querySelector('.-pageInfo')).toHaveTextContent('Page 1 of 2 for Home Health Care - Measure Date Range') + }) + + it('User can resize the table columns with a keyboard', async () => { + // Set fake timers + vi.useFakeTimers() + + axios.mockResolvedValueOnce({ + data: { + ...mockApiResponse + } + }) + + let element + await act(async () => { + element = await renderComponent({}) + }) + + // Click just for higher test coverage + fireEvent.click(screen.getByLabelText('Resize Measure Name column')) + + // Enter key to grab the resize handle + fireEvent.keyDown(screen.getByLabelText('Resize Measure Name column'), { + key: 'Enter', + code: 'Enter', + keyCode: 13, + charCode: 13 + }) + expect(element.container.querySelector('.aria-live-feedback')).toHaveTextContent('Measure Name grabbed.') + + // ArrowRight key to increase the first column width 10px + fireEvent.keyDown(screen.getByLabelText('Resize Measure Name column'), { + key: 'ArrowRight', + code: 'ArrowRight', + keyCode: 39, + charCode: 39 + }) + expect(element.container.querySelector('.aria-live-feedback')).toHaveTextContent('Measure Name has been resized. The new width is 160 pixels.') + + // Escape key to drop the column + fireEvent.keyDown(screen.getByLabelText('Resize Measure Name column'), { + key: 'Escape', + code: 'Escape', + keyCode: 27, + charCode: 27 + }) + expect(element.container.querySelector('.aria-live-feedback')).toHaveTextContent('Measure Name dropped.') + + // Enter key to grab the resize handle + fireEvent.keyDown(screen.getByLabelText('Resize Measure Name column'), { + key: 'Enter', + code: 'Enter', + keyCode: 13, + charCode: 13 + }) + expect(element.container.querySelector('.aria-live-feedback')).toHaveTextContent('Measure Name grabbed.') + + // ArrowLeft key to reduce the first column width 10px + fireEvent.keyDown(screen.getByLabelText('Resize Measure Name column'), { + key: 'ArrowLeft', + code: 'ArrowLeft', + keyCode: 37, + charCode: 37 + }) + expect(element.container.querySelector('.aria-live-feedback')).toHaveTextContent('Measure Name has been resized. The new width is 150 pixels.') + + // Space key to drop the column + fireEvent.keyDown(screen.getByLabelText('Resize Measure Name column'), { + key: ' ', + code: 'Space', + keyCode: 32, + charCode: 32 + }) + expect(element.container.querySelector('.aria-live-feedback')).toHaveTextContent('Measure Name dropped.') + + // Enter key to grab the resize handle one last time + fireEvent.keyDown(screen.getByLabelText('Resize Measure Name column'), { + key: 'Enter', + code: 'Enter', + keyCode: 13, + charCode: 13 + }) + expect(element.container.querySelector('.aria-live-feedback')).toHaveTextContent('Measure Name grabbed.') + + // Advance time by 5 seconds + await act(async () => { + vi.advanceTimersByTime(5000) + }) + // aria-live region clears text after 5 seconds so it's not left hanging around + expect(element.container.querySelector('.aria-live-feedback')).toBeEmptyDOMElement() + + // Blur the resize handle to end resizing + fireEvent.blur(screen.getByLabelText('Resize Measure Name column')) + expect(element.container.querySelector('[aria-label="Resize Measure Name column"]').classList.contains('isResizing')).toBe(false) + + // Reset timers + vi.useRealTimers() + }) + + it('Columns don\'t resize above 400px', async () => { + axios.mockResolvedValueOnce({ + data: { + ...mockApiResponse + } + }) + + let element + await act(async () => { + element = await renderComponent({}) + }) + + // Enter key to grab the resize handle + fireEvent.keyDown(screen.getByLabelText('Resize Measure Name column'), { + key: 'Enter', + code: 'Enter', + keyCode: 13, + charCode: 13 + }) + + // ArrowRight key to increase the first column width 10px + for (let i = 1; i <= 26; i++) { // Iterate 26 times to set the width to 410px (Increments by 10 and starts at 150) + fireEvent.keyDown(screen.getByLabelText('Resize Measure Name column'), { + key: 'ArrowRight', + code: 'ArrowRight', + keyCode: 39, + charCode: 39 + }) + } + expect(element.container.querySelector('.aria-live-feedback')).toHaveTextContent('Measure Name has been resized. The new width is 400 pixels.') + }) + + it('Columns don\'t resize below 30px', async () => { + axios.mockResolvedValueOnce({ + data: { + ...mockApiResponse + } + }) + + let element + await act(async () => { + element = await renderComponent({}) + }) + + // Enter key to grab the resize handle + fireEvent.keyDown(screen.getByLabelText('Resize Measure Name column'), { + key: 'Enter', + code: 'Enter', + keyCode: 13, + charCode: 13 + }) + + // ArrowRight key to increase the first column width 10px + for (let i = 1; i <= 13; i++) { // Iterate 13 times to set the width to 20px (Increments by 10 and starts at 150) + fireEvent.keyDown(screen.getByLabelText('Resize Measure Name column'), { + key: 'ArrowLeft', + code: 'ArrowLeft', + keyCode: 37, + charCode: 37 + }) + } + expect(element.container.querySelector('.aria-live-feedback')).toHaveTextContent('Measure Name has been resized. The new width is 30 pixels.') + }) + + it('User can sort the table columns', async () => { + axios + .mockResolvedValueOnce({ + data: { + ...mockApiResponse + } + }) + .mockResolvedValueOnce({ + data: { + ...mockApiResponse, + results: [ + { + measure_name: 'Average Medicare spending associated with an agency\u0027s home health episodes compared with all home health episodes nationally - agency score', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'Average Medicare spending associated with an agency\u0027s home health episodes compared with all home health episodes nationally - count', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'For patients with diabetes, how often the home health team got doctor\u0027s orders, gave foot care, and taught patients about foot care', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often home health patients had new or worsened pressure ulcers', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often home health patients had to be admitted to the hospital', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often home health patients, who have had a recent hospital stay, had a preventable hospital readmission within 30 days of discharge from home health', + measure_date_range: 'January 1, 2016 - December 31, 2018' + }, + { + measure_name: 'How often patients got better at bathing', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients got better at getting in and out of bed', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients got better at taking their drugs correctly by mouth', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients got better at walking or moving around', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients receiving home health care needed any urgent, unplanned care in the hospital emergency room - without being admitted to the hospital', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients remained at home within 31 days of being discharged from home health', + measure_date_range: 'January 1, 2017 - December 31, 2018' + }, + { + measure_name: 'How often patients\u0027 breathing improved', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients\u0027 wounds improved or healed after an operation', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team began their patients\u0027 care in a timely manner', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team checked patients for depression', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team checked patients\u0027 medications and got doctor\u0027s orders for medication issues in a timely manner', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team checked patients\u0027 risk of falling', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team made sure that their patients have received a flu shot for the current flu season', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team made sure that their patients have received a pneumococcal vaccine (pneumonia shot)', + measure_date_range: 'January 1, 2018 - December 31, 2018' + } + ], + query: { + ...mockApiResponse.query, + sorts: [ + { + property: 'measure_name', + order: 'asc' + } + ] + } + } + }) + .mockResolvedValueOnce({ + data: { + ...mockApiResponse, + results: [ + { + measure_name: 'How often the home health team taught patients (or their family caregivers) about their drugs', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team made sure that their patients have received a pneumococcal vaccine (pneumonia shot)', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team made sure that their patients have received a flu shot for the current flu season', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team checked patients\u0027 risk of falling', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team checked patients\u0027 medications and got doctor\u0027s orders for medication issues in a timely manner', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team checked patients for depression', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team began their patients\u0027 care in a timely manner', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients\u0027 wounds improved or healed after an operation', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients\u0027 breathing improved', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients remained at home within 31 days of being discharged from home health', + measure_date_range: 'January 1, 2017 - December 31, 2018' + }, + { + measure_name: 'How often patients receiving home health care needed any urgent, unplanned care in the hospital emergency room - without being admitted to the hospital', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients got better at walking or moving around', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients got better at taking their drugs correctly by mouth', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients got better at getting in and out of bed', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients got better at bathing', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often home health patients, who have had a recent hospital stay, had a preventable hospital readmission within 30 days of discharge from home health', + measure_date_range: 'January 1, 2016 - December 31, 2018' + }, + { + measure_name: 'How often home health patients had to be admitted to the hospital', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often home health patients had new or worsened pressure ulcers', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'For patients with diabetes, how often the home health team got doctor\u0027s orders, gave foot care, and taught patients about foot care', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'Average Medicare spending associated with an agency\u0027s home health episodes compared with all home health episodes nationally - count', + measure_date_range: 'January 1, 2018 - December 31, 2018' + } + ], + query: { + ...mockApiResponse.query, + sorts: [ + { + property: 'measure_name', + order: 'desc' + } + ] + } + } + }) + .mockResolvedValueOnce({ + data: { + ...mockApiResponse + } + }) + + let element + await act(async () => { + element = await renderComponent({}) + }) + + // Check initial value + expect(element.container.querySelector('.dc-thead > .tr > .th:first-child .sort-icon > button').getAttribute('aria-label')).toEqual('Sort Measure Name column in ascending order') + + // Check sorting asc + await act(async () => { + fireEvent.click(element.container.querySelector('.dc-thead > .tr > .th:first-child .sort-icon > button')) + }) + expect(element.container.querySelector('.dc-thead > .tr > .th:first-child .sort-icon > button').getAttribute('aria-label')).toEqual('Sort Measure Name column in descending order') + + // Check sorting desc + await act(async () => { + fireEvent.click(element.container.querySelector('.dc-thead > .tr > .th:first-child .sort-icon > button')) + }) + expect(element.container.querySelector('.dc-thead > .tr > .th:first-child .sort-icon > button').getAttribute('aria-label')).toEqual('Reset Measure Name column sort order') + + // Check sorting reset + await act(async () => { + fireEvent.click(element.container.querySelector('.dc-thead > .tr > .th:first-child .sort-icon > button')) + }) + expect(element.container.querySelector('.dc-thead > .tr > .th:first-child .sort-icon > button').getAttribute('aria-label')).toEqual('Sort Measure Name column in ascending order') + }) +}) diff --git a/src/components/dataset/DataTable/__snapshots__/DataTable.test.js.snap b/src/components/dataset/DataTable/__snapshots__/DataTable.test.js.snap new file mode 100644 index 00000000..1a0d05f6 --- /dev/null +++ b/src/components/dataset/DataTable/__snapshots__/DataTable.test.js.snap @@ -0,0 +1,1142 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DataTable component. Matches snapshot. 1`] = ` +
+
+
+
+
+
+
+ + Measure Name + + + + + +
+
+ + Measure Date Range + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Average Medicare spending associated with an agency's home health episodes compared with all home health episodes nationally - agency score +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ Average Medicare spending associated with an agency's home health episodes compared with all home health episodes nationally - count +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ For patients with diabetes, how often the home health team got doctor's orders, gave foot care, and taught patients about foot care +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often home health patients had new or worsened pressure ulcers +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often home health patients had to be admitted to the hospital +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often home health patients, who have had a recent hospital stay, had a preventable hospital readmission within 30 days of discharge from home health +
+
+ January 1, 2016 - December 31, 2018 +
+
+
+
+ How often patients got better at bathing +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients got better at getting in and out of bed +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients got better at taking their drugs correctly by mouth +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients got better at walking or moving around +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients receiving home health care needed any urgent, unplanned care in the hospital emergency room - without being admitted to the hospital +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients remained at home within 31 days of being discharged from home health +
+
+ January 1, 2017 - December 31, 2018 +
+
+
+
+ How often patients' breathing improved +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients' wounds improved or healed after an operation +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team began their patients' care in a timely manner +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team checked patients for depression +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team checked patients' medications and got doctor's orders for medication issues in a timely manner +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team checked patients' risk of falling +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team made sure that their patients have received a flu shot for the current flu season +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team made sure that their patients have received a pneumococcal vaccine (pneumonia shot) +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+
+
+
+
+
+
+ +
+
+ + + Page + + + + 1 + + + + of + + + + 2 + + + + for + Home Health Care - Measure Date Range + + +
+
+ +
+
+
+
+`; diff --git a/src/components/dataset/DataTable/__snapshots__/DataTable.test.jsx.snap b/src/components/dataset/DataTable/__snapshots__/DataTable.test.jsx.snap new file mode 100644 index 00000000..c992c047 --- /dev/null +++ b/src/components/dataset/DataTable/__snapshots__/DataTable.test.jsx.snap @@ -0,0 +1,1150 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`DataTable component. > Matches snapshot. 1`] = ` +
+
+
+
+
+
+
+ + + Measure Name + + + + + + +
+
+ + + Measure Date Range + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Average Medicare spending associated with an agency's home health episodes compared with all home health episodes nationally - agency score +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ Average Medicare spending associated with an agency's home health episodes compared with all home health episodes nationally - count +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ For patients with diabetes, how often the home health team got doctor's orders, gave foot care, and taught patients about foot care +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often home health patients had new or worsened pressure ulcers +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often home health patients had to be admitted to the hospital +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often home health patients, who have had a recent hospital stay, had a preventable hospital readmission within 30 days of discharge from home health +
+
+ January 1, 2016 - December 31, 2018 +
+
+
+
+ How often patients got better at bathing +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients got better at getting in and out of bed +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients got better at taking their drugs correctly by mouth +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients got better at walking or moving around +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients receiving home health care needed any urgent, unplanned care in the hospital emergency room - without being admitted to the hospital +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients remained at home within 31 days of being discharged from home health +
+
+ January 1, 2017 - December 31, 2018 +
+
+
+
+ How often patients' breathing improved +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients' wounds improved or healed after an operation +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team began their patients' care in a timely manner +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team checked patients for depression +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team checked patients' medications and got doctor's orders for medication issues in a timely manner +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team checked patients' risk of falling +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team made sure that their patients have received a flu shot for the current flu season +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team made sure that their patients have received a pneumococcal vaccine (pneumonia shot) +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+
+
+
+
+
+
+ +
+
+ + + Page + + + + 1 + + + + of + + + + 2 + + + + for + Home Health Care - Measure Date Range + + +
+
+ +
+
+
+
+`; diff --git a/src/components/dataset/DataTableHeader/DataHeaderButton.jsx b/src/components/dataset/DataTableHeader/DataHeaderButton.jsx new file mode 100644 index 00000000..cc41447b --- /dev/null +++ b/src/components/dataset/DataTableHeader/DataHeaderButton.jsx @@ -0,0 +1,47 @@ +import React from 'react' +import PropTypes from 'prop-types' +import FontAwesomePro from '../../common/FontAwesomePro/FontAwesomePro' + +const DataHeaderButton = ({ click, text, icon, fill = '#0c2499', secondary = undefined, active = false, ariaLabel }) => { + const buttonClass = active ? 'dataset-button active' : 'dataset-button' + return ( + ) +} + +DataHeaderButton.propTypes = { + /** + * Accessible button label + */ + ariaLabel: PropTypes.string, + /** + * `onClick` callback function + */ + click: PropTypes.func, + /** + * Button text + */ + text: PropTypes.string, + /** + * Button icon - prepended + */ + icon: PropTypes.string, + /** + * Icon color + */ + fill: PropTypes.string, + /** + * Button icon - appended + */ + secondary: PropTypes.string, + /** + * Button active state class + */ + active: PropTypes.bool +} + +DataHeaderButton.displayName = 'DataHeaderButton' +export default DataHeaderButton diff --git a/src/components/dataset/DataTableHeader/DataHeaderButton.test.jsx b/src/components/dataset/DataTableHeader/DataHeaderButton.test.jsx new file mode 100644 index 00000000..2ead4b9c --- /dev/null +++ b/src/components/dataset/DataTableHeader/DataHeaderButton.test.jsx @@ -0,0 +1,46 @@ +import { render } from '@testing-library/react' +import renderer from 'react-test-renderer' +import DataHeaderButton from './DataHeaderButton' + +describe('DataHeaderButton component.', () => { + it('Matches snapshot.', () => { + const renderedDataHeaderButton = renderer.create( + {}} + /> + ).toJSON() + + expect(renderedDataHeaderButton).toMatchSnapshot() + }) + + it('Renders with active class if \'active\' prop is true.', () => { + const { container } = render( + {}} + /> + ) + + expect(container.querySelector('.dataset-button')).toHaveClass('active') + }) + + it('Renders secondary icon if \'secondary\' prop is passed.', () => { + const { container } = render( + {}} + /> + ) + + expect(container.querySelector('svg[aria-label="chevron right"]')).toBeInTheDocument() + }) +}) diff --git a/src/components/dataset/DataTableHeader/DataIcon.jsx b/src/components/dataset/DataTableHeader/DataIcon.jsx new file mode 100644 index 00000000..6dc7eadb --- /dev/null +++ b/src/components/dataset/DataTableHeader/DataIcon.jsx @@ -0,0 +1,141 @@ +import React from 'react' +import PropTypes from 'prop-types' +// from civic actions +class DataIcon extends React.PureComponent { + render () { + const { + icon, height, width, color + } = this.props + + switch (icon) { + case 'density-1': + return ( + + + + ) + case 'density-2': + return ( + + + + ) + case 'density-3': + return ( + + + + ) + case 'group': + return ( + + + + + ) + case 'select': + return ( + + + + + + ) + case 'times': + return ( + + + + ) + } + } +} + +DataIcon.defaultProps = { + icon: 'density-1', + color: 'black', + width: 20, + height: 20 +} + +DataIcon.propTypes = { + /** + * Icon name + */ + icon: PropTypes.oneOf([ + 'density-1', + 'density-2', + 'density-3', + 'group', + 'select', + 'times' + ]), + /** + * Icon color + */ + color: PropTypes.string, + /** + * Icon pixel width + */ + width: PropTypes.number, + /** + * Icon pixel height + */ + height: PropTypes.number +} + +DataIcon.displayName = 'DataIcon' +export default DataIcon diff --git a/src/components/dataset/DataTableHeader/DataIcon.test.jsx b/src/components/dataset/DataTableHeader/DataIcon.test.jsx new file mode 100644 index 00000000..69461274 --- /dev/null +++ b/src/components/dataset/DataTableHeader/DataIcon.test.jsx @@ -0,0 +1,75 @@ +import { render } from '@testing-library/react' +import renderer from 'react-test-renderer' +import DataIcon from './DataIcon' + +const componentArgs = { + icon: 'density-1', + color: '#0C2499', + width: 50, + height: 50 +} + +describe('DataIcon component.', () => { + it('Matches snapshot.', () => { + const renderedDataIcon = renderer.create( + + ).toJSON() + + expect(renderedDataIcon).toMatchSnapshot() + }) + + it('Renders \'density-2\' icon.', () => { + const { container } = render( + + ) + + expect(container.querySelector('svg.density-2')).toBeInTheDocument() + }) + + it('Renders \'density-3\' icon.', () => { + const { container } = render( + + ) + + expect(container.querySelector('svg.density-3')).toBeInTheDocument() + }) + + it('Renders \'group\' icon.', () => { + const { container } = render( + + ) + + expect(container.querySelector('svg.group')).toBeInTheDocument() + }) + + it('Renders \'select\' icon.', () => { + const { container } = render( + + ) + + expect(container.querySelector('svg.select')).toBeInTheDocument() + }) + + it('Renders \'times\' icon.', () => { + const { container } = render( + + ) + + expect(container.querySelector('svg.times')).toBeInTheDocument() + }) +}) diff --git a/src/components/dataset/DataTableHeader/DataTableDensity.jsx b/src/components/dataset/DataTableHeader/DataTableDensity.jsx new file mode 100644 index 00000000..77e41f64 --- /dev/null +++ b/src/components/dataset/DataTableHeader/DataTableDensity.jsx @@ -0,0 +1,84 @@ +import React from 'react' +import PropTypes from 'prop-types' +import DataIcon from './DataIcon' + +const DataTableDensity = ({ + active, + items = [ + { icon: , text: 'expanded', value: 'density-1' }, + { icon: , text: 'normal', value: 'density-2' }, + { icon: , text: 'tight', value: 'density-3' } + ], + densityChange, + className = 'data-table-density', + screenReaderClass = 'sr-only sr-only-focusable', + title = 'Display Density' +}) => ( +
+ {title} +
+ {items.map((item) => { + let srClass = screenReaderClass + if (!item.icon) { + srClass = '' + } + return ( + + ) + })} +
+
+) + +DataTableDensity.propTypes = { + /** + * Button active state class. + * Text of active item. Ex. 'Normal' + */ + active: PropTypes.string, + /** + * Button click callback function + */ + densityChange: PropTypes.func.isRequired, + /** + * Custom class(es) used for accessibility purposes. + * Applied to button text span + */ + screenReaderClass: PropTypes.string, + /** + * Custom class(es) applied to wrapping element + */ + className: PropTypes.string, + /** + * Array of button data + */ + items: PropTypes.arrayOf(PropTypes.shape({ + icon: PropTypes.node, + text: PropTypes.string, + value: PropTypes.string + })), + /** + * Button group title text + */ + title: PropTypes.string +} + +DataTableDensity.displayName = 'DataTableDensity' +export default DataTableDensity diff --git a/src/components/dataset/DataTableHeader/DataTableDensity.test.jsx b/src/components/dataset/DataTableHeader/DataTableDensity.test.jsx new file mode 100644 index 00000000..dae207ee --- /dev/null +++ b/src/components/dataset/DataTableHeader/DataTableDensity.test.jsx @@ -0,0 +1,93 @@ +import { render, fireEvent } from '@testing-library/react' +import renderer from 'react-test-renderer' +import DataTableDensity from './DataTableDensity' +import FontAwesomePro from '../../common/FontAwesomePro/FontAwesomePro' + +const componentArgs = { + title: 'Display Density', + items: [ + { + icon: ( + + ), + text: 'Expanded', + value: 'density-1' + }, + { + icon: ( + + ), + text: 'Normal', + value: 'density-2' + }, + { + icon: ( + + ), + text: 'Compact', + value: 'density-3' + } + ], + densityChange: vi.fn(), + screenReaderClass: 'sr-only sr-only-focusable', + className: 'data-table-density', + active: 'Normal' +} + +describe('DataTableDensity component.', () => { + it('Matches snapshot.', () => { + const renderedDataTableDensity = renderer.create( + + ).toJSON() + + expect(renderedDataTableDensity).toMatchSnapshot() + }) + + it('\'densityChange\' prop is called when buttons are clicked.', () => { + const densityChange = vi.fn() + + const { container } = render( + + ) + + componentArgs.items.forEach((button) => { + fireEvent.click(container.querySelector(`button[aria-label*="${componentArgs.title}, ${button.text}"]`)) + expect(densityChange).toHaveBeenCalled() + + fireEvent.mouseUp(container.querySelector(`button[aria-label*="${componentArgs.title}, ${button.text}"]`)) + expect(densityChange).toHaveBeenCalled() + + fireEvent.touchEnd(container.querySelector(`button[aria-label*="${componentArgs.title}, ${button.text}"]`)) + expect(densityChange).toHaveBeenCalled() + }) + }) + + it('Screen reader class is not applied when buttons aren\'t provided with icons.', () => { + const { container } = render( + + ) + + componentArgs.items.forEach((button) => { + expect(container.querySelector(`button[aria-label*="${componentArgs.title}, ${button.text}"] > span`)).not.toHaveClass(componentArgs.screenReaderClass) + }) + }) +}) diff --git a/src/components/dataset/DataTableHeader/DataTableHeader.jsx b/src/components/dataset/DataTableHeader/DataTableHeader.jsx new file mode 100644 index 00000000..11687b71 --- /dev/null +++ b/src/components/dataset/DataTableHeader/DataTableHeader.jsx @@ -0,0 +1,316 @@ +import React, { useContext, useState, useEffect, useRef } from 'react' +import PropTypes from 'prop-types' +import FullScreenResource from '../FullScreenResource/FullScreenResource' +import ManageColumns from '../ManageColumns/ManageColumns' +import DataHeaderButton from './DataHeaderButton' +import { FilteredDispatch } from '../DatasetResource/FilteredDatasetContext' +import FilterDataset from '../FilterDataset/FilterDataset' +import FilterChipList from '../FilterDataset/FilterChipList' +import PopoverContent from './PopOverContent' +import DataTablePageResults from '../DataTablePageResults/DataTablePageResults' +import { Popover } from 'react-tiny-popover' +import './DataTableHeader.scss' +import Tooltip from '../../common/Tooltip/Tooltip' +import log from '../../../log' +import { useCurrentBreakpointName, useCurrentWidth } from 'react-socks' +import DatasetExplorerDownloadLink from '../DatasetExplorerDownloadLink/DatasetExplorerDownloadLink' + +// modify to check hostname instead. +const filterEnabled = true +log.debug('filterEnabled', filterEnabled) + +const DataTableHeader = ({ + fullscreen, + datasetTitle, + datasetDescription, + datasetModified, + datasetReleased, + datasetRefresh, + instanceId +}) => { + const { + filteredResource, + filteredTable, + initOrder, + curOrder, + setCurOrder, + resetVisibility, + activeDensity, + filters, + setFilters, + filtersApplied, + setFiltersApplied + } = useContext(FilteredDispatch) + const { limit, count, offset } = filteredResource + const updateRows = (r) => { + filteredResource.setLimit(r) + } + + const currentBreakpoint = useCurrentBreakpointName() + const currentWidth = useCurrentWidth() + const [showTooltip, setShowTooltip] = useState(false) + const displaySettingsContainer = useRef(null) + + useEffect(() => { + if (!fullscreen && currentWidth < 1200) { + setShowTooltip(true) + } else if (fullscreen && currentWidth < 800) { + setShowTooltip(true) + } else { + setShowTooltip(false) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentBreakpoint]) + + // have columns been reordered from initial order? + const checkOrder = (arr1, arr2) => { + if (!arr1 || !arr2) { + return false + } + for (let i = 0; i < arr1.length; i++) { + if (arr1[i].id !== arr2[i]) { + return true + } + } + return false + } + const reordered = checkOrder(initOrder, curOrder) + const resetColumnOrder = () => { + const order = initOrder.map((item) => item.id) + setCurOrder(order) + filteredTable.setColumnOrder(order) + } + + // have columns been hidden? + const hidden = filteredTable?.visibleColumns?.length !== initOrder?.length + + const [popoverOpen, setPopoverOpen] = useState(false) + const togglePopover = () => { + setPopoverOpen(!popoverOpen) + } + + // Dataset filter + const emptyFilter = { column: '', condition: '', value: '' } + const [filterOpen, setFilterOpen] = useState(false) + const applyConditions = (conditions) => { + if (conditions.length === 0) { + filteredResource.setConditions([]) + return + } + const mapped = conditions.map((f) => { + if (f.condition === 'LIKE') { + // edge case for contains, we want to add some sql + f.value = `%${f.value}%` + } + return { property: f.column.toLowerCase(), operator: f.condition, value: f.value } + }) + filteredResource.setConditions(mapped) + } + const applyFilter = (filters) => { + setFiltersApplied(filters) + applyConditions(filters) + } + + const addFilter = () => { + setFilters((prevFilters) => { + const copied = prevFilters.slice() + return copied.concat([emptyFilter]) + }) + } + + // this should not reset visibility. + const resetFilters = () => { + setFiltersApplied([]) + setFilters([emptyFilter]) + filteredResource.setConditions([]) + } + + // for filter modal + const deleteFilters = (i) => { + if (filters.length === 0) { + resetFilters() + return + } + setFilters((prevFilters) => { + const copied = prevFilters.slice() + copied.splice(i, 1) + return copied + }) + } + + // delete applied filter, and immediately reflect changes to local filters (filterChips) + const deleteFilter = (i) => { + // no deleting last filter + if (filtersApplied.length === 1) { + resetFilters() + return + } + // we need local filters to reflect this state. + setFiltersApplied((prevFilters) => { + const copied = prevFilters.slice() + copied.splice(i, 1) + setFilters(copied) + applyConditions(copied) + return copied + }) + } + + const updateFilters = (index, type, value) => { + setFilters((prevFilters) => { + const copied = prevFilters.slice() + copied[index][type] = value + return copied + }) + } + + const filterText = filtersApplied.length > 0 ? `Edit filters (${filtersApplied.length})` : 'Filter dataset' + const filterIcon = filtersApplied.length > 0 ? 'filter-solid' : 'filter' + const pageIndex = offset / Number(limit) + const total = count || 0 + + return ( +
+
+
+ + {/* ChoiceList now only takes types for radio or checkboxes. ChoiceList has been converted to an html select component */} +
+ + {/* Filter dataset */} + {filterEnabled && ( +
+ + setFilterOpen(true)} /> + + {filterOpen && ( + { + setFilterOpen(false) + if (!fullscreen) { + document.querySelector('body').classList.remove('ds--dialog-open') + } + }} + addFilter={addFilter} + deleteFilter={deleteFilters} + updateFilters={updateFilters} + resetFilters={resetFilters} + applyFilter={applyFilter} + lFilters={filters} + appliedLength={filtersApplied.length} + /> + )} +
+ )} + + {/* Manage columns */} + + + {/* Display settings */} +
{ + // eslint-disable-next-line no-undef + requestAnimationFrame(() => { + if (!displaySettingsContainer.current.contains(document.activeElement)) { + setPopoverOpen(false) + } + }) + }} + > + + )} + parentElement={document.getElementsByClassName(`display-settings-${instanceId}`)[0]} + > + {/* wrapper div required */} +
+ + + +
+
+
+ + {/* Fullscreen */} + {!fullscreen && ( + + )} +
+
+
+
+ ) +} + +DataTableHeader.propTypes = { + /** + * `true` removes the 'Fullscreen' button. + * Used when in fullscreen mode + */ + fullscreen: PropTypes.bool, + /** + * Dataset title + */ + datasetTitle: PropTypes.string, + /** + * Dataset description + */ + datasetDescription: PropTypes.string, + /** + * Dataset modified date string + */ + datasetModified: PropTypes.string, + /** + * Dataset released date string + */ + datasetReleased: PropTypes.string, + /** + * Dataset anticipated refresh date string + */ + datasetRefresh: PropTypes.string, + /** + * Unique instance ID. + * Used to apply a unique classname to display settings wrapper element. + * Needed for properly scoped logic + */ + instanceId: PropTypes.number +} +DataTableHeader.displayName = 'DataTableHeader' +export default DataTableHeader diff --git a/src/components/dataset/DataTableHeader/DataTableHeader.scss b/src/components/dataset/DataTableHeader/DataTableHeader.scss new file mode 100644 index 00000000..e1f7d995 --- /dev/null +++ b/src/components/dataset/DataTableHeader/DataTableHeader.scss @@ -0,0 +1,348 @@ +@use "../../../scss/modules/modules"; +@use "../../../scss/modules/colormap"; +@use "../../../scss/modules/variables"; + +.null { + display: none; +} + +.resource-table-header { + display: flex; + width: 100%; + font: 400 1.4rem variables.$muli; + position: relative; + + // General button style + button.dataset-button { + display: flex; + align-items: center; + justify-content: center; + background-color: white; + height: 40px; + border: solid 1px transparent; + z-index: 3; + border-radius: 2px; + margin-left: 12px; + padding: 0 12px 0 8px; + + &:hover { + text-decoration: underline solid colormap.$blueberry 1px; + text-underline-offset: 3px; + background-color: #f6f9fd; + } + + &.active { + background-color: #f6f9fd; + } + + .dataset-button-text { + margin-left: 8px; + color: colormap.$dataset-border; + } + } + + .resource-table-header-container { + display: flex; + flex-wrap: wrap; + flex-grow: 1; + flex-shrink: 0; + justify-content: space-between; + white-space: normal; + min-height: 56px; + background-color: white; + //margin-bottom: 1rem; + padding: 0 16px 0 24px; + border-radius: 2px; + border: 1px solid variables.$panel-border; + box-shadow: variables.$panel-shadow; + + .data-table-results { + display: flex; + align-items: center; + margin-right: 24px; + + .low-result, + .high-result, + .total { + font-weight: 700; + } + + .high-result { + margin-right: 2px; + } + + .total { + margin-left: 2px; + } + + p { + margin-top: 0; + margin-bottom: 0; + } + } + + .table-controls { + display: flex; + align-items: center; + justify-content: space-between; + + // Display settings + .display-settings { + .dataset-button { + padding: 0 8px; + + .fa[class*="caret"] { + margin-left: 6px; + } + } + } + + // Manage columns + .manage-columns-button-container { + .data-table-adv-options { + border-radius: 20px; + padding: 0 20px; + + span { + padding-bottom: 2px; + } + } + } + + // Fullscreen + .fullscreen-button-container { + .fullscreen-button { + width: 40px; + border-radius: 100%; + padding: 0; + } + } + } + } +} + +.resize-help { + font-size: .9em; + margin: 1em 0; +} + +// Popover +#react-tiny-popover-container, +.react-tiny-popover-container { + z-index: 9999; + border: solid 1px colormap.$charcoal-10; + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.12); + border-radius: 2px; + padding: 0 16px; + background-color: white; + min-width: 272px; + + .popover-container { + background-color: #fff; + .data-table-density { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + .density-buttons-title { + font-size: 14px; + font-family: 'Muli'; + } + .density-buttons { + button { + border: solid 1px colormap.$blueberry-20; + background-color: #fff; + padding: 7px; + } + .active { + border: solid 1px colormap.$blueberry; + background-color: colormap.$blueberry-5; + svg { + path { + fill: colormap.$blueberry-80; + } + } + } + } + } + } + .popover-row { + display: flex; + align-items: center; + padding: 12px 0; + font-size: 14px; + white-space: nowrap; + > * { + width: 50%; + } + + // Row height + &.data-table-density { + .density-buttons { + display: flex; + button { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + background-color: white; + min-width: 40px; + height: 40px; + margin-right: -1px; + border: 0; + padding: 0; + box-shadow: 0 0 0 1px colormap.$border-color inset; + + &:first-child { + border-radius: 2px 0 0 2px; + } + + &:last-child { + border-radius: 0 2px 2px 0; + } + + &:hover { + z-index: 2; + background-color: #f6f9fd; + } + + &:focus { + z-index: 3; + + &:hover { + z-index: 2; + } + } + + svg path { + fill: colormap.$charcoal-70; + } + + &.active { + background-color: #f6f9fd; + box-shadow: 0 0 0 1px colormap.$blueberry inset; + z-index: 4; + + svg path { + fill: colormap.$blueberry; + } + } + } + } + } + + // Rows per page + &.rows-per-page { + select { + margin: 0; + height: 40px; + } + } + + &:not(:last-child) { + border-bottom: 1px solid colormap.$charcoal-10; + } + } +} + +@mixin condense-controls() { + .resource-table-header { + .resource-table-header-container { + min-height: 64px; + padding: 0 12px 0 24px; + + .table-control-item.display-settings { + button.dataset-button { + // Optical correctness + padding: 0 6px 0 10px; + } + } + + button.dataset-button { + width: 64px; + height: 48px; + + svg:first-of-type { + width: 18px; + height: 18px; + } + + .dataset-button-text { + display: none; + } + } + } + } +} + +@media screen and (max-width: 1200px) { + .dataset-content { + @include condense-controls(); + } +} + +@media screen and (max-width: 800px) { + .fullscreen-content { + @include condense-controls(); + } +} + +@media screen and (max-width: 767px) { + + .dataset-content, + .fullscreen-content { + .resource-table-header { + .resource-table-header-container { + flex-direction: column; + padding: 0; + + .data-table-results { + margin: 0; + padding: 24px; + } + + .table-controls { + padding: 8px; + flex: 1; + border-top: 1px solid variables.$panel-border; + justify-content: flex-start; + + .table-control-item { + display: flex; + flex: 1; + + .tooltip-trigger { + display: flex; + flex: 1; + + button.dataset-button { + width: initial; + padding: 8px; + flex: 1; + } + } + } + + // When tooltips are shown + .tooltip-trigger { + flex: 1; + } + } + + .display-settings { + button.dataset-button { + width: 64px; + } + } + + button.dataset-button { + padding: 8px; + width: 48px; + height: 48px; + flex: 1; + margin: 0 8px; + } + } + } + } +} diff --git a/src/components/dataset/DataTableHeader/DataTableHeader.test.jsx b/src/components/dataset/DataTableHeader/DataTableHeader.test.jsx new file mode 100644 index 00000000..3c758a0a --- /dev/null +++ b/src/components/dataset/DataTableHeader/DataTableHeader.test.jsx @@ -0,0 +1,587 @@ +import { render, screen, fireEvent, act } from '@testing-library/react' +import DataTableHeader from './DataTableHeader' +import FilteredDatasetResource from '../DatasetResource/FilteredDatasetResource' +import { filteredDatasetResource } from '../../../utilities/data-mocks/data-filteredDatasetResource' +import { DatasetContext } from '../../../context/DatasetContext' +import axios from 'axios' + +// Mock axios +vi.mock('axios') + +const mockDatasetResponse = require('../../../utilities/data-mocks/api-response-dataset.json') + +// This suppresses console warnings for components +// DataTableHeader logs for debugging purposes +vi.mock('../../../log', async () => { + const actual = await vi.importActual('../../../log') + + return { + ...actual, + debug: () => {} + } +}) + +// Mock window.scrollTo +global.scrollTo = vi.fn() + +const mockApiResponse = { + 'results': [ + { + measure_name: 'Average Medicare spending associated with an agency\u0027s home health episodes compared with all home health episodes nationally - agency score', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'Average Medicare spending associated with an agency\u0027s home health episodes compared with all home health episodes nationally - count', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'For patients with diabetes, how often the home health team got doctor\u0027s orders, gave foot care, and taught patients about foot care', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often home health patients had new or worsened pressure ulcers', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often home health patients had to be admitted to the hospital', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often home health patients, who have had a recent hospital stay, had a preventable hospital readmission within 30 days of discharge from home health', + measure_date_range: 'January 1, 2016 - December 31, 2018' + }, + { + measure_name: 'How often patients got better at bathing', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients got better at getting in and out of bed', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients got better at taking their drugs correctly by mouth', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients got better at walking or moving around', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients receiving home health care needed any urgent, unplanned care in the hospital emergency room - without being admitted to the hospital', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients remained at home within 31 days of being discharged from home health', + measure_date_range: 'January 1, 2017 - December 31, 2018' + }, + { + measure_name: 'How often patients\u0027 breathing improved', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients\u0027 wounds improved or healed after an operation', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team began their patients\u0027 care in a timely manner', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team checked patients for depression', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team checked patients\u0027 medications and got doctor\u0027s orders for medication issues in a timely manner', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team checked patients\u0027 risk of falling', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team made sure that their patients have received a flu shot for the current flu season', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team made sure that their patients have received a pneumococcal vaccine (pneumonia shot)', + measure_date_range: 'January 1, 2018 - December 31, 2018' + } + ], + count: 21, + schema: { + 'a4eb46a4-fd19-55b0-baf4-de08c3a70abd': { + fields: { + measure_name: { + type: 'text', + mysql_type: 'text', + description: 'Measure Name' + }, + measure_date_range: { + type: 'text', + mysql_type: 'text', + description: 'Measure Date Range' + } + } + } + }, + query: { + keys: true, + limit: 20, + offset: 0, + resources: [ + { + id: 'a4eb46a4-fd19-55b0-baf4-de08c3a70abd', + alias: 't' + } + ], + count: true, + results: true, + schema: true, + format: 'json', + rowIds: false, + properties: [ + 'measure_name', + 'measure_date_range' + ] + } +} + +const datasetContextProviderValue = { + data: mockDatasetResponse, + error: null, + isLoading: true, + setDatasetState: vi.fn(), + resetDatasetState: vi.fn() +} + +const componentArgs = { + fullscreen: false, + datasetTitle: 'Supplier Directory Data', + datasetDescription: 'A list of Suppliers that indicates the supplies carried at that location and the supplier\'s Medicare participation status.', + datasetModified: '2020-05-10', + datasetReleased: '2020-05-10', + instanceId: 1 +} + +const renderComponent = async (args = componentArgs) => render ( + + + + + +) + +describe('DataHeaderButton component.', () => { + beforeEach(() => { + // Mock useDatastore hook + axios.mockResolvedValueOnce({ + data: { + ...mockApiResponse + } + }) + }) + + it('Matches snapshot.', async () => { + let renderedDataTableHeader + + await act(async () => { + renderedDataTableHeader = await renderComponent() + }) + + expect(renderedDataTableHeader.asFragment()).toMatchSnapshot() + }) + + it('User can set a filter and remove a filter for a dataset.', async () => { + axios + .mockResolvedValueOnce({ + data: { + ...mockApiResponse, + results: [ + { + measure_name: 'Average Medicare spending associated with an agency\u0027s home health episodes compared with all home health episodes nationally - agency score', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'Average Medicare spending associated with an agency\u0027s home health episodes compared with all home health episodes nationally - count', + measure_date_range: 'January 1, 2018 - December 31, 2018' + } + ], + count: 2, + query: { + keys: true, + limit: 20, + offset: 0, + conditions: [ + { + property: 'measure_name', + operator: 'LIKE', + value: '%%Average%%' + }, + { + property: 'measure_date_range', + operator: 'LIKE', + value: '%January 1, 2018 - December 31, 2018%' + } + ], + resources: [ + { + id: 'a4eb46a4-fd19-55b0-baf4-de08c3a70abd', + alias: 't' + } + ], + count: true, + results: true, + schema: true, + format: 'json', + rowIds: false, + properties: [ + 'measure_name', + 'measure_date_range' + ] + } + } + }) + .mockResolvedValueOnce({ + data: { + ...mockApiResponse, + results: [ + { + measure_name: 'Average Medicare spending associated with an agency\u0027s home health episodes compared with all home health episodes nationally - agency score', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'Average Medicare spending associated with an agency\u0027s home health episodes compared with all home health episodes nationally - count', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'For patients with diabetes, how often the home health team got doctor\u0027s orders, gave foot care, and taught patients about foot care', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often home health patients had new or worsened pressure ulcers', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often home health patients had to be admitted to the hospital', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients got better at bathing', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients got better at getting in and out of bed', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients got better at taking their drugs correctly by mouth', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients got better at walking or moving around', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients receiving home health care needed any urgent, unplanned care in the hospital emergency room - without being admitted to the hospital', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients\u0027 breathing improved', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often patients\u0027 wounds improved or healed after an operation', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team began their patients\u0027 care in a timely manner', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team checked patients for depression', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team checked patients\u0027 medications and got doctor\u0027s orders for medication issues in a timely manner', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team checked patients\u0027 risk of falling', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team made sure that their patients have received a flu shot for the current flu season', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team made sure that their patients have received a pneumococcal vaccine (pneumonia shot)', + measure_date_range: 'January 1, 2018 - December 31, 2018' + }, + { + measure_name: 'How often the home health team taught patients (or their family caregivers) about their drugs', + measure_date_range: 'January 1, 2018 - December 31, 2018' + } + ], + count: 19, + query: { + keys: true, + limit: 20, + offset: 0, + conditions: [ + { + property: 'measure_date_range', + operator: 'LIKE', + value: '%January 1, 2018 - December 31, 2018%' + } + ], + resources: [ + { + id: 'a4eb46a4-fd19-55b0-baf4-de08c3a70abd', + alias: 't' + } + ], + count: true, + results: true, + schema: true, + format: 'json', + rowIds: false, + properties: [ + 'measure_name', + 'measure_date_range' + ] + } + } + }) + .mockResolvedValueOnce({ + data: { + ...mockApiResponse + } + }) + + let element + + await act(async () => { + element = await renderComponent() + }) + + // Click 'Filter dataset' button to open dialog + fireEvent.click(screen.getByRole('button', { name: 'Filter dataset - Opens in a dialog' })) + expect(element.container.querySelector('.filter-dialog')).toBeInTheDocument() + + // Choose a column name for first filter + fireEvent.change(screen.getAllByLabelText('Select column')[0], { target: { value: 'measure_name' } }) + + // Choose a condition for first filter + fireEvent.change(screen.getAllByLabelText('Select condition')[0], { target: { value: 'LIKE' } }) + + // Type a value for first filter + fireEvent.change(screen.getAllByLabelText('Enter value')[0], { target: { value: 'Average' } }) + + // Add another filter + fireEvent.click(screen.getByRole('button', { name: 'Add filter' })) + + // Choose a column name for second filter + fireEvent.change(screen.getAllByLabelText('Select column')[1], { target: { value: 'measure_date_range' } }) + + // Choose a condition for second filter + fireEvent.change(screen.getAllByLabelText('Select condition')[1], { target: { value: 'LIKE' } }) + + // Type a value for second filter + fireEvent.change(screen.getAllByLabelText('Enter value')[1], { target: { value: 'January 1, 2018 - December 31, 2018' } }) + + // Click 'Appy 2 Filters' button + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: 'Apply 2 Filters' })) + }) + expect(element.container.querySelector('.data-table-results > p')).toHaveTextContent('Viewing 1 - 2 of 2 rows') + expect(screen.queryByRole('button', { name: 'Remove measure_name contains Average filter' })).toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Remove measure_date_range contains January 1, 2018 - December 31, 2018 filter' })).toBeInTheDocument() + + // Remove the measure_name filter + await act(async () => { + fireEvent.click(screen.queryByRole('button', { name: 'Remove measure_name contains Average filter' })) + }) + expect(element.container.querySelector('.data-table-results > p')).toHaveTextContent('Viewing 1 - 19 of 19 rows') + expect(screen.queryByRole('button', { name: 'Remove measure_name contains Average filter' })).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Remove measure_date_range contains January 1, 2018 - December 31, 2018 filter' })).toBeInTheDocument() + + // Remove the measure_date_range filter + await act(async () => { + fireEvent.click(screen.queryByRole('button', { name: 'Remove measure_date_range contains January 1, 2018 - December 31, 2018 filter' })) + }) + expect(element.container.querySelector('.data-table-results > p')).toHaveTextContent('Viewing 1 - 20 of 21 rows') + expect(screen.queryByRole('button', { name: 'Remove measure_date_range contains January 1, 2018 - December 31, 2018 filter' })).not.toBeInTheDocument() + + // Click 'Filter dataset' button to open dialog + fireEvent.click(screen.getByRole('button', { name: 'Filter dataset - Opens in a dialog' })) + expect(element.container.querySelector('.filter-dialog')).toBeInTheDocument() + + // Choose a column name for first filter + fireEvent.change(screen.getAllByLabelText('Select column')[0], { target: { value: 'measure_name' } }) + + // Choose a condition for first filter + fireEvent.change(screen.getAllByLabelText('Select condition')[0], { target: { value: 'LIKE' } }) + + // Type a value for first filter + fireEvent.change(screen.getAllByLabelText('Enter value')[0], { target: { value: 'Average' } }) + + // Delete the filter from the dialog + fireEvent.click(screen.getByRole('button', { name: 'delete filter' })) + + // Close the filter dialog + fireEvent.click(screen.getByRole('button', { name: 'Close filter dataset dialog' })) + + expect(element.container.querySelector('.data-table-results > p')).toHaveTextContent('Viewing 1 - 20 of 21 rows') + expect(element.container.querySelector('.chip-list')).toBeEmptyDOMElement() + }) + + it('User can reorder the columns for a dataset.', async () => { + axios.mockResolvedValueOnce({ + data: { + ...mockApiResponse + } + }) + + let element + + await act(async () => { + element = await renderComponent() + }) + + // Click 'Manage columns' button to open dialog + fireEvent.click(screen.getByRole('button', { name: 'Manage columns - Opens in a dialog' })) + expect(element.container.querySelector('.manage-col-dialog')).toBeInTheDocument() + + // Enter key to grab the move handle + fireEvent.keyDown(screen.getAllByLabelText('Reorder Measure Name column')[0], { + key: 'Enter', + code: 'Enter', + keyCode: 13, + charCode: 13 + }) + expect(element.container.querySelector('.aria-live-feedback')).toHaveTextContent('Measure Name grabbed') + + // ArrowDown key to change the column order + fireEvent.keyDown(screen.getAllByLabelText('Reorder Measure Name column')[0], { + key: 'ArrowDown', + code: 'ArrowDown', + keyCode: 40, + charCode: 40 + }) + expect(element.container.querySelector('.aria-live-feedback')).toHaveTextContent('The rankings have been updated, Measure Name is now ranked 2 of 2') + + // Enter key to drop the move handle + fireEvent.keyDown(screen.getAllByLabelText('Reorder Measure Name column')[0], { + key: 'Enter', + code: 'Enter', + keyCode: 13, + charCode: 13 + }) + expect(element.container.querySelector('.aria-live-feedback')).toHaveTextContent('Measure Name dropped') + + // Update the column order + fireEvent.click(screen.getByRole('button', { name: 'Update columns' })) + expect(screen.queryByRole('button', { name: 'Columns reordered' })).toBeInTheDocument() + + // Clear column reorder + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: 'Clear all filters' })) + }) + expect(screen.queryByRole('button', { name: 'Columns reordered' })).not.toBeInTheDocument() + }) + + it('User can change the row height of the data table.', async () => { + let element + + await act(async () => { + element = await renderComponent() + }) + + // Mock document.activeElement + Object.defineProperty(document, 'activeElement', { + value: screen.getByRole('button', { name: 'Manage columns - Opens in a dialog' }), + writable: true + }) + + // Open the 'Display settings' dropdown + fireEvent.click(screen.getByRole('button', { name: 'Display settings' })) + expect(element.container.querySelector('.popover-row.data-table-density')).toBeInTheDocument() + + // Click 'Expanded' option + fireEvent.click(screen.getByRole('button', { name: 'Row height, Expanded' })) + expect(screen.queryByRole('button', { name: 'Row height, Expanded - selected' })).toHaveClass('active') + + // Click 'Normal' option + fireEvent.click(screen.getByRole('button', { name: 'Row height, Normal' })) + expect(screen.queryByRole('button', { name: 'Row height, Normal - selected' })).toHaveClass('active') + + // Click 'Compact' option + fireEvent.click(screen.getByRole('button', { name: 'Row height, Compact' })) + expect(screen.queryByRole('button', { name: 'Row height, Compact - selected' })).toHaveClass('active') + + // Close 'Display settings' dropdown + await act(async () => { + fireEvent.blur(screen.getByRole('button', { name: 'Display settings' })) + }) + expect(element.container.querySelector('.popover-row.data-table-density')).not.toBeInTheDocument() + }) + + it('User can change the rows per page of the data table.', async () => { + axios.mockResolvedValueOnce({ + data: { + ...mockApiResponse, + results: [ + ...mockApiResponse.results, + { + measure_name: 'How often the home health team taught patients (or their family caregivers) about their drugs', + measure_date_range: 'January 1, 2018 - December 31, 2018' + } + ], + query: { + ...mockApiResponse.query, + limit: 50 + } + } + }) + + let element + + await act(async () => { + element = await renderComponent() + }) + + // Open the 'Display settings' dropdown + fireEvent.click(screen.getByRole('button', { name: 'Display settings' })) + expect(element.container.querySelector('.popover-row.data-table-density')).toBeInTheDocument() + + // Select rows per page value + await act(async () => { + fireEvent.change(screen.getByLabelText('Rows per page'), { target: { value: '50' } }) + }) + + expect(element.container.querySelector('.data-table-results > p')).toHaveTextContent('Viewing 1 - 21 of 21 rows') + }) + + it('User can open dataset explorer in fullscreen mode.', async () => { + axios.mockResolvedValueOnce({ + data: { + ...mockApiResponse + } + }) + + let element + + await act(async () => { + element = await renderComponent() + }) + + // Open the 'Fullscreen' dialog + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: 'Fullscreen - Opens in a dialog' })) + }) + expect(element.container.querySelector('.fullscreen-resource')).toBeInTheDocument() + expect(element.container.querySelector('.fullscreen-resource .DataTable')).toBeInTheDocument() + }) +}) diff --git a/src/components/dataset/DataTableHeader/PopOverContent.jsx b/src/components/dataset/DataTableHeader/PopOverContent.jsx new file mode 100644 index 00000000..49e3d953 --- /dev/null +++ b/src/components/dataset/DataTableHeader/PopOverContent.jsx @@ -0,0 +1,101 @@ +import React, { useContext } from 'react' +import PropTypes from 'prop-types' +import { FilteredDispatch } from '../DatasetResource/FilteredDatasetContext' +import DataTableDensity from './DataTableDensity' +import find from 'lodash/find' +import FontAwesomePro from '../../common/FontAwesomePro/FontAwesomePro' + +const PopOverContent = ({ + datasetTitle, + updateRows, + limit +}) => { + const { + activeDensity, + setActiveDensity + } = useContext(FilteredDispatch) + const pageSizeOptions = [ + { defaultChecked: true, label: '20', value: '20' }, + { label: '50', value: '50' }, + { label: '100', value: '100' } + ] + const densityItems = [ + { + icon: ( + + ), + text: 'Expanded', + value: 'density-1' + }, + { + icon: ( + + ), + text: 'Normal', + value: 'density-2' + }, + { + icon: ( + + ), + text: 'Compact', + value: 'density-3' + } + ] + return ( +
+ { + const densityText = find(densityItems, { value: density }).text + setActiveDensity(densityText) + }} + items={densityItems.slice()} + active={activeDensity} + /> +
+ + Rows per page + + +
+
+ ) +} + +PopOverContent.propTypes = { + /** + * Dataset title + */ + datasetTitle: PropTypes.string, + /** + * 'Rows per page' select callback function. + * Returns the selected value as an argument + */ + updateRows: PropTypes.func, + /** + * Default 'Rows per page' selected value + */ + limit: PropTypes.oneOf([20, 50, 100]) +} +PopOverContent.displayName = 'PopOverContent' +export default PopOverContent diff --git a/src/components/dataset/DataTableHeader/PopOverContent.test.jsx b/src/components/dataset/DataTableHeader/PopOverContent.test.jsx new file mode 100644 index 00000000..05b04d42 --- /dev/null +++ b/src/components/dataset/DataTableHeader/PopOverContent.test.jsx @@ -0,0 +1,65 @@ +import { render, screen, fireEvent } from '@testing-library/react' +import renderer from 'react-test-renderer' +import PopOverContent from './PopOverContent' +import { FilteredDispatch } from '../DatasetResource/FilteredDatasetContext' + +const updateRowsProp = vi.fn() +const setActiveDensityFn = vi.fn() + +const componentArgs = { + datasetTitle: 'Home Health Care - Measure Date Range', + limit: 20, + updateRows: updateRowsProp +} + +const renderComponent = (args = componentArgs) => render ( + + + +) + +describe('PopOverContent component.', () => { + it('Matches snapshot.', async () => { + const renderedPopOverContent = renderer.create( + + + + ).toJSON() + + expect(renderedPopOverContent).toMatchSnapshot() + }) + + it('Row density can be changed.', () => { + renderComponent() + + // Click 'Expanded' option + fireEvent.click(screen.getByRole('button', { name: 'Row height, Expanded' })) + expect(setActiveDensityFn).toHaveBeenCalled() + + // Click 'Normal' option + fireEvent.click(screen.getByRole('button', { name: 'Row height, Normal - selected' })) + expect(setActiveDensityFn).toHaveBeenCalled() + + // Click 'Compact' option + fireEvent.click(screen.getByRole('button', { name: 'Row height, Compact' })) + expect(setActiveDensityFn).toHaveBeenCalled() + }) + + it('Rows per page can be changed.', async () => { + renderComponent() + + fireEvent.change(screen.getByLabelText('Rows per page'), { target: { value: '50' } }) + + expect(updateRowsProp).toHaveBeenCalled() + }) +}) diff --git a/src/components/dataset/DataTableHeader/__snapshots__/DataHeaderButton.test.js.snap b/src/components/dataset/DataTableHeader/__snapshots__/DataHeaderButton.test.js.snap new file mode 100644 index 00000000..2c550ddd --- /dev/null +++ b/src/components/dataset/DataTableHeader/__snapshots__/DataHeaderButton.test.js.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DataHeaderButton component. Matches snapshot. 1`] = ` + +`; diff --git a/src/components/dataset/DataTableHeader/__snapshots__/DataHeaderButton.test.jsx.snap b/src/components/dataset/DataTableHeader/__snapshots__/DataHeaderButton.test.jsx.snap new file mode 100644 index 00000000..397925f0 --- /dev/null +++ b/src/components/dataset/DataTableHeader/__snapshots__/DataHeaderButton.test.jsx.snap @@ -0,0 +1,29 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`DataHeaderButton component. > Matches snapshot. 1`] = ` + +`; diff --git a/src/components/dataset/DataTableHeader/__snapshots__/DataIcon.test.js.snap b/src/components/dataset/DataTableHeader/__snapshots__/DataIcon.test.js.snap new file mode 100644 index 00000000..0e7b0d93 --- /dev/null +++ b/src/components/dataset/DataTableHeader/__snapshots__/DataIcon.test.js.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DataIcon component. Matches snapshot. 1`] = ` + + + +`; diff --git a/src/components/dataset/DataTableHeader/__snapshots__/DataIcon.test.jsx.snap b/src/components/dataset/DataTableHeader/__snapshots__/DataIcon.test.jsx.snap new file mode 100644 index 00000000..a50aa0a5 --- /dev/null +++ b/src/components/dataset/DataTableHeader/__snapshots__/DataIcon.test.jsx.snap @@ -0,0 +1,15 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`DataIcon component. > Matches snapshot. 1`] = ` + + + +`; diff --git a/src/components/dataset/DataTableHeader/__snapshots__/DataTableDensity.test.js.snap b/src/components/dataset/DataTableHeader/__snapshots__/DataTableDensity.test.js.snap new file mode 100644 index 00000000..9a9d96a3 --- /dev/null +++ b/src/components/dataset/DataTableHeader/__snapshots__/DataTableDensity.test.js.snap @@ -0,0 +1,107 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DataTableDensity component. Matches snapshot. 1`] = ` +
+ + Display Density + +
+ + + +
+
+`; diff --git a/src/components/dataset/DataTableHeader/__snapshots__/DataTableDensity.test.jsx.snap b/src/components/dataset/DataTableHeader/__snapshots__/DataTableDensity.test.jsx.snap new file mode 100644 index 00000000..5ce0c359 --- /dev/null +++ b/src/components/dataset/DataTableHeader/__snapshots__/DataTableDensity.test.jsx.snap @@ -0,0 +1,107 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`DataTableDensity component. > Matches snapshot. 1`] = ` +
+ + Display Density + +
+ + + +
+
+`; diff --git a/src/components/dataset/DataTableHeader/__snapshots__/DataTableHeader.test.js.snap b/src/components/dataset/DataTableHeader/__snapshots__/DataTableHeader.test.js.snap new file mode 100644 index 00000000..03bd0d04 --- /dev/null +++ b/src/components/dataset/DataTableHeader/__snapshots__/DataTableHeader.test.js.snap @@ -0,0 +1,1590 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DataHeaderButton component. Matches snapshot. 1`] = ` + +
+
+
+
+

+ Viewing + + 1 + + - + + 20 + + of + + 21 + + rows +

+
+
+
+ + + +
+
+ + + +
+ +
+
+

+ Manage columns +

+ +
+
+
+
+ + Display column + + + Reorder + +
+

+ Activate the reorder button and use the arrow keys to reorder the list or use your mouse to drag/reorder. Press escape to cancel the reordering. +

+
    +
  • +
    +
    + + +
    +
    + +
  • +
  • +
    +
    + + +
    +
    + +
  • +
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
+
+
+ + + +
+
+
+ + + +
+ +
+
+

+ + Dataset Explorer + +

+ +
+
+
+
+
+

+ +

+
+
+
+ A list of Suppliers that indicates the supplies carried at that location and the supplier's Medicare participation status. +
+
+
+ + Last Modified + + : May 10, 2020 +
+ + • + +
+ + Released + + : May 10, 2020 +
+
+
+
+
+
+
+
+
+

+ Viewing + + 1 + + - + + 20 + + of + + 21 + + rows +

+
+
+
+ + + +
+
+ + + +
+ +
+
+

+ Manage columns +

+ +
+
+
+
+ + Display column + + + Reorder + +
+

+ Activate the reorder button and use the arrow keys to reorder the list or use your mouse to drag/reorder. Press escape to cancel the reordering. +

+
    +
  • +
    +
    + + +
    +
    + +
  • +
  • +
    +
    + + +
    +
    + +
  • +
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+

+ Activate the column resize button and use the right and left arrow keys to resize a column or use your mouse to drag/resize. Press escape to cancel the resizing. +

+
+
+
+
+
+
+
+
+ + Measure Name + + + + + +
+
+ + Measure Date Range + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Average Medicare spending associated with an agency's home health episodes compared with all home health episodes nationally - agency score +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ Average Medicare spending associated with an agency's home health episodes compared with all home health episodes nationally - count +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ For patients with diabetes, how often the home health team got doctor's orders, gave foot care, and taught patients about foot care +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often home health patients had new or worsened pressure ulcers +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often home health patients had to be admitted to the hospital +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often home health patients, who have had a recent hospital stay, had a preventable hospital readmission within 30 days of discharge from home health +
+
+ January 1, 2016 - December 31, 2018 +
+
+
+
+ How often patients got better at bathing +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients got better at getting in and out of bed +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients got better at taking their drugs correctly by mouth +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients got better at walking or moving around +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients receiving home health care needed any urgent, unplanned care in the hospital emergency room - without being admitted to the hospital +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients remained at home within 31 days of being discharged from home health +
+
+ January 1, 2017 - December 31, 2018 +
+
+
+
+ How often patients' breathing improved +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients' wounds improved or healed after an operation +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team began their patients' care in a timely manner +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team checked patients for depression +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team checked patients' medications and got doctor's orders for medication issues in a timely manner +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team checked patients' risk of falling +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team made sure that their patients have received a flu shot for the current flu season +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team made sure that their patients have received a pneumococcal vaccine (pneumonia shot) +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+
+
+
+
+
+
+ +
+
+ + + Page + + + 1 + + + of + + + 2 + + + for Supplier Directory Data + + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Activate the column resize button and use the right and left arrow keys to resize a column or use your mouse to drag/resize. Press escape to cancel the resizing. +

+
+ +`; diff --git a/src/components/dataset/DataTableHeader/__snapshots__/DataTableHeader.test.jsx.snap b/src/components/dataset/DataTableHeader/__snapshots__/DataTableHeader.test.jsx.snap new file mode 100644 index 00000000..91484cf8 --- /dev/null +++ b/src/components/dataset/DataTableHeader/__snapshots__/DataTableHeader.test.jsx.snap @@ -0,0 +1,1768 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`DataHeaderButton component. > Matches snapshot. 1`] = ` + +
+
+
+
+

+ Viewing + + 1 + + - + + 20 + + of + + 21 + + rows +

+
+
+
+ + + +
+
+ + + +
+ +
+
+

+ Manage columns +

+ +
+
+
+
+ + Display column + + + Reorder + +
+

+ Activate the reorder button and use the arrow keys to reorder the list or use your mouse to drag/reorder. Press escape to cancel the reordering. +

+
    +
  • +
    +
    + + +

    +

    +
    + +
  • +
  • +
    +
    + + +

    +

    +
    + +
  • +
+
+
+
+
+
+
+ + +

+

+
+ +
+
+
+
+
+
+
+
+
+ + + +
+
+
+ + + +
+ +
+
+

+ + Dataset Explorer + +

+ +
+
+
+
+
+

+ +

+
+
+
+ A list of Suppliers that indicates the supplies carried at that location and the supplier's Medicare participation status. +
+
+
+ + + Last Modified + + : May 10, 2020 + +
+ + +
+
+ + • + +
+ + + Released + + : May 10, 2020 + +
+ + +
+
+
+
+
+
+
+
+
+
+

+ Viewing + + 1 + + - + + 20 + + of + + 21 + + rows +

+
+
+
+ + + +
+
+ + + +
+ +
+
+

+ Manage columns +

+ +
+
+
+
+ + Display column + + + Reorder + +
+

+ Activate the reorder button and use the arrow keys to reorder the list or use your mouse to drag/reorder. Press escape to cancel the reordering. +

+
    +
  • +
    +
    + + +

    +

    +
    + +
  • +
  • +
    +
    + + +

    +

    +
    + +
  • +
+
+
+
+
+
+
+ + +

+

+
+ +
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+

+ Activate the column resize button and use the right and left arrow keys to resize a column or use your mouse to drag/resize. Press escape to cancel the resizing. +

+
+
+
+
+
+
+
+
+ + + Measure Name + + + + + + +
+
+ + + Measure Date Range + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Average Medicare spending associated with an agency's home health episodes compared with all home health episodes nationally - agency score +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ Average Medicare spending associated with an agency's home health episodes compared with all home health episodes nationally - count +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ For patients with diabetes, how often the home health team got doctor's orders, gave foot care, and taught patients about foot care +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often home health patients had new or worsened pressure ulcers +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often home health patients had to be admitted to the hospital +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often home health patients, who have had a recent hospital stay, had a preventable hospital readmission within 30 days of discharge from home health +
+
+ January 1, 2016 - December 31, 2018 +
+
+
+
+ How often patients got better at bathing +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients got better at getting in and out of bed +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients got better at taking their drugs correctly by mouth +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients got better at walking or moving around +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients receiving home health care needed any urgent, unplanned care in the hospital emergency room - without being admitted to the hospital +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients remained at home within 31 days of being discharged from home health +
+
+ January 1, 2017 - December 31, 2018 +
+
+
+
+ How often patients' breathing improved +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often patients' wounds improved or healed after an operation +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team began their patients' care in a timely manner +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team checked patients for depression +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team checked patients' medications and got doctor's orders for medication issues in a timely manner +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team checked patients' risk of falling +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team made sure that their patients have received a flu shot for the current flu season +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+ How often the home health team made sure that their patients have received a pneumococcal vaccine (pneumonia shot) +
+
+ January 1, 2018 - December 31, 2018 +
+
+
+
+
+
+
+
+
+
+ +
+
+ + + Page + + + 1 + + + of + + + 2 + + + for Supplier Directory Data + + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Activate the column resize button and use the right and left arrow keys to resize a column or use your mouse to drag/resize. Press escape to cancel the resizing. +

+
+ +`; diff --git a/src/components/dataset/DataTableHeader/__snapshots__/PopOverContent.test.js.snap b/src/components/dataset/DataTableHeader/__snapshots__/PopOverContent.test.js.snap new file mode 100644 index 00000000..d06b02ef --- /dev/null +++ b/src/components/dataset/DataTableHeader/__snapshots__/PopOverContent.test.js.snap @@ -0,0 +1,143 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PopOverContent component. Matches snapshot. 1`] = ` +
+
+ + Row height + +
+ + + +
+
+
+ + Rows per page + + +
+
+`; diff --git a/src/components/dataset/DataTableHeader/__snapshots__/PopOverContent.test.jsx.snap b/src/components/dataset/DataTableHeader/__snapshots__/PopOverContent.test.jsx.snap new file mode 100644 index 00000000..28183668 --- /dev/null +++ b/src/components/dataset/DataTableHeader/__snapshots__/PopOverContent.test.jsx.snap @@ -0,0 +1,143 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`PopOverContent component. > Matches snapshot. 1`] = ` +
+
+ + Row height + +
+ + + +
+
+
+ + Rows per page + + +
+
+`; diff --git a/src/components/dataset/DataTablePageResults/DataTablePageResults.jsx b/src/components/dataset/DataTablePageResults/DataTablePageResults.jsx new file mode 100644 index 00000000..a9e5f0d7 --- /dev/null +++ b/src/components/dataset/DataTablePageResults/DataTablePageResults.jsx @@ -0,0 +1,52 @@ +import React from 'react' +import PropTypes from 'prop-types' + +const DataTablePageResults = ({ + total, + pageSize, + currentPage, + className = 'data-table-results', + viewing = false +}) => { + // Add one to offset the 0 array index. + const page = currentPage + 1 + let displayTotal = total + const currentLowestResult = total <= 0 ? 0 : 1 + ((pageSize * page) - pageSize) + let currentHighestResult = (pageSize * page) + if (total < 0) { + displayTotal = 0 + } + if (currentHighestResult > total) { + currentHighestResult = displayTotal + } + return ( +
+

+ {viewing && ( + 'Viewing ' + )} + {currentLowestResult} + {' '} + - + {' '} + {currentHighestResult} + {' '} + of + {' '} + {displayTotal.toLocaleString()} + {' '} + rows +

+
+ ) +} + +DataTablePageResults.propTypes = { + className: PropTypes.string, + total: PropTypes.number.isRequired, + pageSize: PropTypes.number.isRequired, + currentPage: PropTypes.number.isRequired, + viewing: PropTypes.bool +} + +export default DataTablePageResults diff --git a/src/components/dataset/DataTablePageResults/DataTablePageResults.test.jsx b/src/components/dataset/DataTablePageResults/DataTablePageResults.test.jsx new file mode 100644 index 00000000..b72d4d23 --- /dev/null +++ b/src/components/dataset/DataTablePageResults/DataTablePageResults.test.jsx @@ -0,0 +1,45 @@ +import { render, fireEvent } from '@testing-library/react' +import DataTablePageResults from './DataTablePageResults' + +const componentArgs = { + total: 100, + pageSize: 20, + currentPage: 0, + className: '', + viewing: false +} + +const renderComponent = (args = componentArgs) => render( + +) + +describe('DataTablePageResults component.', () => { + it('Matches snapshot.', () => { + const renderedDataTablePageResults = renderComponent({ + ...componentArgs, + viewing: undefined + }) + + expect(renderedDataTablePageResults.asFragment()).toMatchSnapshot() + }) + + it('Displays \'Viewing\' text when \'viewing\' prop is true.', () => { + const { container } = renderComponent({ + ...componentArgs, + viewing: true, + className: 'test-container' + }) + + expect(container.querySelector('.test-container').textContent).toBe('Viewing 1 - 20 of 100 rows') + }) + + it('Display total is 0 by default.', () => { + const { container } = renderComponent({ + ...componentArgs, + className: 'test-container', + total: -1 + }) + + expect(container.querySelector('.test-container').textContent).toBe('0 - 0 of 0 rows') + }) +}) diff --git a/src/components/dataset/DataTablePageResults/__snapshots__/DataTablePageResults.test.js.snap b/src/components/dataset/DataTablePageResults/__snapshots__/DataTablePageResults.test.js.snap new file mode 100644 index 00000000..fc67ba16 --- /dev/null +++ b/src/components/dataset/DataTablePageResults/__snapshots__/DataTablePageResults.test.js.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DataTablePageResults component. Matches snapshot. 1`] = ` + +
+

+ + 1 + + - + + 20 + + of + + 100 + + rows +

+
+
+`; diff --git a/src/components/dataset/DataTablePageResults/__snapshots__/DataTablePageResults.test.jsx.snap b/src/components/dataset/DataTablePageResults/__snapshots__/DataTablePageResults.test.jsx.snap new file mode 100644 index 00000000..ac8fef8c --- /dev/null +++ b/src/components/dataset/DataTablePageResults/__snapshots__/DataTablePageResults.test.jsx.snap @@ -0,0 +1,30 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`DataTablePageResults component. > Matches snapshot. 1`] = ` + +
+

+ + 1 + + - + + 20 + + of + + 100 + + rows +

+
+
+`; diff --git a/src/components/dataset/DatasetContentLoading/DatasetContentLoading.jsx b/src/components/dataset/DatasetContentLoading/DatasetContentLoading.jsx new file mode 100644 index 00000000..f22c4e84 --- /dev/null +++ b/src/components/dataset/DatasetContentLoading/DatasetContentLoading.jsx @@ -0,0 +1,80 @@ +import React from 'react' +import cx from 'classnames' +import PropTypes from 'prop-types' +import './DatasetContentLoading.scss' + +const DatasetContentLoadingTable = ({ + className, + columns, + rows, + pagination +}) => ( +
+
+ {Array(rows) + .fill(0) + .map((v, i) => ( +
+ {Array(columns) + .fill(0) + .map((v, i) => ( +
+
+
+ ))} +
+ ))} + {pagination && ( +
+
+
+
+
+ )} +
+) + +DatasetContentLoadingTable.propTypes = { + className: PropTypes.any, + columns: PropTypes.any, + rows: PropTypes.any, + pagination: PropTypes.any +} +DatasetContentLoadingTable.displayName = 'DatasetContentLoadingTable' + +export const DatasetHeaderLoading = () => ( +
+ +
+) +export const DatasetContentLoading = () => ( +
+ +
+) +const DatasetLoading = () => ( +
+ + +
+) +DatasetLoading.displayName = 'DatasetLoading' +export default DatasetLoading diff --git a/src/components/dataset/DatasetContentLoading/DatasetContentLoading.scss b/src/components/dataset/DatasetContentLoading/DatasetContentLoading.scss new file mode 100644 index 00000000..82c30d35 --- /dev/null +++ b/src/components/dataset/DatasetContentLoading/DatasetContentLoading.scss @@ -0,0 +1,66 @@ +.DatasetContentLoading { + > .DatasetContentLoading__dataset-explorer { + margin-bottom: 60px; + } + + > .DatasetContentLoading__additional-info { + > .DatasetContentLoadingTable__row { + > .DatasetContentLoadingTable__row_column { + &:first-child { + max-width: 30%; + } + } + } + } +} + +.DatasetContentLoadingTable { + > .DatasetContentLoadingTable__title { + height: 30px; + max-width: 300px; + width: 100%; + display: block; + margin-bottom: 30px; + } + + > .DatasetContentLoadingTable__row { + display: flex; + justify-content: space-between; + margin-bottom: 10px; + + > .DatasetContentLoadingTable__row_column { + padding-right: 20px; + flex: 1; + display: flex; + align-items: center; + + &:last-child { + padding-right: 0; + } + + > div { + width: 100%; + height: 30px; + } + } + } + + .DatasetContentLoadingTable__pagination { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 12px; + + > .DatasetContentLoadingTable__pagination_btn { + height: 30px; + max-width: 120px; + width: 100%; + } + + > .DatasetContentLoadingTable__pagination_txt { + height: 20px; + max-width: 150px; + width: 100%; + } + } +} diff --git a/src/components/dataset/DatasetContentLoading/DatasetContentLoading.test.jsx b/src/components/dataset/DatasetContentLoading/DatasetContentLoading.test.jsx new file mode 100644 index 00000000..02ddf864 --- /dev/null +++ b/src/components/dataset/DatasetContentLoading/DatasetContentLoading.test.jsx @@ -0,0 +1,28 @@ +import renderer from 'react-test-renderer' +import DatasetLoading, { DatasetContentLoading, DatasetHeaderLoading } from './DatasetContentLoading' + +describe('DatasetContentLoading component.', () => { + it('DatasetContentLoading matches snapshot.', () => { + const renderedDatasetContentLoading = renderer.create( + + ).toJSON() + + expect(renderedDatasetContentLoading).toMatchSnapshot() + }) + + it('DatasetLoading matches snapshot.', () => { + const renderedDatasetLoading = renderer.create( + + ).toJSON() + + expect(renderedDatasetLoading).toMatchSnapshot() + }) + + it('DatasetHeaderLoading matches snapshot.', () => { + const renderedDatasetHeaderLoading = renderer.create( + + ).toJSON() + + expect(renderedDatasetHeaderLoading).toMatchSnapshot() + }) +}) diff --git a/src/components/dataset/DatasetContentLoading/__snapshots__/DatasetContentLoading.test.js.snap b/src/components/dataset/DatasetContentLoading/__snapshots__/DatasetContentLoading.test.js.snap new file mode 100644 index 00000000..f771b97a --- /dev/null +++ b/src/components/dataset/DatasetContentLoading/__snapshots__/DatasetContentLoading.test.js.snap @@ -0,0 +1,768 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DatasetContentLoading component. DatasetContentLoading matches snapshot. 1`] = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`DatasetContentLoading component. DatasetHeaderLoading matches snapshot. 1`] = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`DatasetContentLoading component. DatasetLoading matches snapshot. 1`] = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/src/components/dataset/DatasetContentLoading/__snapshots__/DatasetContentLoading.test.jsx.snap b/src/components/dataset/DatasetContentLoading/__snapshots__/DatasetContentLoading.test.jsx.snap new file mode 100644 index 00000000..e2c9ff25 --- /dev/null +++ b/src/components/dataset/DatasetContentLoading/__snapshots__/DatasetContentLoading.test.jsx.snap @@ -0,0 +1,768 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`DatasetContentLoading component. > DatasetContentLoading matches snapshot. 1`] = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`DatasetContentLoading component. > DatasetHeaderLoading matches snapshot. 1`] = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`DatasetContentLoading component. > DatasetLoading matches snapshot. 1`] = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/src/components/dataset/DatasetDownloadLink/DatasetDownloadLink.jsx b/src/components/dataset/DatasetDownloadLink/DatasetDownloadLink.jsx new file mode 100644 index 00000000..a066135f --- /dev/null +++ b/src/components/dataset/DatasetDownloadLink/DatasetDownloadLink.jsx @@ -0,0 +1,131 @@ +import React, { useState } from 'react' +import PropTypes from 'prop-types' +import { Dialog } from '@cmsgov/design-system' +import { HashLink } from 'react-router-hash-link' + +import './DatasetDownloadLink.scss' + +const DatasetDownloadLink = ({ + linkText, + linkAriaLabel = '', + linkHref, + className, + downloadAttr = false, + onClick = () => {} +}) => { + const [modalOpen, toggleModal] = useState(false) + + const additionalLinkAttrs = { + className: 'dataset-download-link' + } + if (linkAriaLabel) { + additionalLinkAttrs['aria-label'] = linkAriaLabel + } + if (className) { + additionalLinkAttrs.className = `${additionalLinkAttrs.className} ${className}` + } + if (downloadAttr) { + additionalLinkAttrs.download = true + } + const additionalBtnAttrs = {} + if (linkHref) { + additionalBtnAttrs['data-download-url'] = linkHref + } + + return ( + <> + { + e.preventDefault() + + toggleModal(!modalOpen) + }} + {...additionalLinkAttrs} + > + {linkText} + +
+ { + toggleModal(!modalOpen) + }} + actions={( +
+ + +
+ )} + > +

Before downloading these files in Excel, please review the Excel Download Instructions to ensure accuracy with the display of your data when downloading, opening and saving these files.

+
    +
  • + { + document.querySelector('body').classList.remove('ds--dialog-open') + }} + > + Excel Download Instructions + +
  • +
+

Would you like to proceed?

+
+
+ + ) +} + +DatasetDownloadLink.propTypes = { + linkText: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.node + ]), + linkAriaLabel: PropTypes.string, + /** + * Leave as empty string or simply do not pass linkHref prop if using onClick + */ + linkHref: PropTypes.string, + className: PropTypes.string, + downloadAttr: PropTypes.bool, + /** + * Only fires if linkHref is falsy + */ + onClick: PropTypes.func +} + +DatasetDownloadLink.displayName = 'DatasetDownloadLink' + +export default DatasetDownloadLink diff --git a/src/components/dataset/DatasetDownloadLink/DatasetDownloadLink.scss b/src/components/dataset/DatasetDownloadLink/DatasetDownloadLink.scss new file mode 100644 index 00000000..164cba09 --- /dev/null +++ b/src/components/dataset/DatasetDownloadLink/DatasetDownloadLink.scss @@ -0,0 +1,139 @@ +@use "../../../scss/modules/modules"; +@use "../../../scss/modules/colormap"; +@use "../../../scss/modules/functions"; +@use "../../../scss/modules/variables"; + +// Desktop +$header-height: 64px; +$subheader-height: 56px; +$h-padding-md: 24px; +$footer-height: 80px; + +// Mobile +$subheader-height-xs: 48px; +$h-padding-xs: 16px; +$footer-height-xs: 72px; + +.ds-c-dialog-wrap { + display: flex; + align-items: center; + justify-content: center; + padding: 0 !important; + overflow: hidden !important; +} + +.csv-download-link-dialog { + &.ds-c-dialog { + font-family: variables.$muli; + color: colormap.$charcoal; + padding: 0; + z-index: 12; + border-radius: 2px; + width: 100%; + max-width: 768px; + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.4); + + .ds-c-dialog__window { + .ds-c-dialog__body { + height: calc(100% - (#{$header-height})); + display: flex; + flex-direction: column; + + div[id^="dialog"][id$="__content"] { + flex: 1; + overflow: scroll; + padding: 0 24px 20px; + font-size: 16px; + } + } + } + + .ds-c-dialog__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 $h-padding-md; + border-bottom: 1px solid colormap.$border-color; + min-height: $header-height; + + h1 { + @include functions.h(3); + & { + margin: 0; + font-weight: 800; + font-size: 20px; + } + } + + button.ds-c-dialog__close { + margin: 0; + color: colormap.$charcoal; + + &:hover { + background-color: colormap.$charcoal-5; + } + + &::before { + color: colormap.$charcoal; + } + } + } + + // Footer + .ds-c-dialog__actions { + @include functions.box-shadow(bottom); + margin: 0; + padding: 0 $h-padding-md; + display: flex; + align-items: center; + border-top: 1px solid colormap.$border-color; + height: $footer-height; + position: relative; + z-index: 2; + overflow: auto; + + .csv-download-link-dialog-actions { + padding: 12px 24px; + display: flex; + align-items: center; + justify-content: flex-end; + flex: 1; + background-color: #fff; + white-space: nowrap; + } + } + + .csv-faq-link { + margin-top: 15px; + } + } +} + +@media screen and (max-width: 767px) { + .csv-download-link-dialog { + &.ds-c-dialog[class*="csv-download-link-"] { + height: 100%; + border-radius: 0; + + .ds-c-dialog__window { + .ds-c-dialog__body { + height: calc(100% - (#{$header-height} + #{$footer-height-xs})); + } + } + + .ds-c-dialog__header { + padding: 0 $h-padding-xs; + + h1 { + @include functions.h(3); + } + } + + // Footer + .ds-c-dialog__actions { + padding: 0 $h-padding-xs; + height: $footer-height-xs; + } + } + } +} diff --git a/src/components/dataset/DatasetDownloadLink/DatasetDownloadLink.test.jsx b/src/components/dataset/DatasetDownloadLink/DatasetDownloadLink.test.jsx new file mode 100644 index 00000000..4ad13734 --- /dev/null +++ b/src/components/dataset/DatasetDownloadLink/DatasetDownloadLink.test.jsx @@ -0,0 +1,162 @@ +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import DatasetDownloadLink from './DatasetDownloadLink' +import { MemoryRouter } from 'react-router-dom' + +// Mock window.scrollTo +global.scrollTo = vi.fn() + +// Mock window.location +global.window = Object.create(window) +Object.defineProperty(window, 'location', { + value: { + href: '' + }, + writable: true +}) + +const componentArgs = { + linkText: 'Download CSV', + linkAriaLabel: undefined, + linkHref: 'https://www.google.com', + className: undefined, + downloadAttr: false, + onClick: vi.fn() +} + +const renderComponent = (args = componentArgs) => render( + + + +) + +describe('DatasetDownloadLink component.', () => { + it('Matches snapshot.', () => { + const renderedDatasetDownloadLink = renderComponent({ + ...componentArgs, + linkText: 'Download CSV', + linkAriaLabel: 'Download CSV', + linkHref: 'https://www.google.com' + }) + + expect(renderedDatasetDownloadLink.asFragment()).toMatchSnapshot() + }) + + it('Dialog does not show initially', () => { + const { container } = renderComponent() + + expect(container.querySelector('.csv-download-link-dialog').getAttribute('open')).toBe(null) + }) + + it('Download link has class name when passed via \'className\' prop', () => { + renderComponent({ + ...componentArgs, + linkText: 'Download CSV', + linkHref: 'https://www.google.com', + className: 'test-class' + }) + + expect(screen.getByRole('button', { name: 'Download CSV' })).toHaveClass('test-class') + }) + + it('Download link has download attribute when passed via \'download\' prop', () => { + renderComponent({ + ...componentArgs, + linkText: 'Download CSV', + linkHref: 'https://www.google.com', + downloadAttr: true + }) + + expect(screen.getByRole('button', { name: 'Download CSV' }).getAttribute('download')).toEqual('') + }) + + it('Clicking the \'Download CSV\' link reveals the dialog', async () => { + const { container } = renderComponent() + + await waitFor(() => userEvent.click(screen.getByRole('button', { name: 'Download CSV' }))) + + expect(container.querySelector('.csv-download-link-dialog').getAttribute('open')).toBe('') + }) + + it('Cancelling the dialog closes the dialog', async () => { + const { container } = renderComponent() + + await waitFor(() => userEvent.click(screen.getByRole('button', { name: 'Download CSV' }))) + + expect(container.querySelector('.csv-download-link-dialog').getAttribute('open')).toBe('') + + await waitFor(() => userEvent.click(screen.getByRole('button', { name: 'No, cancel' }))) + + expect(container.querySelector('.csv-download-link-dialog').getAttribute('open')).toBe(null) + }) + + it('Clicking the close button (X) closes the dialog', async () => { + const { container } = renderComponent() + + await waitFor(() => userEvent.click(screen.getByRole('button', { name: 'Download CSV' }))) + + expect(container.querySelector('.csv-download-link-dialog').getAttribute('open')).toBe('') + + await waitFor(() => userEvent.click(screen.getByRole('button', { name: 'Close notice dialog' }))) + + expect(container.querySelector('.csv-download-link-dialog').getAttribute('open')).toBe(null) + }) + + it('Clicking the \'Ok, download\' button downloads the CSV', async () => { + const { container } = renderComponent() + + await waitFor(() => userEvent.click(screen.getByRole('button', { name: 'Download CSV' }))) + + expect(container.querySelector('.csv-download-link-dialog').getAttribute('open')).toBe('') + + await waitFor(() => userEvent.click(screen.getByRole('button', { name: 'Yes, download' }))) + + expect(container.querySelector('.csv-download-link-dialog').getAttribute('open')).toBe(null) + expect(window.location.href).toEqual('https://www.google.com') + }) + + it('Fires default \'onClick\' prop function when onClick prop is passed', async () => { + const { container } = renderComponent({ + ...componentArgs, + linkHref: undefined, // No purpose for doing this - just for full test coverage + onClick: undefined // No purpose for doing this - just for full test coverage + }) + + await waitFor(() => userEvent.click(screen.getByRole('button', { name: 'Download CSV' }))) + + expect(container.querySelector('.csv-download-link-dialog').getAttribute('open')).toBe('') + + await waitFor(() => userEvent.click(screen.getByRole('button', { name: 'Yes, download' }))) + + expect(container.querySelector('.csv-download-link-dialog').getAttribute('open')).toBe(null) + }) + + it('Fires custom \'onClick\' prop function when onClick prop is passed', async () => { + const onClick = vi.fn() + + const { container } = renderComponent({ + ...componentArgs, + linkHref: undefined, + onClick: onClick + }) + + await waitFor(() => userEvent.click(screen.getByRole('button', { name: 'Download CSV' }))) + + expect(container.querySelector('.csv-download-link-dialog').getAttribute('open')).toBe('') + + await waitFor(() => userEvent.click(screen.getByRole('button', { name: 'Yes, download' }))) + + expect(onClick).toHaveBeenCalled() + }) +}) diff --git a/src/components/dataset/DatasetDownloadLink/__snapshots__/DatasetDownloadLink.test.js.snap b/src/components/dataset/DatasetDownloadLink/__snapshots__/DatasetDownloadLink.test.js.snap new file mode 100644 index 00000000..cff01738 --- /dev/null +++ b/src/components/dataset/DatasetDownloadLink/__snapshots__/DatasetDownloadLink.test.js.snap @@ -0,0 +1,113 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DatasetDownloadLink component. Matches snapshot. 1`] = ` + + + Download CSV + +
+ +
+
+

+ Notice: Downloading in Excel +

+ +
+
+
+

+ Before downloading these files in Excel, please review the + + Excel Download Instructions + + to ensure accuracy with the display of your data when downloading, opening and saving these files. +

+ +

+ Would you like to proceed? +

+
+
+ +
+
+
+
+
+
+`; diff --git a/src/components/dataset/DatasetDownloadLink/__snapshots__/DatasetDownloadLink.test.jsx.snap b/src/components/dataset/DatasetDownloadLink/__snapshots__/DatasetDownloadLink.test.jsx.snap new file mode 100644 index 00000000..72cd8dc7 --- /dev/null +++ b/src/components/dataset/DatasetDownloadLink/__snapshots__/DatasetDownloadLink.test.jsx.snap @@ -0,0 +1,113 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`DatasetDownloadLink component. > Matches snapshot. 1`] = ` + + + Download CSV + +
+ +
+
+

+ Notice: Downloading in Excel +

+ +
+
+
+

+ Before downloading these files in Excel, please review the + + Excel Download Instructions + + to ensure accuracy with the display of your data when downloading, opening and saving these files. +

+ +

+ Would you like to proceed? +

+
+
+ +
+
+
+
+
+
+`; diff --git a/src/components/dataset/DatasetExplorerDownloadLink/DatasetExplorerDownloadLink.jsx b/src/components/dataset/DatasetExplorerDownloadLink/DatasetExplorerDownloadLink.jsx new file mode 100644 index 00000000..53db56e5 --- /dev/null +++ b/src/components/dataset/DatasetExplorerDownloadLink/DatasetExplorerDownloadLink.jsx @@ -0,0 +1,135 @@ +import React, { useContext, useState, useEffect } from 'react' +import { FilteredDispatch } from '../DatasetResource/FilteredDatasetContext' +import FilteredDownload from '../../common/FileDownload/FilteredDownload' +import FileDownload from '../../common/FileDownload/FileDownload' +import { getApiBaseUrl } from '../../../utilities/getApiBaseUrl' +import { DatasetContext } from '../../../context/DatasetContext' +import qs from 'qs' +import PropTypes from 'prop-types' +import toLower from 'lodash/toLower' +import './DatasetExplorerDownloadLink.scss' + +const DatasetExplorerDownloadLink = ({ force }) => { + const { + filteredResource, + filteredTable, + initOrder, + curOrder, + filtersApplied + } = useContext(FilteredDispatch) + + const [filteredURL, setFilteredURL] = useState('') + const datasetState = useContext(DatasetContext) + const { data: dataset = {}, isLoading } = datasetState + const title = dataset?.title || '' + const identifier = datasetState?.data?.identifier || undefined + const distribution = 'distribution' in dataset ? dataset.distribution : [] + let isPublic + if (dataset.accessLevel) { + isPublic = toLower(dataset.accessLevel) === 'public' + } + + const fileDownloadResource = distribution[0] + /* istanbul ignore else */ + if (fileDownloadResource !== undefined) { + fileDownloadResource.format = 'csv' + } + + // have columns been reordered from initial order? + const checkOrder = (arr1, arr2) => { + if (!arr1 || !arr2) { + return false + } + for (let i = 0; i < arr1.length; i++) { + if (arr1[i].id !== arr2[i]) { + return true + } + } + return false + } + + const resourceData = { data: fileDownloadResource } + const columnsHidden = filteredTable?.visibleColumns?.length !== initOrder?.length + const columnsReOrdered = checkOrder(initOrder, curOrder) + + const isFiltered = (() => { + if (columnsReOrdered) { + return true + } + if (columnsHidden) { + return true + } + if (filtersApplied.length > 0) { + return true + } + return false + })() + + useEffect(() => { + if (!isFiltered) { + setFilteredURL('') + } else { + const baseUrl = getApiBaseUrl() + + const getFilteredDownload = () => { + const fConditions = filteredResource?.conditions?.length > 0 ? filteredResource?.conditions.map((c) => { + return c + }) : [] + const fProperties = filteredTable?.visibleColumns?.length > 0 ? filteredTable?.visibleColumns.map((c) => c.id) : [] + const orderMap = {} + initOrder.forEach((columnInfo, index) => { + orderMap[columnInfo.id] = index + 1 + }) + const transformedProperties = fProperties.map((property) => { + return orderMap[property] + }) + const reqObject = { + conditions: fConditions, + properties: transformedProperties + } + let joiner = '' + if (fConditions.length > 0 && transformedProperties.length > 0) { + joiner = '&' + } + const encodedProperties = transformedProperties.length > 0 ? `properties=${transformedProperties.join('-')}` : '' + const url = `${baseUrl}/pdc/query/${identifier}/0/download?${qs.stringify({ conditions: reqObject.conditions }, { encode: true })}${joiner}${encodedProperties}&format=csv` + setFilteredURL(url) + } + getFilteredDownload() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isFiltered, filteredResource.conditions, filteredTable.visibleColumns]) + + return ( + (isPublic && !isLoading && resourceData.data !== undefined) ? ( +
+ {((isFiltered && !force) || force === 'filtered') ? ( + + ) : ( + + )} +
+ ) : null + ) +} + +DatasetExplorerDownloadLink.propTypes = { + /** + * Forces a particular download link to show + * whether the dataset is filtered or not + */ + force: PropTypes.oneOf(['full', 'filtered']) +} + +DatasetExplorerDownloadLink.displayName = 'DatasetExplorerDownloadLink' +export default DatasetExplorerDownloadLink diff --git a/src/components/dataset/DatasetExplorerDownloadLink/DatasetExplorerDownloadLink.scss b/src/components/dataset/DatasetExplorerDownloadLink/DatasetExplorerDownloadLink.scss new file mode 100644 index 00000000..f600181b --- /dev/null +++ b/src/components/dataset/DatasetExplorerDownloadLink/DatasetExplorerDownloadLink.scss @@ -0,0 +1,37 @@ +@use "../../../scss/modules/modules"; +@use "../../../scss/modules/colormap"; + +.dataset-explorer-download-link { + margin: 0; + padding-bottom: 8px; + text-align: right; + + > a { + display: inline-flex; + align-items: center; + border: solid 2px colormap.$blueberry; + margin: 0; + border-radius: 25px; + font-weight: 700; + padding: 6px 23px; + max-width: 100%; + word-break: break-all; + + span { + display: inline-block; + margin-left: 6px; + } + + > svg { + flex: none; + + > path { + fill: colormap.$blueberry; + } + } + } +} + +.chip-list:empty + .dataset-explorer-download-link { + padding-top: 12px; +} diff --git a/src/components/dataset/DatasetHeaderLoading/DatasetHeaderLoading.jsx b/src/components/dataset/DatasetHeaderLoading/DatasetHeaderLoading.jsx new file mode 100644 index 00000000..4a834bab --- /dev/null +++ b/src/components/dataset/DatasetHeaderLoading/DatasetHeaderLoading.jsx @@ -0,0 +1,16 @@ +import React from 'react' + +import './DatasetHeaderLoading.scss' + +const DatasetHeaderLoading = () => ( +
+
+
+
+
+
+
+
+) +DatasetHeaderLoading.displayName = 'DatasetHeaderLoading' +export default DatasetHeaderLoading diff --git a/src/components/dataset/DatasetHeaderLoading/DatasetHeaderLoading.scss b/src/components/dataset/DatasetHeaderLoading/DatasetHeaderLoading.scss new file mode 100644 index 00000000..12455eb6 --- /dev/null +++ b/src/components/dataset/DatasetHeaderLoading/DatasetHeaderLoading.scss @@ -0,0 +1,41 @@ +.DatasetHeaderLoading { + position: relative; + margin-bottom: 24px; + padding-bottom: 24px; + + &::after { + content: ""; + width: 48px; + height: 4px; + background-color: #42e288; + position: absolute; + bottom: 0; + left: 0; + } + + > .DatasetHeaderLoading__title { + margin: 1.61rem 0; + height: 38px; + width: 40%; + } + + > .DatasetHeaderLoading__desc-container { + > .DatasetHeaderLoading__desc_item { + height: 18px; + width: 100%; + margin-bottom: 3px; + display: block; + + &:last-child { + width: 80%; + margin-bottom: 0; + } + } + } + + > .DatasetHeaderLoading__date { + height: 16px; + width: 30%; + margin-top: 25px; + } +} diff --git a/src/components/dataset/DatasetHeaderLoading/DatasetHeaderLoading.test.jsx b/src/components/dataset/DatasetHeaderLoading/DatasetHeaderLoading.test.jsx new file mode 100644 index 00000000..b6b76f95 --- /dev/null +++ b/src/components/dataset/DatasetHeaderLoading/DatasetHeaderLoading.test.jsx @@ -0,0 +1,12 @@ +import renderer from 'react-test-renderer' +import DatasetHeaderLoading from './DatasetHeaderLoading' + +describe('DatasetHeaderLoading component.', () => { + it('Matches snapshot.', async () => { + const renderedDatasetHeaderLoading = renderer.create( + + ).toJSON() + + expect(renderedDatasetHeaderLoading).toMatchSnapshot() + }) +}) diff --git a/src/components/dataset/DatasetHeaderLoading/__snapshots__/DatasetHeaderLoading.test.js.snap b/src/components/dataset/DatasetHeaderLoading/__snapshots__/DatasetHeaderLoading.test.js.snap new file mode 100644 index 00000000..ffd90a46 --- /dev/null +++ b/src/components/dataset/DatasetHeaderLoading/__snapshots__/DatasetHeaderLoading.test.js.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DatasetHeaderLoading component. Matches snapshot. 1`] = ` +
+
+
+
+
+
+
+
+`; diff --git a/src/components/dataset/DatasetHeaderLoading/__snapshots__/DatasetHeaderLoading.test.jsx.snap b/src/components/dataset/DatasetHeaderLoading/__snapshots__/DatasetHeaderLoading.test.jsx.snap new file mode 100644 index 00000000..c33bcbb6 --- /dev/null +++ b/src/components/dataset/DatasetHeaderLoading/__snapshots__/DatasetHeaderLoading.test.jsx.snap @@ -0,0 +1,25 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`DatasetHeaderLoading component. > Matches snapshot. 1`] = ` +
+
+
+
+
+
+
+
+`; diff --git a/src/components/dataset/DatasetResource/DatasetResource.jsx b/src/components/dataset/DatasetResource/DatasetResource.jsx new file mode 100644 index 00000000..98f7ef89 --- /dev/null +++ b/src/components/dataset/DatasetResource/DatasetResource.jsx @@ -0,0 +1,60 @@ +import React from 'react' +import PropTypes from 'prop-types' +import DataTableHeader from '../DataTableHeader/DataTableHeader' +import DataTable from '../DataTable/DataTable' + +const DatasetResource = ({ + resource, + datasetTitle, + datasetDescription, + datasetModified, + datasetReleased, + datasetRefresh +}) => { + return ( +
+ {/* Filter logic here */} + + +
+ ) +} + +DatasetResource.propTypes = { + /** + * Dataset data object + */ + resource: PropTypes.object, + /** + * Dataset title + */ + datasetTitle: PropTypes.string, + /** + * Dataset description + */ + datasetDescription: PropTypes.string, + /** + * Dataset modified date string + */ + datasetModified: PropTypes.string, + /** + * Dataset released date string + */ + datasetReleased: PropTypes.string, + /** + * Dataset anticipated refresh date string + */ + datasetRefresh: PropTypes.string +} + +DatasetResource.displayName = 'DatasetResource' +export default DatasetResource diff --git a/src/components/dataset/DatasetResource/DatasetResource.test.jsx b/src/components/dataset/DatasetResource/DatasetResource.test.jsx new file mode 100644 index 00000000..1c4de39c --- /dev/null +++ b/src/components/dataset/DatasetResource/DatasetResource.test.jsx @@ -0,0 +1,233 @@ +import { render } from '@testing-library/react' +import DatasetResource from './DatasetResource' +import { DatasetContext } from '../../../context/DatasetContext' +import FilteredDatasetResource from './FilteredDatasetResource' +import { filteredDatasetResource } from '../../../utilities/data-mocks/data-filteredDatasetResource' + +const mockDatasetResponse = require('../../../utilities/data-mocks/api-response-dataset.json') + +// Mock useDataStore hook +vi.mock('../../../hooks/useDataStore', () => ({ + __esModule: true, + default: () => ({ + loading: false, + values: [ + { + col1: '00022509', + col2: 'H5050', + col3: '022', + col4: '0', + col5: '1745108', + col6: 'M0023901' + }, + { + col1: '00022225', + col2: 'S5660', + col3: '804', + col4: '0', + col5: '545293', + col6: 'M0004994' + }, + { + col1: '00022225', + col2: 'S5660', + col3: '804', + col4: '0', + col5: '545293', + col6: 'M0012387' + }, + { + col1: '00022225', + col2: 'S5660', + col3: '806', + col4: '0', + col5: '1251596', + col6: 'M0004994' + }, + { + col1: '00022509', + col2: 'H5050', + col3: '017', + col4: '0', + col5: '1482814', + col6: 'M0004771' + }, + { + col1: '00022509', + col2: 'H5050', + col3: '802', + col4: '0', + col5: '795085', + col6: 'M0374010' + }, + { + col1: '00022509', + col2: 'H5050', + col3: '802', + col4: '0', + col5: '848164', + col6: 'M0023901' + }, + { + col1: '00022225', + col2: 'S5660', + col3: '801', + col4: '0', + col5: '2200177', + col6: 'M0006031' + }, + { + col1: '00022509', + col2: 'H5050', + col3: '021', + col4: '0', + col5: '1653166', + col6: 'M0023901' + }, + { + col1: '00022225', + col2: 'S5660', + col3: '803', + col4: '0', + col5: '1720881', + col6: 'M0004994' + }, + { + col1: '00022509', + col2: 'H5050', + col3: '009', + col4: '0', + col5: '1745108', + col6: 'M0023901' + }, + { + col1: '00022509', + col2: 'H5050', + col3: '022', + col4: '0', + col5: '795085', + col6: 'M0005335' + }, + { + col1: '00022225', + col2: 'S5660', + col3: '804', + col4: '0', + col5: '545293', + col6: 'M0019437' + }, + { + col1: '00022509', + col2: 'H5050', + col3: '004', + col4: '0', + col5: '795085', + col6: 'M0001750' + }, + { + col1: '00022509', + col2: 'H5050', + col3: '801', + col4: '0', + col5: '1653166', + col6: 'M0004771' + }, + { + col1: '00022509', + col2: 'H5050', + col3: '019', + col4: '0', + col5: '795085', + col6: 'M0374010' + } + ], + count: 16, + columns: [ + 'col1', + 'col2', + 'col3', + 'col4', + 'col5', + 'col6' + ], + limit: 20, + offset: 0, + schema: { + '7d48577d-2944-56f7-bb48-2f8272e87007': { + fields: { + col1: { + type: 'text', + mysql_type: 'text' + }, + col2: { + type: 'text', + mysql_type: 'text' + }, + col3: { + type: 'text', + mysql_type: 'text' + }, + col4: { + type: 'text', + mysql_type: 'text' + }, + col5: { + type: 'int', + length: 11, + mysql_type: 'int', + description: 'Column 5' + }, + col6: { + type: 'text', + mysql_type: 'text' + } + } + } + }, + conditions: undefined, + properties: undefined, + setProperties: vi.fn(), + setGroupings: vi.fn(), + setResource: vi.fn(), + setRootUrl: vi.fn(), + setLimit: vi.fn(), + setOffset: vi.fn(), + setConditions: vi.fn(), + setSort: vi.fn(), + setManual: vi.fn(), + setRequireConditions: vi.fn(), + fetchData: vi.fn() + }) +})) + +const datasetContextProviderValue = { + data: mockDatasetResponse, + error: null, + isLoading: true, + setDatasetState: vi.fn(), + resetDatasetState: vi.fn() +} + +const componentArgs = { + resource: filteredDatasetResource, + datasetTitle: 'Medicare Plan Info Data', + datasetDescription: 'A dataset showing all Medicare plan information from CMS.', + datasetModified: '2021-11-29', + datasetReleased: '2021-11-29', +} + +describe('DatasetResource component.', () => { + it('Matches snapshot.', async () => { + const renderedSearchContent = render( + + + + + + ) + + expect(renderedSearchContent.asFragment()).toMatchSnapshot() + }) +}) diff --git a/src/components/dataset/DatasetResource/FilteredDatasetContext.jsx b/src/components/dataset/DatasetResource/FilteredDatasetContext.jsx new file mode 100644 index 00000000..d1b0bd22 --- /dev/null +++ b/src/components/dataset/DatasetResource/FilteredDatasetContext.jsx @@ -0,0 +1,23 @@ +import { createContext } from 'react' + +export const FilteredDispatch = createContext(null) + +export const defaultResourceState = { + columnOrder: [], + columns: [], + count: 0, + unfilteredCount: 0, + unfilteredColumnCount: 0, + currentPage: 0, + density: 'density-3', + excludedColumns: {}, + filters: [], + loading: false, + limit: 20, + sort: [], + values: [], + downloadSize: { + sizeLabel: 'KB', + size: 0 + } +} diff --git a/src/components/dataset/DatasetResource/FilteredDatasetResource.jsx b/src/components/dataset/DatasetResource/FilteredDatasetResource.jsx new file mode 100644 index 00000000..3f2d6855 --- /dev/null +++ b/src/components/dataset/DatasetResource/FilteredDatasetResource.jsx @@ -0,0 +1,184 @@ +import React, { useEffect, useState } from 'react' +import PropTypes from 'prop-types' +import { FilteredDispatch } from './FilteredDatasetContext' +import useDatastore from '../../../hooks/useDataStore' +import { getApiBaseUrl } from '../../../utilities/getApiBaseUrl' + +import { + useTable, + usePagination, + useFilters, + useSortBy, + useFlexLayout, + useResizeColumns, + useColumnOrder +} from 'react-table' + +const FilteredDatasetResource = ({ + children, + resource +}) => { + const [filters, setFilters] = useState([{ column: '', condition: '', value: '' }]) // keep track of the applied filters + const [filtersApplied, setFiltersApplied] = useState([]) + const [initCol, setInitCol] = useState([]) // we want to save a reference to filter against + const [col, setCol] = useState([]) // columns we pass to table, post filter + const [data, setData] = useState([]) // these are our cell values + const [initOrder, setInitOrder] = useState([]) // initial order saved as a reference + const [curOrder, setCurOrder] = useState([]) // current order that we pass to the data table + const [visCol, setVisCol] = useState([]) // our visible column reference to prevent more datastore queries than need be + const [activeDensity, setActiveDensity] = useState('Normal') + const [downloadSize, setDownloadSize] = useState({ + sizeLabel: 'KB', + size: 0 + }) + const prepareColumns = (columns, schema) => { + return columns.map((column) => ({ + Header: schema && schema.fields[column].description ? schema.fields[column].description : column, + accessor: column + })) + } + const baseUrl = getApiBaseUrl() + const filteredResource = useDatastore( + '', + baseUrl, + { + limit: 20, + manual: true + }, + {} + // additionalParams + ) + + // listen to our filter resource changing, and flip the go switch once we have the info to query. + useEffect(() => { + if (resource.identifier) { + filteredResource.setResource(resource.identifier) + filteredResource.setManual(false) + } + if (!resource.identifier) { + setInitOrder([]) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [resource]) + + useEffect(() => { + if (visCol.length) { + const newCol = initCol.filter((item) => visCol.includes(item.accessor)) + setCol(newCol) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [visCol]) + + // the issue is that this fires more than expected. + useEffect(() => { + if (Object.keys(filteredResource.schema).length > 0) { + setData(filteredResource.values) + // we only want to initialize columns if we don't have any info + if (!initCol.length) { + const newColumns = prepareColumns(filteredResource.columns, filteredResource.schema[Object.keys(filteredResource.schema)]) + setCol(newColumns) // init + setInitCol(newColumns) // init + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filteredResource.loading]) + + const filterTypes = React.useMemo( + () => ({ + // Add a new fuzzyTextFilterFn filter type. + // fuzzyText: fuzzyTextFilterFn, + // Or, override the default text filter to use + // "startWith" + text: (rows, id, filterValue) => ( + rows.filter((row) => { + const rowValue = row.values[id] + return rowValue !== undefined + ? String(rowValue) + .toLowerCase() + .startsWith(String(filterValue).toLowerCase()) + : true + }) + ) + }), + [] + ) + + const defaultColumn = React.useMemo( + () => ({ + // Let's set up our default Filter UI + // Filter: DefaultColumnFilter, + Filter: false, + minWidth: 30, + // width: 150, + maxWidth: 400 + }), + [] + ) + const filteredTable = useTable( + { + columns: col, + data, + initialState: { pageIndex: Number(filteredResource.offset / filteredResource.limit), pageSize: 20 }, + manualPagination: true, + manualSortBy: true, + manualFilters: true, + pageCount: Math.ceil(Number(filteredResource.count) / filteredResource.limit), + defaultColumn, + filterTypes + // NextPage and previous Page are defined methods in the Dataset explorer instead + }, + useFilters, + useFlexLayout, + useResizeColumns, + useColumnOrder, + useSortBy, + usePagination + ) + + // Initialize current and init orders + useEffect(() => { + if (filteredTable.allColumns.length && !initOrder.length) { + setInitOrder(filteredTable.allColumns) + setCurOrder(filteredTable.allColumns.map((d) => d.id)) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filteredTable.allColumns.length, initOrder.length]) + + const resetVisibility = () => { + const initialCol = initCol.map((item) => item.accessor) + setVisCol(initialCol) + } + + return ( + + {children} + + ) +} +FilteredDatasetResource.propTypes = { + children: PropTypes.node, + identifier: PropTypes.string, + resource: PropTypes.object +} +FilteredDatasetResource.displayName = 'FilteredDatasetResource' +export default FilteredDatasetResource diff --git a/src/components/dataset/DatasetResource/FilteredDatasetResource.test.jsx b/src/components/dataset/DatasetResource/FilteredDatasetResource.test.jsx new file mode 100644 index 00000000..f7a640fb --- /dev/null +++ b/src/components/dataset/DatasetResource/FilteredDatasetResource.test.jsx @@ -0,0 +1,346 @@ +import { useContext } from 'react' +import { render, act } from '@testing-library/react' +import renderer from 'react-test-renderer' +import { DatasetContext } from '../../../context/DatasetContext' +import FilteredDatasetResource from './FilteredDatasetResource' +import { filteredDatasetResource } from '../../../utilities/data-mocks/data-filteredDatasetResource' +import { FilteredDispatch } from './FilteredDatasetContext' +import * as useDatastore from '../../../hooks/useDataStore' + +const mockDatasetResponse = require('../../../utilities/data-mocks/api-response-dataset.json') + +// Mock useDataStore hook value +const mockUseDataStoreDefaultValue = { + loading: false, + values: [ + { + col1: '00022509', + col2: 'H5050', + col3: '022', + col4: '0', + col5: '1745108', + col6: 'M0023901' + }, + { + col1: '00022225', + col2: 'S5660', + col3: '804', + col4: '0', + col5: '545293', + col6: 'M0004994' + }, + { + col1: '00022225', + col2: 'S5660', + col3: '804', + col4: '0', + col5: '545293', + col6: 'M0012387' + }, + { + col1: '00022225', + col2: 'S5660', + col3: '806', + col4: '0', + col5: '1251596', + col6: 'M0004994' + }, + { + col1: '00022509', + col2: 'H5050', + col3: '017', + col4: '0', + col5: '1482814', + col6: 'M0004771' + }, + { + col1: '00022509', + col2: 'H5050', + col3: '802', + col4: '0', + col5: '795085', + col6: 'M0374010' + }, + { + col1: '00022509', + col2: 'H5050', + col3: '802', + col4: '0', + col5: '848164', + col6: 'M0023901' + }, + { + col1: '00022225', + col2: 'S5660', + col3: '801', + col4: '0', + col5: '2200177', + col6: 'M0006031' + }, + { + col1: '00022509', + col2: 'H5050', + col3: '021', + col4: '0', + col5: '1653166', + col6: 'M0023901' + }, + { + col1: '00022225', + col2: 'S5660', + col3: '803', + col4: '0', + col5: '1720881', + col6: 'M0004994' + }, + { + col1: '00022509', + col2: 'H5050', + col3: '009', + col4: '0', + col5: '1745108', + col6: 'M0023901' + }, + { + col1: '00022509', + col2: 'H5050', + col3: '022', + col4: '0', + col5: '795085', + col6: 'M0005335' + }, + { + col1: '00022225', + col2: 'S5660', + col3: '804', + col4: '0', + col5: '545293', + col6: 'M0019437' + }, + { + col1: '00022509', + col2: 'H5050', + col3: '004', + col4: '0', + col5: '795085', + col6: 'M0001750' + }, + { + col1: '00022509', + col2: 'H5050', + col3: '801', + col4: '0', + col5: '1653166', + col6: 'M0004771' + }, + { + col1: '00022509', + col2: 'H5050', + col3: '019', + col4: '0', + col5: '795085', + col6: 'M0374010' + } + ], + count: 16, + columns: [ + 'col1', + 'col2', + 'col3', + 'col4', + 'col5', + 'col6' + ], + limit: 20, + offset: 0, + schema: { + '7d48577d-2944-56f7-bb48-2f8272e87007': { + fields: { + col1: { + type: 'text', + mysql_type: 'text' + }, + col2: { + type: 'text', + mysql_type: 'text' + }, + col3: { + type: 'text', + mysql_type: 'text' + }, + col4: { + type: 'text', + mysql_type: 'text' + }, + col5: { + type: 'int', + length: 11, + mysql_type: 'int', + description: 'Column 5' + }, + col6: { + type: 'text', + mysql_type: 'text' + } + } + } + }, + conditions: undefined, + properties: undefined, + setProperties: vi.fn(), + setGroupings: vi.fn(), + setResource: vi.fn(), + setRootUrl: vi.fn(), + setLimit: vi.fn(), + setOffset: vi.fn(), + setConditions: vi.fn(), + setSort: vi.fn(), + setManual: vi.fn(), + setRequireConditions: vi.fn(), + fetchData: vi.fn() +} + +const datasetContextProviderValue = { + data: mockDatasetResponse, + error: null, + isLoading: true, + setDatasetState: vi.fn(), + resetDatasetState: vi.fn() +} + +let datasetState +const renderComponent = ({ + contextValue = datasetContextProviderValue, + resource = filteredDatasetResource, + getStateValue = (data) => { + datasetState = data + } +}) => { + return render( + + + + + + ) +} + +const FilteredDatasetResourceTestingComponent = ({ getStateValue }) => { + getStateValue(useContext(FilteredDispatch)) +} + +describe('FilteredDatasetResource component', () => { + beforeEach(() => { + datasetState = null + + vi.spyOn(useDatastore, 'default').mockImplementation(() => mockUseDataStoreDefaultValue) + }) + + it('Matches snapshot', () => { + const renderedFilteredDatasetResource = renderer.create( + + + Dataset content goes here + + + ).toJSON() + + expect(renderedFilteredDatasetResource).toMatchSnapshot() + }) + + it('Dataset state columns are correct', () => { + renderComponent({}) + + expect(datasetState.filteredResource.columns).toEqual(['col1', 'col2', 'col3', 'col4', 'col5', 'col6']) + }) + + it('Dataset state columns are empty if useDatastore returns none', () => { + vi.spyOn(useDatastore, 'default').mockImplementation(() => ({ + loading: false, + values: [], + count: 0, + columns: [], + limit: 20, + offset: 0, + schema: {}, + conditions: undefined, + properties: undefined, + setProperties: vi.fn(), + setGroupings: vi.fn(), + setResource: vi.fn(), + setRootUrl: vi.fn(), + setLimit: vi.fn(), + setOffset: vi.fn(), + setConditions: vi.fn(), + setSort: vi.fn(), + setManual: vi.fn(), + setRequireConditions: vi.fn(), + fetchData: vi.fn() + })) + + const mockResource = Object.assign({}, filteredDatasetResource) + delete mockResource.identifier + + renderComponent({ + resource: mockResource + }) + + expect(datasetState.filteredResource.columns).toEqual([]) + }) + + it('Dataset columns are properly updated when filtered', async () => { + renderComponent({}) + + // Filter the columns + await act(async () => { + datasetState.filteredResource.loading = true // This is just for full test coverage + datasetState.setVisCol(['col1', 'col2']) + }) + + const filteredTableColumns = [] + datasetState.filteredTable.columns.forEach((column) => { + filteredTableColumns.push(column.id) + }) + + expect(datasetState.visCol).toEqual(['col1', 'col2']) + expect(filteredTableColumns).toEqual(['col1', 'col2']) + + // Reset the columns + await act(async () => { + datasetState.filteredResource.loading = false // This is just for full test coverage + datasetState.resetVisibility() + }) + + const resetTableColumns = [] + datasetState.filteredTable.columns.forEach((column) => { + resetTableColumns.push(column.id) + }) + + expect(datasetState.visCol).toEqual(['col1', 'col2', 'col3', 'col4', 'col5', 'col6']) + expect(resetTableColumns).toEqual(['col1', 'col2', 'col3', 'col4', 'col5', 'col6']) + }) + + it('Data table rows are properly filtered when triggered', () => { + renderComponent({}) + + // This is just to get full test coverage + const modifiedRows = {...datasetState.filteredTable.rows} + modifiedRows[1].values['col6'] = undefined + + // Filter table rows at column 6 with 'M0023901' filter - uses 'starts with' + const filteredRows = datasetState.filteredTable.filterTypes.text(datasetState.filteredTable.rows, 'col6', 'M0023901') + const expectedRowIds = ['0', '1', '6', '8', '10'] + const actualRowIds = [] + + filteredRows.forEach((row) => { + actualRowIds.push(row.id) + }) + + expect(filteredRows.length).toEqual(5) + expect(actualRowIds).toEqual(expectedRowIds) + }) +}) diff --git a/src/components/dataset/DatasetResource/__snapshots__/DatasetResource.test.js.snap b/src/components/dataset/DatasetResource/__snapshots__/DatasetResource.test.js.snap new file mode 100644 index 00000000..58ca5614 --- /dev/null +++ b/src/components/dataset/DatasetResource/__snapshots__/DatasetResource.test.js.snap @@ -0,0 +1,3841 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DatasetResource component. Matches snapshot. 1`] = ` + +
+
+
+
+
+

+ Viewing + + 1 + + - + + 16 + + of + + 16 + + rows +

+
+
+
+ + + +
+
+ + + +
+ +
+
+

+ Manage columns +

+ +
+
+
+
+ + Display column + + + Reorder + +
+

+ Activate the reorder button and use the arrow keys to reorder the list or use your mouse to drag/reorder. Press escape to cancel the reordering. +

+
    +
  • +
    +
    + + +
    +
    + +
  • +
  • +
    +
    + + +
    +
    + +
  • +
  • +
    +
    + + +
    +
    + +
  • +
  • +
    +
    + + +
    +
    + +
  • +
  • +
    +
    + + +
    +
    + +
  • +
  • +
    +
    + + +
    +
    + +
  • +
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
+
+
+ + + +
+
+
+ + + +
+ +
+
+

+ + Dataset Explorer + +

+ +
+
+
+
+
+

+ +

+
+
+
+ A dataset showing all Medicare plan information from CMS. +
+
+
+ + Last Modified + + : November 29, 2021 +
+ + • + +
+ + Released + + : November 29, 2021 +
+
+
+
+
+
+
+
+
+

+ Viewing + + 1 + + - + + 16 + + of + + 16 + + rows +

+
+
+
+ + + +
+
+ + + +
+ +
+
+

+ Manage columns +

+ +
+
+
+
+ + Display column + + + Reorder + +
+

+ Activate the reorder button and use the arrow keys to reorder the list or use your mouse to drag/reorder. Press escape to cancel the reordering. +

+
    +
  • +
    +
    + + +
    +
    + +
  • +
  • +
    +
    + + +
    +
    + +
  • +
  • +
    +
    + + +
    +
    + +
  • +
  • +
    +
    + + +
    +
    + +
  • +
  • +
    +
    + + +
    +
    + +
  • +
  • +
    +
    + + +
    +
    + +
  • +
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+

+ Activate the column resize button and use the right and left arrow keys to resize a column or use your mouse to drag/resize. Press escape to cancel the resizing. +

+
+
+
+
+
+
+
+
+ + col1 + + + + + +
+
+ + col2 + + + + + +
+
+ + col3 + + + + + +
+
+ + col4 + + + + + +
+
+ + Column 5 + + + + + +
+
+ + col6 + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 022 +
+
+ 0 +
+
+ 1745108 +
+
+ M0023901 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 804 +
+
+ 0 +
+
+ 545293 +
+
+ M0004994 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 804 +
+
+ 0 +
+
+ 545293 +
+
+ M0012387 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 806 +
+
+ 0 +
+
+ 1251596 +
+
+ M0004994 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 017 +
+
+ 0 +
+
+ 1482814 +
+
+ M0004771 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 802 +
+
+ 0 +
+
+ 795085 +
+
+ M0374010 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 802 +
+
+ 0 +
+
+ 848164 +
+
+ M0023901 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 801 +
+
+ 0 +
+
+ 2200177 +
+
+ M0006031 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 021 +
+
+ 0 +
+
+ 1653166 +
+
+ M0023901 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 803 +
+
+ 0 +
+
+ 1720881 +
+
+ M0004994 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 009 +
+
+ 0 +
+
+ 1745108 +
+
+ M0023901 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 022 +
+
+ 0 +
+
+ 795085 +
+
+ M0005335 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 804 +
+
+ 0 +
+
+ 545293 +
+
+ M0019437 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 004 +
+
+ 0 +
+
+ 795085 +
+
+ M0001750 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 801 +
+
+ 0 +
+
+ 1653166 +
+
+ M0004771 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 019 +
+
+ 0 +
+
+ 795085 +
+
+ M0374010 +
+
+
+
+
+
+
+
+
+
+ +
+
+ + + Page + + + 1 + + + of + + + 1 + + + for Medicare Plan Info Data + + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Activate the column resize button and use the right and left arrow keys to resize a column or use your mouse to drag/resize. Press escape to cancel the resizing. +

+
+
+
+
+
+
+
+
+ + col1 + + + + + +
+
+ + col2 + + + + + +
+
+ + col3 + + + + + +
+
+ + col4 + + + + + +
+
+ + Column 5 + + + + + +
+
+ + col6 + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 022 +
+
+ 0 +
+
+ 1745108 +
+
+ M0023901 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 804 +
+
+ 0 +
+
+ 545293 +
+
+ M0004994 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 804 +
+
+ 0 +
+
+ 545293 +
+
+ M0012387 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 806 +
+
+ 0 +
+
+ 1251596 +
+
+ M0004994 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 017 +
+
+ 0 +
+
+ 1482814 +
+
+ M0004771 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 802 +
+
+ 0 +
+
+ 795085 +
+
+ M0374010 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 802 +
+
+ 0 +
+
+ 848164 +
+
+ M0023901 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 801 +
+
+ 0 +
+
+ 2200177 +
+
+ M0006031 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 021 +
+
+ 0 +
+
+ 1653166 +
+
+ M0023901 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 803 +
+
+ 0 +
+
+ 1720881 +
+
+ M0004994 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 009 +
+
+ 0 +
+
+ 1745108 +
+
+ M0023901 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 022 +
+
+ 0 +
+
+ 795085 +
+
+ M0005335 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 804 +
+
+ 0 +
+
+ 545293 +
+
+ M0019437 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 004 +
+
+ 0 +
+
+ 795085 +
+
+ M0001750 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 801 +
+
+ 0 +
+
+ 1653166 +
+
+ M0004771 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 019 +
+
+ 0 +
+
+ 795085 +
+
+ M0374010 +
+
+
+
+
+
+
+
+
+
+ +
+
+ + + Page + + + 1 + + + of + + + 1 + + + for Medicare Plan Info Data + + +
+
+ +
+
+
+
+
+ +`; diff --git a/src/components/dataset/DatasetResource/__snapshots__/DatasetResource.test.jsx.snap b/src/components/dataset/DatasetResource/__snapshots__/DatasetResource.test.jsx.snap new file mode 100644 index 00000000..9227bebb --- /dev/null +++ b/src/components/dataset/DatasetResource/__snapshots__/DatasetResource.test.jsx.snap @@ -0,0 +1,4115 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`DatasetResource component. > Matches snapshot. 1`] = ` + +
+
+
+
+
+

+ Viewing + + 1 + + - + + 16 + + of + + 16 + + rows +

+
+
+
+ + + +
+
+ + + +
+ +
+
+

+ Manage columns +

+ +
+
+
+
+ + Display column + + + Reorder + +
+

+ Activate the reorder button and use the arrow keys to reorder the list or use your mouse to drag/reorder. Press escape to cancel the reordering. +

+
    +
  • +
    +
    + + +

    +

    +
    + +
  • +
  • +
    +
    + + +

    +

    +
    + +
  • +
  • +
    +
    + + +

    +

    +
    + +
  • +
  • +
    +
    + + +

    +

    +
    + +
  • +
  • +
    +
    + + +

    +

    +
    + +
  • +
  • +
    +
    + + +

    +

    +
    + +
  • +
+
+
+
+
+
+
+ + +

+

+
+ +
+
+
+
+
+
+
+
+
+ + + +
+
+
+ + + +
+ +
+
+

+ + Dataset Explorer + +

+ +
+
+
+
+
+

+ +

+
+
+
+ A dataset showing all Medicare plan information from CMS. +
+
+
+ + + Last Modified + + : November 29, 2021 + +
+ + +
+
+ + • + +
+ + + Released + + : November 29, 2021 + +
+ + +
+
+
+
+
+
+
+
+
+
+

+ Viewing + + 1 + + - + + 16 + + of + + 16 + + rows +

+
+
+
+ + + +
+
+ + + +
+ +
+
+

+ Manage columns +

+ +
+
+
+
+ + Display column + + + Reorder + +
+

+ Activate the reorder button and use the arrow keys to reorder the list or use your mouse to drag/reorder. Press escape to cancel the reordering. +

+
    +
  • +
    +
    + + +

    +

    +
    + +
  • +
  • +
    +
    + + +

    +

    +
    + +
  • +
  • +
    +
    + + +

    +

    +
    + +
  • +
  • +
    +
    + + +

    +

    +
    + +
  • +
  • +
    +
    + + +

    +

    +
    + +
  • +
  • +
    +
    + + +

    +

    +
    + +
  • +
+
+
+
+
+
+
+ + +

+

+
+ +
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+

+ Activate the column resize button and use the right and left arrow keys to resize a column or use your mouse to drag/resize. Press escape to cancel the resizing. +

+
+
+
+
+
+
+
+
+ + + col1 + + + + + + +
+
+ + + col2 + + + + + + +
+
+ + + col3 + + + + + + +
+
+ + + col4 + + + + + + +
+
+ + + Column 5 + + + + + + +
+
+ + + col6 + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 022 +
+
+ 0 +
+
+ 1745108 +
+
+ M0023901 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 804 +
+
+ 0 +
+
+ 545293 +
+
+ M0004994 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 804 +
+
+ 0 +
+
+ 545293 +
+
+ M0012387 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 806 +
+
+ 0 +
+
+ 1251596 +
+
+ M0004994 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 017 +
+
+ 0 +
+
+ 1482814 +
+
+ M0004771 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 802 +
+
+ 0 +
+
+ 795085 +
+
+ M0374010 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 802 +
+
+ 0 +
+
+ 848164 +
+
+ M0023901 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 801 +
+
+ 0 +
+
+ 2200177 +
+
+ M0006031 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 021 +
+
+ 0 +
+
+ 1653166 +
+
+ M0023901 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 803 +
+
+ 0 +
+
+ 1720881 +
+
+ M0004994 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 009 +
+
+ 0 +
+
+ 1745108 +
+
+ M0023901 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 022 +
+
+ 0 +
+
+ 795085 +
+
+ M0005335 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 804 +
+
+ 0 +
+
+ 545293 +
+
+ M0019437 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 004 +
+
+ 0 +
+
+ 795085 +
+
+ M0001750 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 801 +
+
+ 0 +
+
+ 1653166 +
+
+ M0004771 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 019 +
+
+ 0 +
+
+ 795085 +
+
+ M0374010 +
+
+
+
+
+
+
+
+
+
+ +
+
+ + + Page + + + 1 + + + of + + + 1 + + + for Medicare Plan Info Data + + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Activate the column resize button and use the right and left arrow keys to resize a column or use your mouse to drag/resize. Press escape to cancel the resizing. +

+
+
+
+
+
+
+
+
+ + + col1 + + + + + + +
+
+ + + col2 + + + + + + +
+
+ + + col3 + + + + + + +
+
+ + + col4 + + + + + + +
+
+ + + Column 5 + + + + + + +
+
+ + + col6 + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 022 +
+
+ 0 +
+
+ 1745108 +
+
+ M0023901 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 804 +
+
+ 0 +
+
+ 545293 +
+
+ M0004994 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 804 +
+
+ 0 +
+
+ 545293 +
+
+ M0012387 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 806 +
+
+ 0 +
+
+ 1251596 +
+
+ M0004994 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 017 +
+
+ 0 +
+
+ 1482814 +
+
+ M0004771 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 802 +
+
+ 0 +
+
+ 795085 +
+
+ M0374010 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 802 +
+
+ 0 +
+
+ 848164 +
+
+ M0023901 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 801 +
+
+ 0 +
+
+ 2200177 +
+
+ M0006031 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 021 +
+
+ 0 +
+
+ 1653166 +
+
+ M0023901 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 803 +
+
+ 0 +
+
+ 1720881 +
+
+ M0004994 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 009 +
+
+ 0 +
+
+ 1745108 +
+
+ M0023901 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 022 +
+
+ 0 +
+
+ 795085 +
+
+ M0005335 +
+
+
+
+ 00022225 +
+
+ S5660 +
+
+ 804 +
+
+ 0 +
+
+ 545293 +
+
+ M0019437 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 004 +
+
+ 0 +
+
+ 795085 +
+
+ M0001750 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 801 +
+
+ 0 +
+
+ 1653166 +
+
+ M0004771 +
+
+
+
+ 00022509 +
+
+ H5050 +
+
+ 019 +
+
+ 0 +
+
+ 795085 +
+
+ M0374010 +
+
+
+
+
+
+
+
+
+
+ +
+
+ + + Page + + + 1 + + + of + + + 1 + + + for Medicare Plan Info Data + + +
+
+ +
+
+
+
+
+ +`; diff --git a/src/components/dataset/DatasetResource/__snapshots__/FilteredDatasetResource.test.js.snap b/src/components/dataset/DatasetResource/__snapshots__/FilteredDatasetResource.test.js.snap new file mode 100644 index 00000000..3e688257 --- /dev/null +++ b/src/components/dataset/DatasetResource/__snapshots__/FilteredDatasetResource.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FilteredDatasetResource component Matches snapshot 1`] = `"Dataset content goes here"`; diff --git a/src/components/dataset/DatasetResource/__snapshots__/FilteredDatasetResource.test.jsx.snap b/src/components/dataset/DatasetResource/__snapshots__/FilteredDatasetResource.test.jsx.snap new file mode 100644 index 00000000..89e8b3ee --- /dev/null +++ b/src/components/dataset/DatasetResource/__snapshots__/FilteredDatasetResource.test.jsx.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`FilteredDatasetResource component > Matches snapshot 1`] = `"Dataset content goes here"`; diff --git a/src/components/dataset/FilterDataset/AddFilter.jsx b/src/components/dataset/FilterDataset/AddFilter.jsx new file mode 100644 index 00000000..38cd4bb1 --- /dev/null +++ b/src/components/dataset/FilterDataset/AddFilter.jsx @@ -0,0 +1,22 @@ +import React from 'react' +import PropTypes from 'prop-types' +import FontAwesomePro from '../../common/FontAwesomePro/FontAwesomePro' + +const AddFilter = ({ addFilter }) => { + return ( + + ) +} + +AddFilter.propTypes = { + /** + * Callback function to set filter + */ + addFilter: PropTypes.func +} + +AddFilter.displayName = 'AddFilter' +export default AddFilter diff --git a/src/components/dataset/FilterDataset/AddFilter.test.jsx b/src/components/dataset/FilterDataset/AddFilter.test.jsx new file mode 100644 index 00000000..f86fde2b --- /dev/null +++ b/src/components/dataset/FilterDataset/AddFilter.test.jsx @@ -0,0 +1,25 @@ +import { render, fireEvent, screen } from '@testing-library/react' +import renderer from 'react-test-renderer' +import AddFilter from './AddFilter' + +describe('AddFilter component.', () => { + it('Matches snapshot.', () => { + const renderedAddFilter = renderer.create( + {}} /> + ).toJSON() + + expect(renderedAddFilter).toMatchSnapshot() + }) + + it('\'addFilter\' prop is called when button is clicked.', () => { + const addFilter = vi.fn() + + render( + + ) + + fireEvent.click(screen.getByRole('button', { name: 'Add filter' })) + + expect(addFilter).toHaveBeenCalled() + }) +}) diff --git a/src/components/dataset/FilterDataset/DeleteFilter.jsx b/src/components/dataset/FilterDataset/DeleteFilter.jsx new file mode 100644 index 00000000..454c9a01 --- /dev/null +++ b/src/components/dataset/FilterDataset/DeleteFilter.jsx @@ -0,0 +1,35 @@ +import React from 'react' +import FontAwesomePro from '../../common/FontAwesomePro/FontAwesomePro' +import PropTypes from 'prop-types' + +const DeleteButton = ({ deleteFilter, enabled, i }) => { + const deleteClass = enabled ? 'delete-button' : 'delete-button disabled' + + return ( + + ) +} + +DeleteButton.propTypes = { + /** + * Callback function to delete filter. + * Passes `i` prop value as an argument + */ + deleteFilter: PropTypes.func, + /** + * `true` enables the button. + * `false` disables the button + */ + enabled: PropTypes.bool, + /** + * Index value used for identifying the + * correct filter to delete. Passed through + * deleteFilter callback as an argument + */ + i: PropTypes.number +} + +DeleteButton.displayName = 'DeleteButton' +export default DeleteButton diff --git a/src/components/dataset/FilterDataset/DeleteFilter.test.jsx b/src/components/dataset/FilterDataset/DeleteFilter.test.jsx new file mode 100644 index 00000000..48c6720f --- /dev/null +++ b/src/components/dataset/FilterDataset/DeleteFilter.test.jsx @@ -0,0 +1,35 @@ +import { render, fireEvent, screen } from '@testing-library/react' +import renderer from 'react-test-renderer' +import DeleteButton from './DeleteFilter' + +const componentArgs = { + enabled: false, + i: 0, + deleteFilter: vi.fn() +} + +describe('DeleteButton component.', () => { + it('Matches snapshot.', () => { + const renderedDeleteButton = renderer.create( + + ).toJSON() + + expect(renderedDeleteButton).toMatchSnapshot() + }) + + it('\'deleteFilter\' prop is called when button is clicked.', () => { + const deleteFilter = vi.fn() + + render( + + ) + + fireEvent.click(screen.getByRole('button', { name: 'delete filter' })) + + expect(deleteFilter).toHaveBeenCalled() + }) +}) diff --git a/src/components/dataset/FilterDataset/FilterChip.jsx b/src/components/dataset/FilterDataset/FilterChip.jsx new file mode 100644 index 00000000..57803d42 --- /dev/null +++ b/src/components/dataset/FilterDataset/FilterChip.jsx @@ -0,0 +1,140 @@ +import React from 'react' +import PropTypes from 'prop-types' +import FontAwesomePro from '../../common/FontAwesomePro/FontAwesomePro' + +/* + wrapping unicode selectors in span tags cause text decoration underline breaks + The quick solution appears to return entire jsx terms instead of trying to template it. +*/ + +const FilterChip = ({ filter, deleteFilter, i }) => { + const { condition, value, column } = filter + + switch (condition) { + case '=': + return ( + + ) + case '<>': + return ( + + ) + case '>': + return ( + + ) + case '<': + return ( + + ) + case 'LIKE': + return ( + + ) + case 'starts with': + return ( + + ) + case 'is_empty': + return ( + + ) + case 'not_empty': + return ( + + ) + default: + return ( + + ) + } +} + +FilterChip.propTypes = { + /** + * Filter data + */ + filter: PropTypes.shape({ + column: PropTypes.string, + condition: PropTypes.oneOf([ + '=', + '<>', + '>', + '<', + 'LIKE', + 'starts with', + 'is_empty', + 'not_empty', + '' // We need to allow an empty case because there are initially no filters so there is no condition set which throws a type error + ]), + value: PropTypes.string + }), + /** + * Callback function to delete filter. + * Passes `i` prop value as an argument + */ + deleteFilter: PropTypes.func, + /** + * Index value used for identifying the + * correct filter to delete. Passed through + * deleteFilter callback as an argument + */ + i: PropTypes.number +} + +FilterChip.displayName = 'FilterChip' +export default FilterChip diff --git a/src/components/dataset/FilterDataset/FilterChip.scss b/src/components/dataset/FilterDataset/FilterChip.scss new file mode 100644 index 00000000..06570a0b --- /dev/null +++ b/src/components/dataset/FilterDataset/FilterChip.scss @@ -0,0 +1,51 @@ +@use "../../../scss/modules/modules"; +@use "../../../scss/modules/colormap"; + +.chip-list { + display: flex; + flex-direction: row; + flex-wrap: wrap; + margin-bottom: 12px; + + button { + display: flex; + flex-direction: row; + align-items: center; + height: 32px; + background-color: #fff; + color: colormap.$charcoal; + border: solid 1px colormap.$charcoal-10; + border-radius: 20px; + padding: 8px; + font-family: Muli; + font-weight: 300; + font-size: 12px; + margin: 8px 8px 8px 0; + + &:focus { + outline: 2px dotted #84898f; + outline-offset: 2px; + } + + &.chip-body { + &:hover { + text-decoration: underline; + } + } + + &.clear-all { + color: #0c2499; + padding: 0 8px; + border: 0; + background-color: transparent; + + &:hover { + text-decoration: underline; + } + } + + svg { + margin: 0px 8px 0px 8px; + } + } +} diff --git a/src/components/dataset/FilterDataset/FilterChip.test.jsx b/src/components/dataset/FilterDataset/FilterChip.test.jsx new file mode 100644 index 00000000..9466e916 --- /dev/null +++ b/src/components/dataset/FilterDataset/FilterChip.test.jsx @@ -0,0 +1,90 @@ +import { render, fireEvent, screen } from '@testing-library/react' +import renderer from 'react-test-renderer' +import FilterChip from './FilterChip' + +const componentArgs = { + filter: { + column: 'col4', + condition: 'is_empty', + value: '' + }, + i: 0, + deleteFilter: vi.fn() +} + +const conditions = [ + { + condition: '=', + text: 'equals' + }, + { + condition: '<>', + text: 'not equal to' + }, + { + condition: '>', + text: 'greater than' + }, + { + condition: '<', + text: 'less than' + }, + { + condition: 'LIKE', + text: 'contains' + }, + { + condition: 'starts with', + text: 'starts with' + }, + { + condition: 'is_empty', + text: 'is empty' + }, + { + condition: 'not_empty', + text: 'not empty' + }, + { + condition: '', + text: 'contains' + } +] + +describe('FilterChip component.', () => { + it('Matches snapshot.', () => { + const renderedFilterChip = renderer.create( + + ).toJSON() + + expect(renderedFilterChip).toMatchSnapshot() + }) + + conditions.forEach((condition) => { + const conditionName = condition.condition === '' ? 'Default \'contains\'' : condition.condition + + it(`'${conditionName}' condition chip is rendered.`, () => { + const deleteFilter = vi.fn() + + render( + + ) + + const buttonName = `Remove col4 ${condition.text}${condition.condition !== 'is_empty' && condition.condition !== 'not_empty' ? ' test' : ''} filter` + + expect(screen.queryByRole('button', { name: buttonName })).toBeInTheDocument() + + fireEvent.click(screen.getByRole('button', { name: buttonName })) + + expect(deleteFilter).toHaveBeenCalled() + }) + }) +}) diff --git a/src/components/dataset/FilterDataset/FilterChipList.jsx b/src/components/dataset/FilterDataset/FilterChipList.jsx new file mode 100644 index 00000000..770591dd --- /dev/null +++ b/src/components/dataset/FilterDataset/FilterChipList.jsx @@ -0,0 +1,95 @@ +import React from 'react' +import FilterChip from './FilterChip' +import FontAwesomePro from '../../common/FontAwesomePro/FontAwesomePro' +import PropTypes from 'prop-types' +import './FilterChip.scss' + +const FilterChipList = ({ + filters, + deleteFilter, + resetFilters, + hidden, + resetHiddenColumns, + reordered, + resetColumnOrder +}) => { + const showReset = filters.length > 0 || hidden || reordered + const applyReset = () => { + resetHiddenColumns() + resetColumnOrder() + resetFilters() + } + // lets avoid displaying filters if the only one there is our empty starter filter + return ( +
+ {filters && filters.map((filter, i) => ( + + ))} + {hidden && ( + + )} + {reordered && ( + + )} + {showReset && ( + + )} +
+ ) +} + +FilterChipList.propTypes = { + /** + * Array of filters + */ + filters: PropTypes.arrayOf(PropTypes.shape({ + column: PropTypes.string, + condition: PropTypes.oneOf([ + '=', + '<>', + '>', + '<', + 'LIKE', + 'starts with', + 'is_empty', + 'not_empty', + '' // We need to allow an empty case because there are initially no filters so there is no condition set which throws a type error + ]), + value: PropTypes.string + })), + /** + * `true` if some columns have been hidden + * as a result of a filter + */ + hidden: PropTypes.bool, + /** + * `true` if columns have been reordered + */ + reordered: PropTypes.bool, + /** + * Callback function to delete filter + */ + deleteFilter: PropTypes.func, + /** + * Callback function to reset the original column order + */ + resetColumnOrder: PropTypes.func, + /** + * Callback function to reset hidden columns + */ + resetHiddenColumns: PropTypes.func, + /** + * Callback function to remove all applied filters + */ + resetFilters: PropTypes.func +} +FilterChipList.displayName = 'FilterChipList' +export default FilterChipList diff --git a/src/components/dataset/FilterDataset/FilterChipList.test.jsx b/src/components/dataset/FilterDataset/FilterChipList.test.jsx new file mode 100644 index 00000000..564af05d --- /dev/null +++ b/src/components/dataset/FilterDataset/FilterChipList.test.jsx @@ -0,0 +1,118 @@ +import { render, fireEvent, screen } from '@testing-library/react' +import renderer from 'react-test-renderer' +import FilterChipList from './FilterChipList' + +const componentArgs = { + filters: [ + { + column: 'col4', + condition: 'is_empty', + value: '' + }, + { + column: 'col2', + condition: 'LIKE', + value: '%medicare%' + } + ], + hidden: false, + reordered: false, + deleteFilter: vi.fn(), + resetColumnOrder: vi.fn(), + resetHiddenColumns: vi.fn(), + resetFilters: vi.fn() +} + +describe('FilterChipList component.', () => { + it('Matches snapshot.', () => { + const renderedFilterChipList = renderer.create( + + ).toJSON() + + expect(renderedFilterChipList).toMatchSnapshot() + }) + + it('Renders the \'Columns hidden\' chip when \'hidden\' prop is set.', () => { + const resetHiddenColumns = vi.fn() + + render( +