Skip to content

Commit

Permalink
Remember resource and template search state
Browse files Browse the repository at this point in the history
Fixes #2227

Also, allow clearing the search via a new button.

See screencast to watch feature in action:

TBD
  • Loading branch information
mjgiarlo committed Oct 1, 2020
1 parent 7eb7cd7 commit 60c3137
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 43 deletions.
13 changes: 11 additions & 2 deletions __tests__/feature/searchAndOpenTemplate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,20 +45,29 @@ describe('searching and opening a resource', () => {
it('adds the template to recently used template history', async () => {
renderApp(store, history)

const queryString = 'title'

// Search for a template
const input = screen.getByPlaceholderText('Enter id, label, URI, remark, or author')
await fireEvent.change(input, { target: { value: 'title' } })
await fireEvent.change(input, { target: { value: queryString } })
await screen.findByText('resourceTemplate:bf2:Title:Note')

// open the template
const link = await screen.findByText('Title note', { selector: 'a' })
fireEvent.click(link)
await act(() => promise)

// return the the RT list
// return to the RT list
const rtLink = await screen.findByText('Resource Templates', { selector: 'a' })
fireEvent.click(rtLink)

// confirm RT query is still in place (stored in state and not cleared)
expect(input.value).toEqual(queryString)

// Clear search button empties the search field
fireEvent.click(screen.getByTestId('Clear query string', { selector: 'button' }))
expect(screen.getByPlaceholderText('Enter id, label, URI, remark, or author').value).toEqual('')

// see the recently used RTs
const histTemplateBtn = await screen.findByText('Most recently used resource templates')
fireEvent.click(histTemplateBtn)
Expand Down
18 changes: 14 additions & 4 deletions __tests__/feature/searchAndViewResource.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ describe('searching and viewing a resource', () => {
error: undefined,
})

it('renders a modal without edit controls', async () => {
// Setup search component to return known resource
const uri = 'http://localhost:3000/resource/c7db5404-7d7d-40ac-b38e-c821d2c3ae3f'
sinopiaSearch.getSearchResultsWithFacets.mockResolvedValue(resourceSearchResults(uri))
// Setup search component to return known resource
const uri = 'http://localhost:3000/resource/c7db5404-7d7d-40ac-b38e-c821d2c3ae3f'
sinopiaSearch.getSearchResultsWithFacets.mockResolvedValue(resourceSearchResults(uri))

it('renders a modal without edit controls', async () => {
renderApp()

fireEvent.click(screen.getByText('Linked Data Editor', { selector: 'a' }))
Expand Down Expand Up @@ -75,5 +75,15 @@ describe('searching and viewing a resource', () => {
fireEvent.click(screen.getByLabelText('Edit', { selector: 'button', exact: true }))
expect(screen.getByText('Uber template1', { selector: 'h3' })).toBeInTheDocument()
expect(screen.getByText('Copy URI', { selector: 'button' })).toBeInTheDocument()

// Switch back to search page
fireEvent.click(screen.getByText('Search', { selector: 'a' }))

// Confirm search query is still in place (stored in state and not cleared)
expect(await screen.getByLabelText('Query').value).toEqual(uri)

// Clear search button empties the search field
fireEvent.click(screen.getByTestId('Clear query string', { selector: 'button' }))
expect(await screen.getByLabelText('Query').value).toEqual('')
})
})
4 changes: 3 additions & 1 deletion cypress/integration/end2end.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ describe('End-to-end test', () => {
cy.url().should('include', '/templates')

cy.get('#searchInput')
.type('resourceTemplate:bf2:WorkTitle')
.should('have.value', 'resourceTemplate:bf2:WorkTitle')

// eslint-disable-next-line cypress/no-unnecessary-waiting
Expand Down Expand Up @@ -115,6 +114,9 @@ describe('End-to-end test', () => {
cy.get('a').contains('Search').click()
cy.url().should('include', '/search')

// Test search clear button
cy.get('button[title="Clear query string"]').click()

// Indexing latency is possible problem here.
// Force is necessary because reflow of search inputs is suboptimal.
cy.get('input#searchInput')
Expand Down
1 change: 0 additions & 1 deletion cypress/integration/leftNav.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ describe('Left-nav test', () => {
cy.url().should('include', '/templates')

cy.get('#searchInput')
.type('resourceTemplate:testing:uber1')
.should('have.value', 'resourceTemplate:testing:uber1')

// eslint-disable-next-line cypress/no-unnecessary-waiting
Expand Down
73 changes: 50 additions & 23 deletions src/components/search/Search.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,45 +10,57 @@ import {
fetchSinopiaSearchResults as fetchSinopiaSearchResultsCreator,
fetchQASearchResults as fetchQASearchResultsCreator,
} from 'actionCreators/search'
import { clearSearchResults as clearSearchResultsAction } from 'actions/search'
import { clearSearchResults as clearSearchResultsAction, setSearchResults } from 'actions/search'
import SinopiaSearchResults from './SinopiaSearchResults'
import QASearchResults from './QASearchResults'
import SearchResultsPaging from './SearchResultsPaging'
import SearchResultsMessage from './SearchResultsMessage'
import Alert from '../Alert'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faSearch } from '@fortawesome/free-solid-svg-icons'
import { faTrashAlt, faSearch } from '@fortawesome/free-solid-svg-icons'
import searchConfig from '../../../static/searchConfig.json'
import { selectSearchError, selectSearchUri, selectSearchOptions } from 'selectors/search'
import {
selectSearchError, selectSearchQuery, selectSearchUri, selectSearchOptions,
} from 'selectors/search'

const Search = (props) => {
const dispatch = useDispatch()
const fetchQASearchResults = (queryString, uri, startOfRange) => dispatch(fetchQASearchResultsCreator(queryString, uri, { ...searchOptions, startOfRange }))

const searchOptions = useSelector((state) => selectSearchOptions(state, 'resource'))
const error = useSelector((state) => selectSearchError(state, 'resource'))
const searchUri = useSelector((state) => selectSearchUri(state, 'resource'))
const lastQueryString = useSelector((state) => selectSearchQuery(state, 'resource'))

const fetchSinopiaSearchResults = (queryString, startOfRange) => dispatch(fetchSinopiaSearchResultsCreator(
queryString, { ...searchOptions, startOfRange },
))
const clearSearchResults = useCallback(() => dispatch(clearSearchResultsAction('resource')), [dispatch])

const fetchNewSinopiaSearchResults = (queryString) => dispatch(fetchSinopiaSearchResultsCreator(
queryString,
))
const topRef = useRef(null)

const defaultUri = 'sinopia'

const clearSearchResults = useCallback(() => dispatch(clearSearchResultsAction('resource')), [dispatch])
const [queryString, setQueryString] = useState(lastQueryString || '')
const [uri, setUri] = useState(searchUri || defaultUri)

const error = useSelector((state) => selectSearchError(state, 'resource'))
const searchUri = useSelector((state) => selectSearchUri(state, 'resource'))
useEffect(() => {
if (!queryString) clearSearchResults()
}, [clearSearchResults, queryString])

const topRef = useRef(null)
const fetchQASearchResults = (queryString, uri, startOfRange) => {
dispatch(fetchQASearchResultsCreator(queryString, uri, { ...searchOptions, startOfRange })).then((response) => {
if (response) dispatch(setSearchResults('resource', uri, response.results, response.totalHits, {}, queryString, { startOfRange }, response.error))
})
}

const [queryString, setQueryString] = useState('')
const [uri, setUri] = useState('sinopia')
const fetchSinopiaSearchResults = (queryString, startOfRange) => {
dispatch(fetchSinopiaSearchResultsCreator(queryString, { ...searchOptions, startOfRange })).then((response) => {
if (response) dispatch(setSearchResults('resource', null, response.results, response.totalHits, {}, queryString, { startOfRange }, response.error))
})
}

useEffect(() => {
clearSearchResults()
}, [clearSearchResults])
const fetchNewSinopiaSearchResults = (queryString) => {
dispatch(fetchSinopiaSearchResultsCreator(queryString)).then((response) => {
if (response) dispatch(setSearchResults('resource', null, response.results, response.totalHits, {}, queryString, {}, response.error))
})
}

const handleKeyPress = (event) => {
if (event.key === 'Enter') {
Expand All @@ -66,7 +78,7 @@ const Search = (props) => {
if (queryString === '') {
return
}
if (uri === 'sinopia') {
if (uri === defaultUri) {
fetchNewSinopiaSearchResults(queryString)
} else {
fetchQASearchResults(queryString, uri, 0)
Expand All @@ -85,7 +97,7 @@ const Search = (props) => {
const options = searchConfig.map((config) => (<option key={config.uri} value={config.uri}>{config.label}</option>))

let results
if (searchUri === 'sinopia') {
if (searchUri === defaultUri) {
results = (
<div>
<SinopiaSearchResults {...props} key="search-results" />
Expand Down Expand Up @@ -116,7 +128,7 @@ const Search = (props) => {
value={uri}
onChange={ (event) => setUri(event.target.value) }
onBlur={ (event) => setUri(event.target.value) }>
<option value="sinopia">Sinopia</option>
<option value={defaultUri}>Sinopia</option>
{options}
</select>
</div>
Expand All @@ -127,14 +139,29 @@ const Search = (props) => {
<div className="input-group" style={{ width: '100%' }}>
<input id="searchInput" type="text" className="form-control"
onChange={ (event) => setQueryString(event.target.value) }
onKeyPress={handleKeyPress} />
onKeyPress={handleKeyPress}
placeholder="Enter query string"
value={ queryString } />
<span className="input-group-btn">
<button className="btn btn-default"
type="submit"
title="Submit search"
aria-label="Submit search"
data-testid="Submit search">
<FontAwesomeIcon className="fa-search" icon={faSearch} />
</button>
<button className="btn btn-default"
type="button"
aria-label="Clear query string"
title="Clear query string"
data-testid="Clear query string"
onClick={() => {
setQueryString('')
setUri(defaultUri)
clearSearchResults()
} }>
<FontAwesomeIcon className="trash-icon" icon={faTrashAlt} />
</button>
</span>
</div>
</div>
Expand Down
40 changes: 28 additions & 12 deletions src/components/templates/TemplateSearch.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,28 @@ import Alert from '../Alert'
import SinopiaResourceTemplates from './SinopiaResourceTemplates'
import SearchResultsPaging from 'components/search/SearchResultsPaging'
import NewResourceTemplateButton from './NewResourceTemplateButton'
import { selectSearchError } from 'selectors/search'
import { selectSearchError, selectSearchQuery } from 'selectors/search'
import PropTypes from 'prop-types'

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons'

const TemplateSearch = (props) => {
const dispatch = useDispatch()
// Tokens allow us to cancel an existing search. Does not actually stop the
// search, but causes result to be ignored.
const tokens = useRef([])

const [query, setQueryText] = useState('')
const error = useSelector((state) => selectSearchError(state, 'template'))
const lastQueryString = useSelector((state) => selectSearchQuery(state, 'template'))

const [queryString, setQueryString] = useState(lastQueryString || '')
const [startOfRange, setStartOfRange] = useState(0)

const error = useSelector((state) => selectSearchError(state, 'template'))
const clearSearchResults = useCallback(() => dispatch(clearSearchResultsAction('template')), [dispatch])

useEffect(() => {
clearSearchResults()
}, [clearSearchResults])
if (!queryString) clearSearchResults()
}, [clearSearchResults, queryString])

useEffect(() => {
// Cancel all current searches
Expand All @@ -39,18 +42,18 @@ const TemplateSearch = (props) => {
// Create a token for this set of searches
const token = { cancel: false }
tokens.current.push(token)
getTemplateSearchResults(query, { startOfRange }).then((response) => {
if (!token.cancel) dispatch(setSearchResults('template', null, response.results, response.totalHits, {}, null, { startOfRange }, response.error))
getTemplateSearchResults(queryString, { startOfRange }).then((response) => {
if (!token.cancel) dispatch(setSearchResults('template', null, response.results, response.totalHits, {}, queryString, { startOfRange }, response.error))
})
}, [dispatch, query, startOfRange])
}, [dispatch, queryString, startOfRange])

const changePage = (startOfRange) => {
setStartOfRange(startOfRange)
}

const updateSearch = (e) => {
setStartOfRange(0)
setQueryText(e.target.value)
setQueryString(e.target.value)
}

return (
Expand All @@ -63,9 +66,22 @@ const TemplateSearch = (props) => {
<div className="form-group" style={{ paddingBottom: '10px', paddingTop: '10px' }}>
<label className="font-weight-bold" htmlFor="searchInput">Find a resource template</label>&nbsp;
<div className="input-group" style={{ width: '750px', paddingLeft: '5px' }}>
<input id="searchInput" type="text" className="form-control"
<input id="searchInput"
type="text"
className="form-control"
onChange={ updateSearch }
placeholder="Enter id, label, URI, remark, or author" />
placeholder="Enter id, label, URI, remark, or author"
value={ queryString } />
<span className="input-group-btn">
<button className="btn btn-default"
type="button"
aria-label="Clear query string"
title="Clear query string"
data-testid="Clear query string"
onClick={() => setQueryString('') }>
<FontAwesomeIcon className="trash-icon" icon={faTrashAlt} />
</button>
</span>
</div>
</div>
</form>
Expand Down

0 comments on commit 60c3137

Please sign in to comment.