-
Notifications
You must be signed in to change notification settings - Fork 123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
17176 Global search results web components conversion #28812
base: main
Are you sure you want to change the base?
Changes from all commits
8060e88
72bc52e
cf3493d
27b1488
eafdcc9
5b2c08c
dfa5016
4605518
e49fbd0
459a5cf
4e3fa40
3f53c47
8316d6f
e6eccee
2b12117
d0e4706
734c55f
4096ff2
61067d4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import React, { useEffect } from 'react'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not new code; it was moved & renamed and converted from a class component to a function component. |
||
import { VaBreadcrumbs } from '@department-of-veterans-affairs/web-components/react-bindings'; | ||
import { focusElement } from 'platform/utilities/ui'; | ||
|
||
const Breadcrumbs = () => { | ||
useEffect(() => { | ||
focusElement('#search-breadcrumbs'); | ||
}, []); | ||
|
||
return ( | ||
<div className="row"> | ||
<VaBreadcrumbs | ||
class="vads-u-margin-left--1p5" | ||
id="search-breadcrumbs" | ||
label="Breadcrumbs" | ||
breadcrumbList={[ | ||
{ | ||
href: '/', | ||
label: 'Home', | ||
}, | ||
{ | ||
href: '/search', | ||
label: 'Search VA.gov', | ||
}, | ||
]} | ||
/> | ||
</div> | ||
); | ||
}; | ||
|
||
export default Breadcrumbs; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import React from 'react'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not new code; it was pulled out of SearchApp.jsx |
||
import PropTypes from 'prop-types'; | ||
|
||
const Errors = ({ userInput }) => { | ||
let errorMessage; | ||
|
||
if (!userInput.trim().length) { | ||
errorMessage = `Enter a search term that contains letters or numbers to find what you're looking for.`; | ||
} else if (userInput.length > 255) { | ||
errorMessage = | ||
'The search is over the character limit. Shorten the search and try again.'; | ||
} else { | ||
errorMessage = `We’re sorry. Something went wrong on our end, and your search | ||
didn't go through. Please try again.`; | ||
} | ||
|
||
return ( | ||
<div className="vads-u-margin-bottom--1p5"> | ||
{/* this is the alert box for when searches fail due to server issues */} | ||
<va-alert status="error" data-e2e-id="alert-box"> | ||
<h2 slot="headline">Your search didn’t go through</h2> | ||
<p>{errorMessage}</p> | ||
</va-alert> | ||
</div> | ||
); | ||
}; | ||
|
||
Errors.propTypes = { | ||
userInput: PropTypes.string.isRequired, | ||
}; | ||
|
||
export default Errors; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import React from 'react'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not new code; it was pulled out of |
||
|
||
const MoreVASearchTools = () => ( | ||
<ul> | ||
<li> | ||
<va-link | ||
href="https://search.usa.gov/search?affiliate=bvadecisions" | ||
text="Look up Board of Veterans' Appeals (BVA) decisions" | ||
/> | ||
</li> | ||
<li> | ||
<va-link href="/find-forms/" text="Find a VA form" /> | ||
</li> | ||
<li> | ||
<va-link | ||
href="https://www.va.gov/vapubs/" | ||
text="VA handbooks and other publications" | ||
/> | ||
</li> | ||
<li> | ||
<va-link | ||
href="https://www.vacareers.va.gov/job-search/index.asp" | ||
text="Explore and apply for open VA jobs" | ||
/> | ||
</li> | ||
</ul> | ||
); | ||
|
||
export default MoreVASearchTools; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import React from 'react'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not new code; pulled out of |
||
import PropTypes from 'prop-types'; | ||
import Result from './Result'; | ||
|
||
const RecommendedResults = ({ query, searchData, typeaheadUsed }) => { | ||
const { recommendedResults } = searchData; | ||
|
||
if (recommendedResults && recommendedResults.length > 0) { | ||
return ( | ||
<div> | ||
<h3 className="vads-u-font-size--base vads-u-font-family--sans vads-u-color--gray-dark vads-u-font-weight--bold"> | ||
Our top recommendations for you | ||
</h3> | ||
<ul className="results-list" data-e2e-id="top-recommendations"> | ||
{recommendedResults.map((result, index) => ( | ||
<Result | ||
index={index} | ||
isBestBet | ||
key={index} | ||
query={query} | ||
result={result} | ||
searchData={searchData} | ||
snippetKey="description" | ||
typeaheadUsed={typeaheadUsed} | ||
/> | ||
))} | ||
</ul> | ||
<hr aria-hidden="true" /> | ||
</div> | ||
); | ||
} | ||
|
||
return null; | ||
}; | ||
|
||
RecommendedResults.propTypes = { | ||
query: PropTypes.string, | ||
searchData: PropTypes.object, | ||
typeaheadUsed: PropTypes.bool, | ||
}; | ||
|
||
export default RecommendedResults; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
import React from 'react'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not new code; pulled from |
||
import PropTypes from 'prop-types'; | ||
import * as Sentry from '@sentry/browser'; | ||
import recordEvent from 'platform/monitoring/record-event'; | ||
import { apiRequest } from 'platform/utilities/api'; | ||
import { replaceWithStagingDomain } from 'platform/utilities/environment/stagingDomains'; | ||
import { | ||
formatResponseString, | ||
truncateResponseString, | ||
removeDoubleBars, | ||
} from '../utils'; | ||
|
||
const MAX_DESCRIPTION_LENGTH = 186; | ||
|
||
const onSearchResultClick = ({ | ||
bestBet, | ||
index, | ||
query, | ||
searchData, | ||
title, | ||
typeaheadUsed, | ||
url, | ||
}) => async e => { | ||
const { currentPage, recommendedResults, totalEntries } = searchData; | ||
e.preventDefault(); | ||
|
||
// clear the &t query param which is used to track typeahead searches | ||
// removing this will better reflect how many typeahead searches result in at least one click | ||
window.history.replaceState( | ||
null, | ||
document.title, | ||
`${window.location.href.replace('&t=true', '')}`, | ||
); | ||
|
||
const bestBetPosition = index + 1; | ||
const normalResultPosition = index + (recommendedResults?.length || 0) + 1; | ||
const searchResultPosition = bestBet ? bestBetPosition : normalResultPosition; | ||
|
||
const encodedUrl = encodeURIComponent(url); | ||
const userAgent = encodeURIComponent(navigator.userAgent); | ||
const encodedQuery = encodeURIComponent(query); | ||
const apiRequestOptions = { | ||
method: 'POST', | ||
}; | ||
const moduleCode = bestBet ? 'BOOS' : 'I14Y'; | ||
|
||
// By implementing in this fashion (i.e. a promise chain), code that follows is not blocked by this api request. Following the link at the end of the | ||
// function should happen regardless of the result of this api request, and it can happen before this request resolves. | ||
apiRequest( | ||
`https://api.va.gov/v0/search_click_tracking?position=${searchResultPosition}&query=${encodedQuery}&url=${encodedUrl}&user_agent=${userAgent}&module_code=${moduleCode}`, | ||
apiRequestOptions, | ||
).catch(error => { | ||
Sentry.captureException(error); | ||
randimays marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Sentry.captureMessage('search_click_tracking_error'); | ||
randimays marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
|
||
if (bestBet) { | ||
recordEvent({ | ||
event: 'nav-searchresults', | ||
'nav-path': `Recommended Results -> ${title}`, | ||
}); | ||
} | ||
|
||
recordEvent({ | ||
event: 'onsite-search-results-click', | ||
'search-page-path': document.location.pathname, | ||
'search-query': query, | ||
'search-result-chosen-page-url': url, | ||
'search-result-chosen-title': title, | ||
'search-results-n-current-page': currentPage, | ||
'search-results-position': searchResultPosition, | ||
'search-results-total-count': totalEntries, | ||
'search-results-total-pages': Math.ceil(totalEntries / 10), | ||
'search-results-top-recommendation': bestBet, | ||
'search-result-type': 'title', | ||
'search-selection': 'All VA.gov', | ||
'search-typeahead-used': typeaheadUsed, | ||
}); | ||
|
||
// relocate to clicked link page | ||
window.location.href = url; | ||
}; | ||
|
||
const Result = ({ | ||
index, | ||
isBestBet, | ||
query, | ||
result, | ||
searchData, | ||
snippetKey = 'snippet', | ||
typeaheadUsed, | ||
}) => { | ||
const strippedTitle = removeDoubleBars( | ||
formatResponseString(result?.title, true), | ||
); | ||
|
||
if (result?.title && result?.url) { | ||
return ( | ||
<li | ||
key={result.url} | ||
className="result-item vads-u-margin-top--1p5 vads-u-margin-bottom--4" | ||
> | ||
<h4 | ||
className="vads-u-display--inline vads-u-margin-top--1 vads-u-margin-bottom--0p25 vads-u-font-size--md vads-u-font-weight--bold vads-u-font-family--serif vads-u-text-decoration--underline" | ||
data-e2e-id="result-title" | ||
> | ||
<va-link | ||
disable-analytics | ||
href={replaceWithStagingDomain(result.url)} | ||
text={strippedTitle} | ||
onClick={onSearchResultClick({ | ||
bestBet: isBestBet, | ||
index, | ||
query, | ||
searchData, | ||
title: strippedTitle, | ||
typeaheadUsed, | ||
url: replaceWithStagingDomain(result.url), | ||
})} | ||
/> | ||
</h4> | ||
<p className="result-url vads-u-color--green vads-u-font-size--base"> | ||
{replaceWithStagingDomain(result.url)} | ||
</p> | ||
<p | ||
className="result-desc" | ||
/* eslint-disable react/no-danger */ | ||
randimays marked this conversation as resolved.
Show resolved
Hide resolved
|
||
dangerouslySetInnerHTML={{ | ||
__html: formatResponseString( | ||
truncateResponseString( | ||
result[snippetKey], | ||
MAX_DESCRIPTION_LENGTH, | ||
), | ||
), | ||
}} | ||
/* eslint-enable react/no-danger */ | ||
/> | ||
</li> | ||
); | ||
} | ||
|
||
return null; | ||
}; | ||
|
||
Result.propTypes = { | ||
index: PropTypes.number.isRequired, | ||
isBestBet: PropTypes.bool.isRequired, | ||
query: PropTypes.string.isRequired, | ||
result: PropTypes.shape({ | ||
title: PropTypes.string, | ||
url: PropTypes.string, | ||
}).isRequired, | ||
searchData: PropTypes.shape({ | ||
currentPage: PropTypes.number, | ||
recommendedResults: PropTypes.array, | ||
totalEntries: PropTypes.number, | ||
}).isRequired, | ||
typeaheadUsed: PropTypes.bool.isRequired, | ||
snippetKey: PropTypes.string, | ||
}; | ||
|
||
export default Result; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed because typeahead will always be enabled in the sitewide search; this value isn't needed.