From 4be609575860a8e0109976bca7180c3ca56f3c8f Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 3 Feb 2021 18:14:42 -0500 Subject: [PATCH] Adds the components needed for dataset search pages (#5) * Add basic search page template with search list item * Add facets and pagination to dataset search page * Add DatasetSearchListItem tests * Add tests to DatasetSearchFacets * Add some basic tests to DatasetSearch and a mocks folder for axios mocking --- __mocks__/styleMock.js | 1 + package.json | 1 + .../dataset_search_facets.test.jsx | 48 ++++++ src/components/DatasetSearchFacets/index.jsx | 59 +++++++ .../datasetsearchlistitem.test.jsx | 30 ++++ .../DatasetSearchListItem/index.jsx | 50 ++++++ src/components/Pagination/index.jsx | 15 +- src/index.js | 1 + .../components/dataset-search-facets.scss | 3 + .../components/dataset-search-list-item.scss | 14 ++ src/styles/scss/components/index.scss | 4 +- src/styles/scss/templates/dataset-search.scss | 28 ++++ src/styles/scss/templates/index.scss | 1 + .../DatasetSearch/datasetsearch.test.jsx | 62 ++++++++ src/templates/DatasetSearch/index.jsx | 148 ++++++++++++++++++ 15 files changed, 461 insertions(+), 4 deletions(-) create mode 100644 __mocks__/styleMock.js create mode 100644 src/components/DatasetSearchFacets/dataset_search_facets.test.jsx create mode 100644 src/components/DatasetSearchFacets/index.jsx create mode 100644 src/components/DatasetSearchListItem/datasetsearchlistitem.test.jsx create mode 100644 src/components/DatasetSearchListItem/index.jsx create mode 100644 src/styles/scss/components/dataset-search-facets.scss create mode 100644 src/styles/scss/components/dataset-search-list-item.scss create mode 100644 src/styles/scss/templates/dataset-search.scss create mode 100644 src/templates/DatasetSearch/datasetsearch.test.jsx create mode 100644 src/templates/DatasetSearch/index.jsx diff --git a/__mocks__/styleMock.js b/__mocks__/styleMock.js new file mode 100644 index 00000000..f053ebf7 --- /dev/null +++ b/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/package.json b/package.json index 251968e8..0783a0d2 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "prepublish": "npm run lib", "test": "jest --verbose", "test:watch": "npm run test -- --watch", + "test:coverage": "npm run test -- --coverage", "docz:dev": "docz dev", "docz:build": "docz build", "docz:serve": "docz build && docz serve", diff --git a/src/components/DatasetSearchFacets/dataset_search_facets.test.jsx b/src/components/DatasetSearchFacets/dataset_search_facets.test.jsx new file mode 100644 index 00000000..d9879348 --- /dev/null +++ b/src/components/DatasetSearchFacets/dataset_search_facets.test.jsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import DatasetSearchFacets from './index'; +import { isSelected } from './index'; + +const testFacets = [{type: 'theme', name: 'facet-1', total: '2'}, {type: 'theme', name: 'facet-2', total: '3'}] + +describe('isSelected Function', () => { + test('returns -1 if not selected', () => { + expect(isSelected('dkan', [''])).toEqual(-1); + }); + test('returns correct index if item in array', () => { + expect(isSelected('dkan', ['dkan'])).toEqual(0); + expect(isSelected('react', ['dkan', 'react'])).toEqual(1); + }); +}) + + +describe('', () => { + test('Renders correctly', () => { + render( ({})} />); + expect(screen.getByRole('button', { name: "Facets" })).toBeInTheDocument(); + expect(screen.getByLabelText('facet-1 (2)')).toBeInTheDocument(); + expect(screen.getByLabelText('facet-2 (3)')).toBeInTheDocument(); + expect(screen.getAllByRole('checkbox')).toHaveLength(2); + }); + test('Opens and closes, hiding facets', () => { + render( ({})} />); + expect(screen.getAllByRole('checkbox')).toHaveLength(2); + fireEvent.click(screen.getByRole('button', { name: "Facets" })); + expect(screen.queryByLabelText('facet-1 (2)')).toBeNull(); + expect(screen.queryAllByRole('checkbox')).toHaveLength(0); + fireEvent.click(screen.getByRole('button', { name: "Facets" })); + expect(screen.getAllByRole('checkbox')).toHaveLength(2); + }); + test('Checkbox fires onclickFunction', () => { + const handleClick = jest.fn() + render(); + fireEvent.click(screen.getByRole('checkbox', { name: 'facet-1 (2)'})); + expect(handleClick).toHaveBeenCalledTimes(1) + }); + test('Checkbox is checked if in selectedFacets array', () => { + const handleClick = jest.fn() + render(); + expect(screen.getByRole('checkbox', { name: 'facet-1 (2)', checked: true})).toBeInTheDocument() + }); +}); diff --git a/src/components/DatasetSearchFacets/index.jsx b/src/components/DatasetSearchFacets/index.jsx new file mode 100644 index 00000000..f514a848 --- /dev/null +++ b/src/components/DatasetSearchFacets/index.jsx @@ -0,0 +1,59 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Choice, Button } from '@cmsgov/design-system' + +export function isSelected(currentFacet, selectedFacets) { + let isSelected = -1; + if(selectedFacets) { + isSelected = selectedFacets.findIndex((s) => s === currentFacet); + } + return isSelected; +} + +const DatasetSearchFacets = ({ title, facets, onclickFunction, selectedFacets }) => { + const [isOpen, setIsOpen] = useState(true); + const filteredFacets = facets.filter((f) => (f.total > 0)); + + + return ( +
+ + {isOpen + &&( +
    + {facets.map((f) => { + return (
  • + -1 ? true : false} + name={`facet_theme_${f.name}`} + type="checkbox" + label={`${f.name} (${f.total})`} + value={f.name} + onClick={(e) => onclickFunction({key: f.type, value: e.target.value})} + /> +
  • ) + })} +
+ ) + } +
+ ); +} + +DatasetSearchFacets.propTypes = { + title: PropTypes.string.isRequired, + facets: PropTypes.arrayOf(PropTypes.shape({ + type: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + total: PropTypes.string.isRequired, + })).isRequired, + onclickFunction: PropTypes.func.isRequired, +} + +export default DatasetSearchFacets; diff --git a/src/components/DatasetSearchListItem/datasetsearchlistitem.test.jsx b/src/components/DatasetSearchListItem/datasetsearchlistitem.test.jsx new file mode 100644 index 00000000..33ef553d --- /dev/null +++ b/src/components/DatasetSearchListItem/datasetsearchlistitem.test.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { render, screen, within } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import DatasetSearchListItem from './index'; + +const singleItem = { + title: "Dataset Title", + modified: "2020-10-22", + description: "This is my description.", + theme: ["dkan"], + keyword: ["my keyword"] +} + +describe('', () => { + test('Renders correctly', () => { + render(); + const listItemOptions = singleItem.theme.concat(singleItem.keyword) + const listItems = screen.getAllByRole('listitem') + listItems.forEach((item, idx) => { + const { getByText } = within(item); + expect(getByText(listItemOptions[idx])).toBeInTheDocument(); + }) + + + expect(screen.getByRole('heading', { name: "Dataset Title" })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: "Dataset Title" })).toBeInTheDocument(); + expect(screen.getByText('Updated October 22, 2020')).toBeInTheDocument(); + expect(screen.getByText('This is my description.')).toBeInTheDocument(); + }); +}); diff --git a/src/components/DatasetSearchListItem/index.jsx b/src/components/DatasetSearchListItem/index.jsx new file mode 100644 index 00000000..dcc5d01e --- /dev/null +++ b/src/components/DatasetSearchListItem/index.jsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Link } from '@reach/router'; +import { Button, Badge } from '@cmsgov/design-system'; + +const DatasetSearchListItem = ({item, updateFacets}) => { + const { title, modified, description, theme, keyword, identifier } = item; + const updatedDate = new Date(modified.replace(/-/g, '\/')) + const dateOptions = {month: 'long', year: 'numeric', day: 'numeric'} + return( +
+
+ {theme && +
    + {theme.map((t) => ( +
  • + {t} +
  • + ))} +
+ } + + Updated {`${updatedDate.toLocaleDateString(undefined, dateOptions)}`} + +
+

+ + {title} + +

+ {/* 215 average character limit */} +

{description}

+
+ {keyword && +
    + {keyword.map((k) => ( +
  • + {k} +
  • + ))} +
+ } +
+
+ ); +} + +export default DatasetSearchListItem; diff --git a/src/components/Pagination/index.jsx b/src/components/Pagination/index.jsx index a8d2a5ce..4ad8ad25 100644 --- a/src/components/Pagination/index.jsx +++ b/src/components/Pagination/index.jsx @@ -3,7 +3,7 @@ import { Button } from '@cmsgov/design-system'; import { usePagination, buildPageArray } from '@civicactions/data-catalog-services'; const Pagination = ({ - currentPage, totalItems, itemsPerPage, gotoPage, + currentPage, totalItems, itemsPerPage, gotoPage, calcByOffset }) => { const { pageIndex, @@ -13,11 +13,20 @@ const Pagination = ({ canGoToNext, goToNext, goToPrevious, - } = usePagination(0, totalItems, itemsPerPage); + setTotalItems, + } = usePagination(currentPage, totalItems, itemsPerPage); const pageButtons = buildPageArray(pageIndex, 2, pages); + useEffect(() => { + setTotalItems(totalItems) + }, [totalItems]) + useEffect(()=> { - gotoPage((Number(pageIndex)) * itemsPerPage) + if (calcByOffset) { + gotoPage((Number(pageIndex)) * itemsPerPage) + } else { + gotoPage((Number(pageIndex))) + } }, [pageIndex]) return (
diff --git a/src/index.js b/src/index.js index c4682fc7..7a83b5db 100644 --- a/src/index.js +++ b/src/index.js @@ -8,4 +8,5 @@ export { default as Pagination } from './components/Pagination'; // Templates export { default as Footer } from './templates/Footer'; export { default as Dataset } from './templates/Dataset'; +export { default as DatasetSearch } from './templates/DatasetSearch'; export { default as DrupalPage } from './templates/DrupalPage'; diff --git a/src/styles/scss/components/dataset-search-facets.scss b/src/styles/scss/components/dataset-search-facets.scss new file mode 100644 index 00000000..ec464652 --- /dev/null +++ b/src/styles/scss/components/dataset-search-facets.scss @@ -0,0 +1,3 @@ +.dc-dataset-search--facets { + list-style: none; +} diff --git a/src/styles/scss/components/dataset-search-list-item.scss b/src/styles/scss/components/dataset-search-list-item.scss new file mode 100644 index 00000000..e6a65a37 --- /dev/null +++ b/src/styles/scss/components/dataset-search-list-item.scss @@ -0,0 +1,14 @@ +@import "~@cmsgov/design-system/dist/scss/settings/variables/color"; + +.dc-dataset-searchlist-item { + + ul { + list-style: none; + } + + .dc-dataset-searchlist-item--keyword { + background: $color-primary-alt-lightest; + border: none; + color: $color-base; + } +} \ No newline at end of file diff --git a/src/styles/scss/components/index.scss b/src/styles/scss/components/index.scss index 58495b6d..69dcc873 100644 --- a/src/styles/scss/components/index.scss +++ b/src/styles/scss/components/index.scss @@ -1,4 +1,6 @@ @import "./datatable.scss"; @import "./dataset-tags.scss"; @import "./dataset-downloads.scss"; -@import "./pagination.scss"; \ No newline at end of file +@import "./pagination.scss"; +@import "./dataset-search-list-item.scss"; +@import "./dataset-search-facets.scss"; \ No newline at end of file diff --git a/src/styles/scss/templates/dataset-search.scss b/src/styles/scss/templates/dataset-search.scss new file mode 100644 index 00000000..0d24f87f --- /dev/null +++ b/src/styles/scss/templates/dataset-search.scss @@ -0,0 +1,28 @@ +@import "~@cmsgov/design-system/dist/scss/settings/variables/color"; + +.dc-dataset-search-list { + list-style: none; +} + +.dc-fulltext--input-container { + width: 100%; + input { + max-width: inherit; + } +} + +.dc-search-header { + display: relative; + &::after { + content: ""; + display: block; + width: 48px; + height: 4px; + margin: 8px 0; + background-color: $color-primary-alt-light; + } +} + +.dataset-results-count { + font-weight: bold; +} \ No newline at end of file diff --git a/src/styles/scss/templates/index.scss b/src/styles/scss/templates/index.scss index 4412f39f..7c6684e8 100644 --- a/src/styles/scss/templates/index.scss +++ b/src/styles/scss/templates/index.scss @@ -1 +1,2 @@ +@import "./dataset-search.scss"; @import "./footer.scss"; diff --git a/src/templates/DatasetSearch/datasetsearch.test.jsx b/src/templates/DatasetSearch/datasetsearch.test.jsx new file mode 100644 index 00000000..6922b904 --- /dev/null +++ b/src/templates/DatasetSearch/datasetsearch.test.jsx @@ -0,0 +1,62 @@ +import React from 'react'; +import axios from 'axios'; +import {act} from 'react-dom/test-utils'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import DatasetSearch, { selectedFacetsMessage } from './index'; + +jest.mock('axios'); +jest.useFakeTimers(); +const rootUrl = 'http://dkan.com/api/1'; +const data_results = { + data: { + total: '1', + results: { + 'dkan_dataset/5d69-frba': {title: 'dkan'} + }, + facets: [ + { + type: 'theme', + name: 'general', + total: '2' + }, + ], + } +}; + +describe('selectedFacetsMessage', () => { + test('turns selectedFacets and titles into a string', () => { + const selectedFacets = {theme: ['dkan'], keyword: ['react']}; + expect( + selectedFacetsMessage(selectedFacets, {theme: 'Categories', keyword: 'Tags'}) + ).toEqual('Categories: dkan & Tags: react') + }) +}) + +describe('', () => { + test('Renders correctly', async () => { + await axios.get.mockImplementation(() => Promise.resolve(data_results)); + const { debug } = render(); + await act(async () => { + + + + // debug() + jest.useFakeTimers(); + + }); + // debug() + // expect(axios.get).toHaveBeenCalledWith( + // `${rootUrl}/search/?`, + // ); + // await act(async () => { }); + + expect(screen.getByRole('heading', {name: 'Datasets'})) + expect(screen.getByRole('textbox', {name: 'Search term'})) + expect(screen.getByRole('button', {name: 'Clear all filters'})) + expect(screen.getByRole('combobox', {name: 'Sort by'})) + expect(screen.getByRole('button', {name: 'Search'})) + expect(screen.getByText(/0-0 out of 0/i)); + expect(screen.getByText('[0 entries total on page]')); + }) +}) diff --git a/src/templates/DatasetSearch/index.jsx b/src/templates/DatasetSearch/index.jsx new file mode 100644 index 00000000..aa74e29b --- /dev/null +++ b/src/templates/DatasetSearch/index.jsx @@ -0,0 +1,148 @@ +import React, { useState } from 'react'; +import { SearchPaginationResults } from '@civicactions/data-catalog-components'; +import { useSearchAPI, separateFacets } from '@civicactions/data-catalog-services'; +import { TextField, Dropdown, Spinner, Button } from '@cmsgov/design-system' +import DatasetSearchListItem from '../../components/DatasetSearchListItem'; +import Pagination from '../../components/Pagination'; +import DatasetSearchFacets from '../../components/DatasetSearchFacets'; + +export function selectedFacetsMessage(facets, alternateTitles) { + let message = []; + const keys = Object.keys(facets); + keys.forEach((k) => { + if(facets[k].length) { + message.push(`${alternateTitles[k]}: ${facets[k].join(', ')}`) + } + }) + return message.join(' & '); +} + + +const DatasetSearch = ({rootUrl}) => { + const { + fulltext, + selectedFacets, + loading, + items, + totalItems, + facets, + updateSelectedFacets, + setFulltext, + setSort, + setPage, + pageSize, + page, + resetFilters + } = useSearchAPI(rootUrl, {}) + const { theme, keyword } = separateFacets(facets); + const [filterText, setFilterText] = useState(''); + React.useEffect(() => { + if(fulltext !== filterText) { + setFilterText(fulltext) + } + }, [fulltext]) + + + return( +
+

+ Datasets +

+
+
+
{e.preventDefault(); () => setFulltext(filterText);}} + className="ds-u-display--flex ds-u-justify-content--between ds-u-margin-bottom--2 " + > + setFilterText(e.target.value)} + /> + + + + +

+ {selectedFacetsMessage(selectedFacets, {theme: 'Categories', keyword: 'Tags'})} +

+ +
    + {items.map((item) => ( +
  1. + +
  2. + ))} +
+

{`[${items.length} ${items.length === 1 ? 'entry' : 'entries'} total on page]`}

+ {totalItems && + () + } +
+
+
+ setSort(e.target.value)} + /> +
+
+ {theme ? + ( + + ) + : () + } + {keyword ? + ( + + ) + : () + } +
+
+
+
+ ); +} + +export default DatasetSearch;