From ac22b0c9e9dcc6d17744ae4ac5684a57c491f458 Mon Sep 17 00:00:00 2001 From: lbennett-moj Date: Thu, 29 Nov 2018 16:40:44 +0000 Subject: [PATCH] NN-1452 hide-able new filters on global search. Tests to follow --- backend/api/elite2Api.js | 4 +- backend/controllers/controller.js | 4 +- backend/controllers/globalSearch.js | 13 +- backend/tests/globalSearch.test.js | 32 ++-- .../prisonstaffhub/mockapis/Elite2Api.groovy | 4 +- src/App.scss | 4 + src/GlobalSearch/GlobalSearchContainer.js | 44 ++++- src/GlobalSearch/GlobalSearchForm.js | 177 +++++++++++++++--- src/govStyles/govuk_frontend/all.scss | 2 + .../govuk_frontend/components/_all.scss | 1 + .../govuk_frontend/components/_details.scss | 91 +++++++++ .../govuk_frontend/helpers/_all.scss | 1 + .../govuk_frontend/helpers/_shape-arrow.scss | 77 ++++++++ src/redux/actions/actionTypes.js | 2 + src/redux/actions/actions.test.js | 16 ++ src/redux/actions/index.js | 10 + src/redux/reducers/index.js | 12 ++ src/redux/reducers/reducers.test.js | 36 ++++ src/utils.js | 10 + 19 files changed, 484 insertions(+), 56 deletions(-) create mode 100644 src/govStyles/govuk_frontend/all.scss create mode 100644 src/govStyles/govuk_frontend/components/_all.scss create mode 100644 src/govStyles/govuk_frontend/components/_details.scss create mode 100644 src/govStyles/govuk_frontend/helpers/_all.scss create mode 100644 src/govStyles/govuk_frontend/helpers/_shape-arrow.scss diff --git a/backend/api/elite2Api.js b/backend/api/elite2Api.js index 2ad3bad46..836672cb2 100644 --- a/backend/api/elite2Api.js +++ b/backend/api/elite2Api.js @@ -60,12 +60,12 @@ const elite2ApiFactory = client => { const getSentenceData = (context, offenderNumbers) => post(context, `api/offender-sentences`, offenderNumbers) const getPrisonerImage = (context, offenderNo) => getStream(context, `api/bookings/offenderNo/${offenderNo}/image/data`) - const globalSearch = (context, offenderNo, lastName, firstName) => + const globalSearch = (context, offenderNo, lastName, firstName, genderFilter, locationFilter) => get( context, `api/prisoners?offenderNo=${offenderNo}&lastName=${encodeQueryString(lastName)}&firstName=${encodeQueryString( firstName - )}&partialNameMatch=false&includeAliases=true` + )}&gender=${genderFilter}&location=${locationFilter}&partialNameMatch=false&includeAliases=true` ) const getLastPrison = (context, body) => post(context, `api/movements/offenders`, body) diff --git a/backend/controllers/controller.js b/backend/controllers/controller.js index bdaba6a7c..736f944f4 100644 --- a/backend/controllers/controller.js +++ b/backend/controllers/controller.js @@ -32,8 +32,8 @@ const factory = ( }) const globalSearch = asyncMiddleware(async (req, res) => { - const { searchText } = req.query - const viewModel = await globalSearchService.globalSearch(res.locals, searchText) + const { searchText, genderFilter, locationFilter } = req.query + const viewModel = await globalSearchService.globalSearch(res.locals, searchText, genderFilter, locationFilter) res.set(res.locals.responseHeaders) res.json(viewModel) }) diff --git a/backend/controllers/globalSearch.js b/backend/controllers/globalSearch.js index b7a6b8c1f..2271a5169 100644 --- a/backend/controllers/globalSearch.js +++ b/backend/controllers/globalSearch.js @@ -5,14 +5,15 @@ const log = require('../log') const offenderIdPattern = /^[A-Za-z][0-9]{4}[A-Za-z]{2}$/ const globalSearchFactory = elite2Api => { - const searchByOffender = (context, offenderNo) => elite2Api.globalSearch(context, offenderNo, '', '') + const searchByOffender = (context, offenderNo, genderFilter, locationFilter) => + elite2Api.globalSearch(context, offenderNo, '', '', genderFilter, locationFilter) - const searchByName = (context, name) => { + const searchByName = (context, name, genderFilter, locationFilter) => { const [lastName, firstName] = name.split(' ') - return elite2Api.globalSearch(context, '', lastName, firstName || '') + return elite2Api.globalSearch(context, '', lastName, firstName || '', genderFilter, locationFilter) } - const globalSearch = async (context, searchText) => { + const globalSearch = async (context, searchText, genderFilter, locationFilter) => { log.info(`In globalSearch, searchText=${searchText}`) if (!searchText) { return [] @@ -21,7 +22,9 @@ const globalSearchFactory = elite2Api => { .replace(/,/g, ' ') .replace(/\s\s+/g, ' ') .trim() - const data = await (offenderIdPattern.test(text) ? searchByOffender(context, text) : searchByName(context, text)) + const data = await (offenderIdPattern.test(text) + ? searchByOffender(context, text, genderFilter, locationFilter) + : searchByName(context, text, genderFilter, locationFilter)) log.info(data, 'globalSearch data received') const offenderOutIds = data diff --git a/backend/tests/globalSearch.test.js b/backend/tests/globalSearch.test.js index 297fbab31..6702cfa16 100644 --- a/backend/tests/globalSearch.test.js +++ b/backend/tests/globalSearch.test.js @@ -151,12 +151,12 @@ describe('Global Search controller', async () => { it('Should return no results as an empty array', async () => { elite2Api.globalSearch.mockReturnValue([]) - const response = await globalSearch({}, 'text') + const response = await globalSearch({}, 'text', '', '') expect(response).toEqual([]) expect(elite2Api.globalSearch).toHaveBeenCalled() - expect(elite2Api.globalSearch.mock.calls[0]).toEqual([{}, '', 'text', '']) + expect(elite2Api.globalSearch.mock.calls[0]).toEqual([{}, '', 'text', '', '', '']) }) it('Should return results', async () => { @@ -188,8 +188,8 @@ describe('Global Search controller', async () => { elite2Api.globalSearch.mockReturnValue(apiResponse) const offenderNo = 'Z4444YY' - await globalSearch({}, offenderNo) - expect(elite2Api.globalSearch.mock.calls[0]).toEqual([{}, offenderNo, '', '']) + await globalSearch({}, offenderNo, '', '', '') + expect(elite2Api.globalSearch.mock.calls[0]).toEqual([{}, offenderNo, '', '', '', '']) }) it('Should detect an offenderId with lowercase letters', async () => { @@ -197,47 +197,47 @@ describe('Global Search controller', async () => { elite2Api.globalSearch.mockReturnValue(apiResponse) const offenderNo = 'z4444yy' - await globalSearch({}, offenderNo) - expect(elite2Api.globalSearch.mock.calls[0]).toEqual([{}, offenderNo, '', '']) + await globalSearch({}, offenderNo, '', '', '') + expect(elite2Api.globalSearch.mock.calls[0]).toEqual([{}, offenderNo, '', '', '', '']) }) it('Should detect 2 words', async () => { const apiResponse = createResponse() elite2Api.globalSearch.mockReturnValue(apiResponse) - await globalSearch({}, 'last first') - expect(elite2Api.globalSearch.mock.calls[0]).toEqual([{}, '', 'last', 'first']) + await globalSearch({}, 'last first', '', '') + expect(elite2Api.globalSearch.mock.calls[0]).toEqual([{}, '', 'last', 'first', '', '']) }) it('Should detect 2 words and remove commas', async () => { const apiResponse = createResponse() elite2Api.globalSearch.mockReturnValue(apiResponse) - await globalSearch({}, ',last, first,') - expect(elite2Api.globalSearch.mock.calls[0]).toEqual([{}, '', 'last', 'first']) + await globalSearch({}, ',last, first,', '', '') + expect(elite2Api.globalSearch.mock.calls[0]).toEqual([{}, '', 'last', 'first', '', '']) }) it('Should detect 2 words with no space between comma', async () => { const apiResponse = createResponse() elite2Api.globalSearch.mockReturnValue(apiResponse) - await globalSearch({}, ',last, first,') - expect(elite2Api.globalSearch.mock.calls[0]).toEqual([{}, '', 'last', 'first']) + await globalSearch({}, ',last, first,', '', '') + expect(elite2Api.globalSearch.mock.calls[0]).toEqual([{}, '', 'last', 'first', '', '']) }) it('Should detect 2 words with various spaces and commas', async () => { const apiResponse = createResponse() elite2Api.globalSearch.mockReturnValue(apiResponse) - await globalSearch({}, ', last , first other, ') - expect(elite2Api.globalSearch.mock.calls[0]).toEqual([{}, '', 'last', 'first']) + await globalSearch({}, ', last , first other, ', '', '') + expect(elite2Api.globalSearch.mock.calls[0]).toEqual([{}, '', 'last', 'first', '', '']) }) it('Should ignore leading and trailing whitespace', async () => { const apiResponse = createResponse() elite2Api.globalSearch.mockReturnValue(apiResponse) - await globalSearch({}, ' word ') - expect(elite2Api.globalSearch.mock.calls[0]).toEqual([{}, '', 'word', '']) + await globalSearch({}, ' word ', '', '') + expect(elite2Api.globalSearch.mock.calls[0]).toEqual([{}, '', 'word', '', '', '']) }) }) diff --git a/prisonstaffhub-specs/src/test/groovy/uk/gov/justice/digital/hmpps/prisonstaffhub/mockapis/Elite2Api.groovy b/prisonstaffhub-specs/src/test/groovy/uk/gov/justice/digital/hmpps/prisonstaffhub/mockapis/Elite2Api.groovy index 2244e4085..3b8da6029 100644 --- a/prisonstaffhub-specs/src/test/groovy/uk/gov/justice/digital/hmpps/prisonstaffhub/mockapis/Elite2Api.groovy +++ b/prisonstaffhub-specs/src/test/groovy/uk/gov/justice/digital/hmpps/prisonstaffhub/mockapis/Elite2Api.groovy @@ -399,7 +399,7 @@ class Elite2Api extends WireMockRule { final totalRecords = String.valueOf(response.size()) this.stubFor( - get("/api/prisoners?offenderNo=${offenderNo}&lastName=${lastName}&firstName=${firstName}&partialNameMatch=false&includeAliases=true") + get("/api/prisoners?offenderNo=${offenderNo}&lastName=${lastName}&firstName=${firstName}&gender=ALL&location=ALL&partialNameMatch=false&includeAliases=true") .withHeader('page-offset', equalTo('0')) .withHeader('page-limit', equalTo('10')) .willReturn( @@ -412,7 +412,7 @@ class Elite2Api extends WireMockRule { .withStatus(200))) if (response.size() > 10) { this.stubFor( - get("/api/prisoners?offenderNo=${offenderNo}&lastName=${lastName}&firstName=${firstName}&partialNameMatch=false&includeAliases=true") + get("/api/prisoners?offenderNo=${offenderNo}&lastName=${lastName}&firstName=${firstName}&gender=ALL&location=ALL&partialNameMatch=false&includeAliases=true") .withHeader('page-offset', equalTo('10')) .withHeader('page-limit', equalTo('10')) .willReturn( diff --git a/src/App.scss b/src/App.scss index 3ca182130..18be5ba9d 100644 --- a/src/App.scss +++ b/src/App.scss @@ -282,6 +282,10 @@ tr a.link:visited { font-weight: 700; } +.visible { + display: block !important; +} + /* Removes headers and footers for printing purposes */ @media print { @page { diff --git a/src/GlobalSearch/GlobalSearchContainer.js b/src/GlobalSearch/GlobalSearchContainer.js index b47203054..0092bd13f 100644 --- a/src/GlobalSearch/GlobalSearchContainer.js +++ b/src/GlobalSearch/GlobalSearchContainer.js @@ -13,6 +13,8 @@ import { setGlobalSearchPageNumber, setGlobalSearchTotalRecords, setApplicationTitle, + setGlobalSearchLocationFilter, + setGlobalSearchGenderFilter, } from '../redux/actions' const axios = require('axios') @@ -25,7 +27,10 @@ class GlobalSearchContainer extends Component { this.doGlobalSearch = this.doGlobalSearch.bind(this) this.handlePageAction = this.handlePageAction.bind(this) this.handleSearchTextChange = this.handleSearchTextChange.bind(this) + this.handleSearchLocationFilterChange = this.handleSearchLocationFilterChange.bind(this) + this.handleSearchGenderFilterChange = this.handleSearchGenderFilterChange.bind(this) this.handleSearch = this.handleSearch.bind(this) + this.clearFilters = this.clearFilters.bind(this) } async componentWillMount() { @@ -42,11 +47,21 @@ class GlobalSearchContainer extends Component { } async doGlobalSearch(pageNumber, searchText) { - const { pageSize, totalRecordsDispatch, dataDispatch, pageNumberDispatch, raiseAnalyticsEvent } = this.props + const { + pageSize, + totalRecordsDispatch, + dataDispatch, + pageNumberDispatch, + raiseAnalyticsEvent, + genderFilter, + locationFilter, + } = this.props const response = await axios.get('/api/globalSearch', { params: { searchText, + genderFilter, + locationFilter, }, headers: { 'Page-Offset': pageSize * pageNumber, @@ -83,6 +98,22 @@ class GlobalSearchContainer extends Component { searchTextDispatch(event.target.value) } + handleSearchLocationFilterChange(event) { + const { locationFilterDispatch } = this.props + locationFilterDispatch(event.target.value) + } + + handleSearchGenderFilterChange(event) { + const { genderFilterDispatch } = this.props + genderFilterDispatch(event.target.value) + } + + clearFilters() { + const { genderFilterDispatch, locationFilterDispatch } = this.props + genderFilterDispatch('ALL') + locationFilterDispatch('ALL') + } + render() { const { loaded, error } = this.props @@ -93,7 +124,10 @@ class GlobalSearchContainer extends Component { @@ -111,6 +145,8 @@ GlobalSearchContainer.propTypes = { loaded: PropTypes.bool.isRequired, agencyId: PropTypes.string.isRequired, searchText: PropTypes.string.isRequired, + genderFilter: PropTypes.string.isRequired, + locationFilter: PropTypes.string.isRequired, data: PropTypes.arrayOf( PropTypes.shape({ offenderNo: PropTypes.string.isRequired, @@ -131,6 +167,8 @@ GlobalSearchContainer.propTypes = { pageNumberDispatch: PropTypes.func.isRequired, totalRecordsDispatch: PropTypes.func.isRequired, searchTextDispatch: PropTypes.func.isRequired, + genderFilterDispatch: PropTypes.func.isRequired, + locationFilterDispatch: PropTypes.func.isRequired, titleDispatch: PropTypes.func.isRequired, // special @@ -149,12 +187,16 @@ const mapStateToProps = state => ({ pageSize: state.globalSearch.pageSize, totalRecords: state.globalSearch.totalRecords, searchText: state.globalSearch.searchText, + locationFilter: state.globalSearch.locationFilter, + genderFilter: state.globalSearch.genderFilter, error: state.app.error, }) const mapDispatchToProps = dispatch => ({ dataDispatch: data => dispatch(setGlobalSearchResults(data)), searchTextDispatch: text => dispatch(setGlobalSearchText(text)), + genderFilterDispatch: text => dispatch(setGlobalSearchGenderFilter(text)), + locationFilterDispatch: text => dispatch(setGlobalSearchLocationFilter(text)), pageNumberDispatch: no => dispatch(setGlobalSearchPageNumber(no)), totalRecordsDispatch: no => dispatch(setGlobalSearchTotalRecords(no)), titleDispatch: title => dispatch(setApplicationTitle(title)), diff --git a/src/GlobalSearch/GlobalSearchForm.js b/src/GlobalSearch/GlobalSearchForm.js index 9135610c9..5e1f56c1c 100644 --- a/src/GlobalSearch/GlobalSearchForm.js +++ b/src/GlobalSearch/GlobalSearchForm.js @@ -1,43 +1,164 @@ -import React from 'react' +import React, { Component, Fragment } from 'react' import '../index.scss' import '../lists.scss' import '../App.scss' +import '../govStyles/govuk_frontend/all.scss' import PropTypes from 'prop-types' import ReactRouterPropTypes from 'react-router-prop-types' +import { linkOnClick } from '../utils' -const GlobalSearchForm = ({ searchText, handleSearchTextChange, handleSearch, history }) => ( -
-
- - - -
-
-) +class GlobalSearchForm extends Component { + constructor(props) { + super(props) + this.state = { + showFilters: false, + } + } + + render() { + const { + handleSearchTextChange, + searchText, + handleSearch, + history, + genderFilter, + locationFilter, + handleSearchGenderFilterChange, + handleSearchLocationFilterChange, + clearFilters, + } = this.props + + const toggleDetails = (event, clear) => { + event.preventDefault() + const { showFilters } = this.state + this.setState({ + showFilters: !showFilters, + }) + if (showFilters) { + clear() + } + } + + const locationSelect = ( + + + + + + ) + + const genderSelect = ( + + + + + + ) + + const { showFilters } = this.state + return ( +
+
+ + + +
+
+ toggleDetails(event, clearFilters)} + onKeyDown={() => {}} + tabIndex="0" + role="switch" + aria-checked={showFilters} + > + {showFilters ? 'Hide filters' : 'Show filters'} + +
+
{locationSelect}
+
{genderSelect}
+ +
+
+
+ ) + } +} GlobalSearchForm.propTypes = { // props handleSearch: PropTypes.func.isRequired, handleSearchTextChange: PropTypes.func.isRequired, + handleSearchGenderFilterChange: PropTypes.func.isRequired, + handleSearchLocationFilterChange: PropTypes.func.isRequired, + clearFilters: PropTypes.func.isRequired, searchText: PropTypes.string.isRequired, + locationFilter: PropTypes.string.isRequired, + genderFilter: PropTypes.string.isRequired, history: ReactRouterPropTypes.history.isRequired, } diff --git a/src/govStyles/govuk_frontend/all.scss b/src/govStyles/govuk_frontend/all.scss new file mode 100644 index 000000000..0aa4d9e36 --- /dev/null +++ b/src/govStyles/govuk_frontend/all.scss @@ -0,0 +1,2 @@ +@import "helpers/all"; +@import "components/all"; diff --git a/src/govStyles/govuk_frontend/components/_all.scss b/src/govStyles/govuk_frontend/components/_all.scss new file mode 100644 index 000000000..2c53edf3b --- /dev/null +++ b/src/govStyles/govuk_frontend/components/_all.scss @@ -0,0 +1 @@ +@import 'details' \ No newline at end of file diff --git a/src/govStyles/govuk_frontend/components/_details.scss b/src/govStyles/govuk_frontend/components/_details.scss new file mode 100644 index 000000000..053827c61 --- /dev/null +++ b/src/govStyles/govuk_frontend/components/_details.scss @@ -0,0 +1,91 @@ +@import "colours/palette"; + +/* stylelint-disable */ +/* Copied from Govuk details css, part of the newer https://github.com/alphagov/govuk-frontend project +which replaces govuk-elements */ +.govuk-details { + font-family: 'nta', Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 400; + font-size: 19px; + line-height: 1.25; + color: #0b0c0c; + margin-bottom: 20px; + display: block; +} + +.govuk-details__summary { + // Make the focus outline shrink-wrap the text content of the summary + display: inline-block; + + // Absolutely position the marker against this element + position: relative; + + margin-bottom: 5px; + + // Allow for absolutely positioned marker and align with disclosed text + padding-left: 20px; + + // Style the summary to look like a link... + color: $link-colour; + cursor: pointer; +} + +// ...but only underline the text, not the arrow +.govuk-details__summary-text { + text-decoration: underline; +} + +.govuk-details__summary:hover { + color: $link-hover-colour; +} + +.govuk-details__summary:focus { + // -1px offset fixes gap between background and outline in Firefox + outline: 4px solid $focus-colour; + outline-offset: -1px; + // When focussed, the text colour needs to be darker to ensure that colour + // contrast is still acceptable + color: black; + background: $focus-colour; +} + +// Remove the default details marker so we can style our own consistently and +// ensure it displays in Firefox (see implementation.md for details) +.govuk-details__summary::-webkit-details-marker { + display: none; +} + +// Append our own open / closed marker using a pseudo-element +.govuk-details__summary:before { + content: ''; + position: absolute; + + top: 0; + bottom: 0; + left: 0; + + margin: auto; + + @include govuk-shape-arrow($direction: right, $base: 14px); + + .govuk-details[open] > & { + @include govuk-shape-arrow($direction: down, $base: 14px); + } +} + +.govuk-details__text { + padding: 15px; + padding-left: 20px; + border-left: 5px solid $border-colour; +} + +.govuk-details__text p { + margin-top: 0; + margin-bottom: 20px; +} + +.govuk-details__text > :last-child { + margin-bottom: 0; +} diff --git a/src/govStyles/govuk_frontend/helpers/_all.scss b/src/govStyles/govuk_frontend/helpers/_all.scss new file mode 100644 index 000000000..a23e7bf7f --- /dev/null +++ b/src/govStyles/govuk_frontend/helpers/_all.scss @@ -0,0 +1 @@ +@import 'shape-arrow'; \ No newline at end of file diff --git a/src/govStyles/govuk_frontend/helpers/_shape-arrow.scss b/src/govStyles/govuk_frontend/helpers/_shape-arrow.scss new file mode 100644 index 000000000..02f4cde67 --- /dev/null +++ b/src/govStyles/govuk_frontend/helpers/_shape-arrow.scss @@ -0,0 +1,77 @@ +/* stylelint-disable */ +//// +/// @group helpers +//// + +/// Calculate the height of an equilateral triangle +/// +/// Multiplying half the length of the base of an equilateral triangle by the +/// square root of three gives us its height. We use 1.732 as an approximation. +/// +/// @param {Number} $base - Length of the base of the triangle +/// @return {Number} Calculated height of the triangle +/// @access private + +@function _govuk-equilateral-height($base) { + $square-root-of-three: 1.732; + + @return ($base / 2) * $square-root-of-three; +} + +/// Arrow mixin +/// +/// Generate Arrows (triangles) by using a mix of transparent (1) and coloured +/// borders. The coloured borders inherit the text colour of the element (2). +/// +/// Ensure the arrow is rendered correctly if browser colours are overridden by +/// providing a clip path (3). Without this the transparent borders are +/// overridden to become visible which results in a square. +/// +/// We need both because older browsers do not support clip-path. +/// +/// @param {String} $direction - Direction for arrow: up, right, down, left. +/// @param {Number} $base - Length of the triangle 'base' side +/// @param {Number} $height [null] - Height of triangle. Omit for equilateral. +/// @param {String} $display [block] - CSS display property of the arrow +/// +/// @access public + +@mixin govuk-shape-arrow($direction, $base, $height: null, $display: block) { + display: $display; + + width: 0; + height: 0; + + border-style: solid; + border-color: transparent; // 1 + + $perpendicular: $base / 2; + + @if ($height == null) { + $height: _govuk-equilateral-height($base); + } + + @if $direction == 'up' { + clip-path: polygon(50% 0%, 0% 100%, 100% 100%); // 3 + + border-width: 0 $perpendicular $height $perpendicular; + border-bottom-color: inherit; // 2 + } @else if $direction == 'right' { + clip-path: polygon(0% 0%, 100% 50%, 0% 100%); // 3 + + border-width: $perpendicular 0 $perpendicular $height; + border-left-color: inherit; // 2 + } @else if $direction == 'down' { + clip-path: polygon(0% 0%, 50% 100%, 100% 0%); // 3 + + border-width: $height $perpendicular 0 $perpendicular; + border-top-color: inherit; // 2 + } @else if $direction == 'left' { + clip-path: polygon(0% 50%, 100% 100%, 100% 0%); // 3 + + border-width: $perpendicular $height $perpendicular 0; + border-right-color: inherit; // 2 + } @else { + @error 'Invalid arrow direction: expected `up`, `right`, `down` or `left`, got `#{$direction}`'; + } +} diff --git a/src/redux/actions/actionTypes.js b/src/redux/actions/actionTypes.js index 294fd5156..b993516d6 100644 --- a/src/redux/actions/actionTypes.js +++ b/src/redux/actions/actionTypes.js @@ -26,6 +26,8 @@ export const SET_MENU_OPEN = 'SET_MENU_OPEN' export const SET_ESTABLISHMENT_ROLL_DATA = 'SET_ESTABLISHMENT_ROLL_DATA' export const SET_GLOBAL_SEARCH_RESULTS_DATA = 'SET_GLOBAL_SEARCH_RESULTS_DATA' export const SET_GLOBAL_SEARCH_TEXT = 'SET_GLOBAL_SEARCH_SEARCH_TEXT' +export const SET_GLOBAL_SEARCH_LOCATION_FILTER = 'SET_GLOBAL_SEARCH_LOCATION_FILTER' +export const SET_GLOBAL_SEARCH_GENDER_FILTER = 'SET_GLOBAL_SEARCH_GENDER_FILTER' export const SET_GLOBAL_SEARCH_PAGINATION_PAGE_SIZE = 'SET_GLOBAL_SEARCH_PAGINATION_PAGE_SIZE' export const SET_GLOBAL_SEARCH_PAGINATION_PAGE_NUMBER = 'SET_GLOBAL_SEARCH_PAGINATION_PAGE_NUMBER' export const SET_GLOBAL_SEARCH_PAGINATION_TOTAL_RECORDS = 'SET_GLOBAL_SEARCH_PAGINATION_TOTAL_RECORDS' diff --git a/src/redux/actions/actions.test.js b/src/redux/actions/actions.test.js index 53756effc..56b9816cb 100644 --- a/src/redux/actions/actions.test.js +++ b/src/redux/actions/actions.test.js @@ -204,6 +204,22 @@ describe('actions', () => { expect(actions.setGlobalSearchText('Ian')).toEqual(expectedAction) }) + it('should create an action to save the global search location filter', () => { + const expectedAction = { + type: types.SET_GLOBAL_SEARCH_LOCATION_FILTER, + locationFilter: 'Ian', + } + expect(actions.setGlobalSearchLocationFilter('Ian')).toEqual(expectedAction) + }) + + it('should create an action to save the global search gender filter', () => { + const expectedAction = { + type: types.SET_GLOBAL_SEARCH_GENDER_FILTER, + genderFilter: 'Ian', + } + expect(actions.setGlobalSearchGenderFilter('Ian')).toEqual(expectedAction) + }) + it('should create an action to save user search page size', () => { const expectedAction = { type: types.SET_GLOBAL_SEARCH_PAGINATION_PAGE_SIZE, diff --git a/src/redux/actions/index.js b/src/redux/actions/index.js index 1c5e9f794..6cce947b0 100644 --- a/src/redux/actions/index.js +++ b/src/redux/actions/index.js @@ -131,6 +131,16 @@ export const setGlobalSearchText = searchText => ({ searchText, }) +export const setGlobalSearchLocationFilter = locationFilter => ({ + type: ActionTypes.SET_GLOBAL_SEARCH_LOCATION_FILTER, + locationFilter, +}) + +export const setGlobalSearchGenderFilter = genderFilter => ({ + type: ActionTypes.SET_GLOBAL_SEARCH_GENDER_FILTER, + genderFilter, +}) + export const setGlobalSearchResults = data => ({ type: ActionTypes.SET_GLOBAL_SEARCH_RESULTS_DATA, data, diff --git a/src/redux/reducers/index.js b/src/redux/reducers/index.js index bb6dc777b..14e566533 100644 --- a/src/redux/reducers/index.js +++ b/src/redux/reducers/index.js @@ -58,6 +58,8 @@ const globalSearchInitialState = { totalRecords: 0, contextUser: {}, searchText: '', + locationFilter: 'ALL', + genderFilter: 'ALL', } function updateObject(oldObject, newValues) { @@ -250,6 +252,16 @@ export function globalSearch(state = globalSearchInitialState, action) { ...state, searchText: action.searchText, } + case ActionTypes.SET_GLOBAL_SEARCH_LOCATION_FILTER: + return { + ...state, + locationFilter: action.locationFilter, + } + case ActionTypes.SET_GLOBAL_SEARCH_GENDER_FILTER: + return { + ...state, + genderFilter: action.genderFilter, + } default: return state } diff --git a/src/redux/reducers/reducers.test.js b/src/redux/reducers/reducers.test.js index ae8f9c793..e4633ae8c 100644 --- a/src/redux/reducers/reducers.test.js +++ b/src/redux/reducers/reducers.test.js @@ -382,6 +382,8 @@ describe('app (global) reducer', () => { ...pagingInitialState, contextUser: {}, searchText: '', + locationFilter: 'ALL', + genderFilter: 'ALL', data: ['data0', 'data1'], }) }) @@ -396,6 +398,40 @@ describe('app (global) reducer', () => { ...pagingInitialState, contextUser: {}, searchText: 'hello', + locationFilter: 'ALL', + genderFilter: 'ALL', + data: [], + }) + }) + + it('should handle SET_GLOBAL_SEARCH_LOCATION_FILTER', () => { + expect( + globalSearch(undefined, { + type: types.SET_GLOBAL_SEARCH_LOCATION_FILTER, + locationFilter: 'MDI', + }) + ).toEqual({ + ...pagingInitialState, + contextUser: {}, + searchText: '', + locationFilter: 'MDI', + genderFilter: 'ALL', + data: [], + }) + }) + + it('should handle SET_GLOBAL_SEARCH_GENDER_FILTER', () => { + expect( + globalSearch(undefined, { + type: types.SET_GLOBAL_SEARCH_GENDER_FILTER, + genderFilter: 'F', + }) + ).toEqual({ + ...pagingInitialState, + contextUser: {}, + searchText: '', + locationFilter: 'ALL', + genderFilter: 'F', data: [], }) }) diff --git a/src/utils.js b/src/utils.js index 769dd5929..68dad0522 100644 --- a/src/utils.js +++ b/src/utils.js @@ -63,6 +63,15 @@ const getEventDescription = event => { return removeBlanks([event.eventDescription, event.eventLocation, event.comment]).join(' - ') } +const linkOnClick = handlerFn => ({ + tabIndex: 0, + role: 'link', + onClick: handlerFn, + onKeyDown: event => { + if (event.key === 'Enter') handlerFn(event) + }, +}) + module.exports = { properCase, properCaseName, @@ -71,4 +80,5 @@ module.exports = { getMainEventDescription, getEventDescription, stripAgencyPrefix, + linkOnClick, }