diff --git a/src/components/SearchInput.js b/src/components/SearchInput.js index b026f71d..698759e2 100644 --- a/src/components/SearchInput.js +++ b/src/components/SearchInput.js @@ -14,6 +14,7 @@ import { Typography } from '@material-ui/core' import awaitTimeLimit from 'src/utils/awaitTimeLimit' import { AwaitedPromiseTimeout } from 'src/utils/errors' import logger from 'src/utils/logger' +import SetUserSearchEngineMutation from 'src/utils/mutations/SetUserSearchEngineMutation' import SearchSelect from './SearchSelect' const searchBoxBorderColor = '#ced4da' @@ -74,32 +75,21 @@ const SearchInput = (props) => { onSearchSelectMoreInfoClick, onSearchInputClick, } = props - const { searchEngine, yahooPaidSearchRewardOptIn } = user + const { searchEngine: currentSearchEngine, yahooPaidSearchRewardOptIn } = user const { searchEngines } = app const [searchSelectOpen, setSearchSelectOpen] = useState(false) const classes = useStyles() const searchInputRef = React.createRef() const fullInputRef = React.createRef() const [anchorEl, setAnchorEl] = React.useState(null) - const [currentSearchEngine, setCurrentSearchEngine] = useState(searchEngine) const [tooltipOpen, setTooltipOpen] = useState(!!tooltip) - - const getSearchEngine = useCallback( - (searchEngineId) => - searchEngines.edges.find( - (engine) => engine.node.engineId === searchEngineId - ).node, - [searchEngines] - ) - useEffect(() => { - setCurrentSearchEngine(searchEngine) setTooltipOpen(!!tooltip) - }, [searchEngine, getSearchEngine, yahooPaidSearchRewardOptIn, tooltip]) + }, [tooltip]) const onSearch = useCallback(async () => { const query = searchInputRef.current.value - const searchURL = currentSearchEngine.searchUrl.replace( + const searchURL = currentSearchEngine.searchUrlPersonalized.replace( /{\w+}/, encodeURIComponent(query) ) @@ -121,10 +111,10 @@ const SearchInput = (props) => { } } windowOpenTop(searchURL) - }, [userId, currentSearchEngine.searchUrl, searchInputRef]) + }, [userId, currentSearchEngine.searchUrlPersonalized, searchInputRef]) const onSwitchSearchEngine = (newSearchEngineId) => { - setCurrentSearchEngine(getSearchEngine(newSearchEngineId)) + SetUserSearchEngineMutation(userId, newSearchEngineId) } const onSearchSelectOpen = () => { @@ -197,9 +187,8 @@ const SearchInput = (props) => { } /> ( ) export const standard = Template.bind({}) standard.args = { - userId: 'abcdefghijklmno', tooltip: 'Great! You can always switch your search engine here later on.', app: { searchEngines: { @@ -23,7 +22,6 @@ standard.args = { node: { name: 'DuckDuckGo', engineId: 'DuckDuckGo', - searchUrl: 'https://duckduckgo.com/?q={searchTerms}', rank: 3, isCharitable: false, inputPrompt: 'Search DuckDuckGo', @@ -33,7 +31,6 @@ standard.args = { node: { name: 'Google', engineId: 'Google', - searchUrl: 'https://www.google.com/search?q={searchTerms}', rank: 1, isCharitable: false, inputPrompt: 'Search Google', @@ -43,7 +40,6 @@ standard.args = { node: { name: 'Ecosia', engineId: 'Ecosia', - searchUrl: 'https://www.ecosia.org/search?q={searchTerms}', rank: 2, isCharitable: false, inputPrompt: 'Search Ecosia', @@ -56,7 +52,6 @@ standard.args = { searchEngine: { name: 'Google', engineId: 'Google', - searchUrl: 'https://www.google.com/search?q={searchTerms}', inputPrompt: 'Search Google', }, yahooPaidSearchRewardOptIn: false, diff --git a/src/components/SearchInputContainer.js b/src/components/SearchInputContainer.js index 4f46cabb..5eb0f207 100644 --- a/src/components/SearchInputContainer.js +++ b/src/components/SearchInputContainer.js @@ -9,7 +9,6 @@ export default createFragmentContainer(SearchInput, { node { engineId name - searchUrl rank isCharitable inputPrompt @@ -23,7 +22,7 @@ export default createFragmentContainer(SearchInput, { searchEngine { engineId inputPrompt - searchUrl + searchUrlPersonalized } yahooPaidSearchRewardOptIn } diff --git a/src/components/SearchSelect.js b/src/components/SearchSelect.js index 6ffc87fc..968d359c 100644 --- a/src/components/SearchSelect.js +++ b/src/components/SearchSelect.js @@ -4,7 +4,6 @@ import { makeStyles } from '@material-ui/core/styles' import Typography from '@material-ui/core/Typography' import ToggleButton from '@material-ui/lab/ToggleButton' import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup' -import SetUserSearchEngineMutation from 'src/utils/mutations/SetUserSearchEngineMutation' import InfoIcon from '@material-ui/icons/InfoOutlined' import CheckIcon from '@material-ui/icons/Check' import DashboardPopover from './DashboardPopover' @@ -122,7 +121,6 @@ const useStyles = makeStyles((theme) => ({ const SearchSelect = ({ anchorEl, onClose, - userId, onMoreInfoClick, onSearchEngineSwitch, userSearchEngine, @@ -135,11 +133,10 @@ const SearchSelect = ({ const setCurrentSearchEngineHandler = useCallback( async (_event, newSearchEngine) => { if (newSearchEngine !== null) { - SetUserSearchEngineMutation(userId, newSearchEngine) onSearchEngineSwitch(newSearchEngine) } }, - [onSearchEngineSwitch, userId] + [onSearchEngineSwitch] ) const onCloseHandler = useCallback(async () => { onClose() @@ -253,10 +250,8 @@ SearchSelect.propTypes = { PropTypes.shape({ current: PropTypes.elementType }), ]), onClose: PropTypes.func, - userId: PropTypes.string.isRequired, userSearchEngine: PropTypes.shape({ engineId: PropTypes.string, - searchUrl: PropTypes.string, inputPrompt: PropTypes.string, }).isRequired, onMoreInfoClick: PropTypes.func, @@ -267,7 +262,6 @@ SearchSelect.propTypes = { node: PropTypes.shape({ engineId: PropTypes.string, name: PropTypes.string, - searchUrl: PropTypes.string, rank: PropTypes.number, isCharitable: PropTypes.bool, inputPrompt: PropTypes.string, diff --git a/src/components/SearchSelect.stories.jsx b/src/components/SearchSelect.stories.jsx index a047851b..b46fe8cb 100644 --- a/src/components/SearchSelect.stories.jsx +++ b/src/components/SearchSelect.stories.jsx @@ -19,7 +19,7 @@ withoutCharitableEngine.args = { userSearchEngine: { name: 'Google', engineId: 'Google', - searchUrl: 'https://www.google.com/search?q={searchTerms}', + searchUrlPersonalized: 'https://www.google.com/search?q={searchTerms}', inputPrompt: 'Search Google', }, onMoreInfoClick: () => {}, @@ -30,7 +30,6 @@ withoutCharitableEngine.args = { node: { name: 'DuckDuckGo', engineId: 'DuckDuckGo', - searchUrl: 'https://duckduckgo.com/?q={searchTerms}', rank: 3, isCharitable: false, inputPrompt: 'Search DuckDuckGo', @@ -40,7 +39,6 @@ withoutCharitableEngine.args = { node: { name: 'Google', engineId: 'Google', - searchUrl: 'https://www.google.com/search?q={searchTerms}', rank: 1, isCharitable: false, inputPrompt: 'Search Google', @@ -50,7 +48,6 @@ withoutCharitableEngine.args = { node: { name: 'Ecosia', engineId: 'Ecosia', - searchUrl: 'https://www.ecosia.org/search?q={searchTerms}', rank: 2, isCharitable: false, inputPrompt: 'Search Ecosia', @@ -69,7 +66,7 @@ withCharitableEngine.args = { userSearchEngine: { name: 'Google', engineId: 'Google', - searchUrl: 'https://www.google.com/search?q={searchTerms}', + searchUrlPersonalized: 'https://www.google.com/search?q={searchTerms}', inputPrompt: 'Search Google', }, onMoreInfoClick: () => {}, @@ -80,7 +77,6 @@ withCharitableEngine.args = { node: { name: 'DuckDuckGo', engineId: 'DuckDuckGo', - searchUrl: 'https://duckduckgo.com/?q={searchTerms}', rank: 3, isCharitable: false, inputPrompt: 'Search DuckDuckGo', @@ -90,7 +86,6 @@ withCharitableEngine.args = { node: { name: 'Google', engineId: 'Google', - searchUrl: 'https://www.google.com/search?q={searchTerms}', rank: 1, isCharitable: false, inputPrompt: 'Search Google', @@ -100,7 +95,6 @@ withCharitableEngine.args = { node: { name: 'Ecosia', engineId: 'Ecosia', - searchUrl: 'https://www.ecosia.org/search?q={searchTerms}', rank: 2, isCharitable: false, inputPrompt: 'Search Ecosia', @@ -110,7 +104,6 @@ withCharitableEngine.args = { node: { name: 'Search for a Cause', engineId: 'SearchForACause', - searchUrl: 'http://tab.gladly.io/search/v2?q={searchTerms}', rank: 0, isCharitable: true, inputPrompt: 'Search for a Cause', @@ -129,7 +122,7 @@ withCharitableEngineAndOptedIn.args = { userSearchEngine: { name: 'Google', engineId: 'Google', - searchUrl: 'https://www.google.com/search?q={searchTerms}', + searchUrlPersonalized: 'https://www.google.com/search?q={searchTerms}', inputPrompt: 'Search Google', }, onMoreInfoClick: () => {}, @@ -140,7 +133,6 @@ withCharitableEngineAndOptedIn.args = { node: { name: 'DuckDuckGo', engineId: 'DuckDuckGo', - searchUrl: 'https://duckduckgo.com/?q={searchTerms}', rank: 3, isCharitable: false, inputPrompt: 'Search DuckDuckGo', @@ -150,7 +142,6 @@ withCharitableEngineAndOptedIn.args = { node: { name: 'Google', engineId: 'Google', - searchUrl: 'https://www.google.com/search?q={searchTerms}', rank: 1, isCharitable: false, inputPrompt: 'Search Google', @@ -160,7 +151,6 @@ withCharitableEngineAndOptedIn.args = { node: { name: 'Ecosia', engineId: 'Ecosia', - searchUrl: 'https://www.ecosia.org/search?q={searchTerms}', rank: 2, isCharitable: false, inputPrompt: 'Search Ecosia', @@ -170,7 +160,6 @@ withCharitableEngineAndOptedIn.args = { node: { name: 'Search for a Cause', engineId: 'SearchForACause', - searchUrl: 'http://tab.gladly.io/search/v2?q={searchTerms}', rank: 0, isCharitable: true, inputPrompt: 'Search for a Cause', diff --git a/src/components/__tests__/SearchInput.test.js b/src/components/__tests__/SearchInput.test.js index ea72735c..f8294651 100644 --- a/src/components/__tests__/SearchInput.test.js +++ b/src/components/__tests__/SearchInput.test.js @@ -8,11 +8,13 @@ import { windowOpenTop } from 'src/utils/navigation' import Tooltip from '@material-ui/core/Tooltip' import flushAllPromises from 'src/utils/testHelpers/flushAllPromises' import logger from 'src/utils/logger' +import SetUserSearchEngineMutation from 'src/utils/mutations/SetUserSearchEngineMutation' import SearchSelect from '../SearchSelect' jest.mock('src/utils/mutations/LogSearchMutation') jest.mock('src/utils/navigation') jest.mock('src/utils/logger') +jest.mock('src/utils/mutations/SetUserSearchEngineMutation') const getMockProps = () => ({ userId: 'abcdefghijklmno', @@ -23,7 +25,6 @@ const getMockProps = () => ({ node: { name: 'Google', engineId: 'Google', - searchUrl: 'https://www.google.com/search?q={searchTerms}', rank: 1, isCharitable: false, inputPrompt: 'Search Google', @@ -33,7 +34,6 @@ const getMockProps = () => ({ node: { name: 'DuckDuckGo', engineId: 'DuckDuckGo', - searchUrl: 'https://duckduckgo.com/?q={searchTerms}', rank: 3, isCharitable: false, inputPrompt: 'Search DuckDuckGo', @@ -46,7 +46,7 @@ const getMockProps = () => ({ searchEngine: { name: 'Google', engineId: 'Google', - searchUrl: 'https://www.google.com/search?q={searchTerms}', + searchUrlPersonalized: 'https://www.google.com/search?q={searchTerms}', inputPrompt: 'Search Google', }, yahooPaidSearchRewardOptIn: true, @@ -97,33 +97,72 @@ describe('SearchInput component', () => { expect(wrapper.find(SearchSelect).first().prop('open')).toEqual(false) }) - it('onSearchEngineSwitch passed to SearchInput changes the search engine result page', async () => { + it("searches with the updated URL when the user's search engine changes", async () => { expect.assertions(2) const SearchInput = require('src/components/SearchInput').default const mockProps = getMockProps() const wrapper = mount() + const searchTextField = wrapper.find(Input) + searchTextField + .find('input') + .simulate('change', { target: { value: 'test' } }) + searchTextField.find('input').simulate('keypress', { key: 'Enter' }) + await flushAllPromises() + expect(windowOpenTop).toHaveBeenCalledWith( + 'https://www.google.com/search?q=' + ) + const updatedProps = { + ...mockProps, + user: { + ...mockProps.user, + searchEngine: { + ...mockProps.user.searchEngine, + name: 'DuckDuckGo', + engineId: 'DuckDuckGo', + rank: 3, + isCharitable: false, + inputPrompt: 'Search DuckDuckGo', + searchUrlPersonalized: 'https://duckduckgo.com/?q=', + }, + }, + } + wrapper.setProps(updatedProps) + wrapper.update() + searchTextField + .find('input') + .simulate('change', { target: { value: 'test' } }) + searchTextField.find('input').simulate('keypress', { key: 'Enter' }) + await flushAllPromises() + expect(windowOpenTop).toHaveBeenCalledWith('https://duckduckgo.com/?q=') + }) + it('calls SetUserSearchEngineMutation when onSearchEngineSwitch from SearchInput is called', async () => { + expect.assertions(1) + const SearchInput = require('src/components/SearchInput').default + const mockProps = getMockProps() + const wrapper = mount() + SetUserSearchEngineMutation.mockResolvedValue({ + name: 'DuckDuckGo', + engineId: 'DuckDuckGo', + rank: 3, + isCharitable: false, + inputPrompt: 'Search DuckDuckGo', + searchUrlPersonalized: 'https://duckduckgo.com/?q=', + }) act(() => { wrapper.find(SearchSelect).first().prop('onSearchEngineSwitch')( 'DuckDuckGo' ) }) wrapper.update() - - expect(wrapper.find(Input).first().prop('placeholder')).toEqual( - 'Search DuckDuckGo' - ) - const searchTextField = wrapper.find(Input) - searchTextField - .find('input') - .simulate('change', { target: { value: 'test' } }) - searchTextField.find('input').simulate('keypress', { key: 'Enter' }) - await flushAllPromises() - expect(windowOpenTop).toHaveBeenCalledWith('https://duckduckgo.com/?q=') + expect(SetUserSearchEngineMutation).toHaveBeenCalledWith( + 'abcdefghijklmno', + 'DuckDuckGo' + ) }) - it('calls LogTab mutation onSearch', () => { + it('calls LogSearchMutation on search', () => { const SearchInput = require('src/components/SearchInput').default const mockProps = getMockProps() const wrapper = mount() @@ -138,7 +177,7 @@ describe('SearchInput component', () => { }) }) - it('calls a redirect to the search engine result page (and does not throw or log) if LogTabMutation takes a really long time to resolve', async () => { + it('calls a redirect to the search engine result page (and does not throw or log) if LogSearchMutation takes a really long time to resolve', async () => { expect.assertions(2) const SearchInput = require('src/components/SearchInput').default const mockProps = getMockProps() @@ -158,7 +197,7 @@ describe('SearchInput component', () => { expect(logger.error).not.toHaveBeenCalled() }) - it('calls a redirect to the search engine result page (and logs but does not throw) if LogTabMutation rejects', async () => { + it('calls a redirect to the search engine result page (and logs but does not throw) if LogSearchMutation rejects', async () => { expect.assertions(2) const SearchInput = require('src/components/SearchInput').default const mockProps = getMockProps() @@ -199,7 +238,6 @@ describe('SearchInput component', () => { expect(searchSelect.prop('userSearchEngine')).toEqual( mockProps.user.searchEngine ) - expect(searchSelect.prop('userId')).toEqual(mockProps.userId) expect(searchSelect.prop('searchEngines')).toEqual( mockProps.app.searchEngines ) diff --git a/src/components/__tests__/SearchSelect.test.js b/src/components/__tests__/SearchSelect.test.js index a42b15d5..c5dd923c 100644 --- a/src/components/__tests__/SearchSelect.test.js +++ b/src/components/__tests__/SearchSelect.test.js @@ -2,7 +2,6 @@ import React from 'react' import { mount, shallow } from 'enzyme' -import SetUserSearchEngineMutation from 'src/utils/mutations/SetUserSearchEngineMutation' import ToggleButton from '@material-ui/lab/ToggleButton' import Button from '@material-ui/core/Button' import { act } from 'react-dom/test-utils' @@ -18,7 +17,6 @@ const getMockProps = () => ({ userSearchEngine: { name: 'Google', engineId: 'Google', - searchUrl: 'https://www.google.com/search?q={searchTerms}', inputPrompt: 'Search Google', }, onMoreInfoClick: jest.fn(), @@ -29,7 +27,6 @@ const getMockProps = () => ({ node: { name: 'DuckDuckGo', engineId: 'DuckDuckGo', - searchUrl: 'https://duckduckgo.com/?q={searchTerms}', rank: 3, isCharitable: false, inputPrompt: 'Search DuckDuckGo', @@ -39,7 +36,6 @@ const getMockProps = () => ({ node: { name: 'Google', engineId: 'Google', - searchUrl: 'https://www.google.com/search?q={searchTerms}', rank: 1, isCharitable: false, inputPrompt: 'Search Google', @@ -49,7 +45,6 @@ const getMockProps = () => ({ node: { name: 'Ecosia', engineId: 'Ecosia', - searchUrl: 'https://www.ecosia.org/search?q={searchTerms}', rank: 2, isCharitable: false, inputPrompt: 'Search Ecosia', @@ -104,7 +99,6 @@ describe('SearchSelect', () => { node: { name: 'Search for a Cause', engineId: 'SearchForACause', - searchUrl: 'http://tab.gladly.io/search/v2?q={searchTerms}', rank: 0, isCharitable: true, inputPrompt: 'Search for a Cause', @@ -130,22 +124,15 @@ describe('SearchSelect', () => { node: { name: 'Search for a Cause', engineId: 'SearchForACause', - searchUrl: 'http://tab.gladly.io/search/v2?q={searchTerms}', rank: 0, isCharitable: true, inputPrompt: 'Search for a Cause', }, }) const wrapper = mount() - const charitableButton = wrapper.find(ToggleButton).first() expect(charitableButton.find(Typography).at(1).text()).toEqual('2x Impact') - charitableButton.simulate('click') - expect(SetUserSearchEngineMutation).toHaveBeenCalledWith( - mockProps.userId, - 'SearchForACause' - ) expect(mockProps.onSearchEngineSwitch).toHaveBeenCalledWith( 'SearchForACause' ) @@ -157,11 +144,6 @@ describe('SearchSelect', () => { const wrapper = mount() const duckDuckGoButton = wrapper.find(ToggleButton).at(2) duckDuckGoButton.simulate('click') - - expect(SetUserSearchEngineMutation).toHaveBeenCalledWith( - mockProps.userId, - 'DuckDuckGo' - ) expect(mockProps.onSearchEngineSwitch).toHaveBeenCalledWith('DuckDuckGo') }) diff --git a/src/schema/schema.graphql b/src/schema/schema.graphql index 49fdc368..157f74a0 100644 --- a/src/schema/schema.graphql +++ b/src/schema/schema.graphql @@ -1048,18 +1048,12 @@ type RestartMissionPayload { """all important data for a search engine.""" type SearchEngine implements Node { - """The ID of an object""" - id: ID! - """Engine's id""" engineId: String! """Name of the Search Engine""" name: String! - """query string to redirect the user to after using the search bar""" - searchUrl: String! - """what order to display the search engine in a list""" rank: Int! @@ -1068,6 +1062,15 @@ type SearchEngine implements Node { """Display string to display in the search bar""" inputPrompt: String! + + """The ID of an object""" + id: ID! + + """ + A search destination URL, with a {searchTerms} placeholder for the client to + replace. Use `user.searchEngine` if the user is authenticated. + """ + searchUrl: String! } """A connection to a list of items.""" @@ -1088,6 +1091,36 @@ type SearchEngineEdge { cursor: String! } +""" +SearchEngineType extended with fields potentially personalized to the user +""" +type SearchEnginePersonalized implements Node { + """Engine's id""" + engineId: String! + + """Name of the Search Engine""" + name: String! + + """what order to display the search engine in a list""" + rank: Int! + + """Whether or not the user can earn extra impact with this Search Engine""" + isCharitable: Boolean! + + """Display string to display in the search bar""" + inputPrompt: String! + + """ + Use this for the user's search behavior. A search destination URL, with a + {searchTerms} placeholder for the client to replace. The URL might be + personalized based on the user. + """ + searchUrlPersonalized: String! + + """The ID of an object""" + id: ID! +} + """Info about any rate-limiting for VC earned from search queries""" type SearchRateLimit { """ @@ -1552,10 +1585,10 @@ type User implements Node { hasSeenSquads: Boolean! """feature values for this specific user""" - features: [Feature] + features: [Feature]! """the User’s search engine""" - searchEngine: SearchEngine + searchEngine: SearchEnginePersonalized """whether to show the yahoo search prompt""" showYahooPrompt: Boolean! diff --git a/src/utils/mutations/SetUserSearchEngineMutation.js b/src/utils/mutations/SetUserSearchEngineMutation.js index 29870b40..edcf6e9a 100644 --- a/src/utils/mutations/SetUserSearchEngineMutation.js +++ b/src/utils/mutations/SetUserSearchEngineMutation.js @@ -9,7 +9,7 @@ const mutation = graphql` searchEngine { engineId name - searchUrl + searchUrlPersonalized rank isCharitable inputPrompt