forked from thewca/worldcubeassociation.org
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Replace WCA ID input with user search (thewca#8422)
* Replace WCA ID input with user search * Brought forward the UserSearch from thewca#7459 * Review changes
- Loading branch information
1 parent
77acf97
commit 6a012e9
Showing
8 changed files
with
263 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
136 changes: 136 additions & 0 deletions
136
WcaOnRails/app/webpacker/components/SearchWidget/MultiSearchInput.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import React, { useState, useEffect, useCallback } from 'react'; | ||
import { Dropdown } from 'semantic-ui-react'; | ||
|
||
import CompetitionItem from './CompetitionItem'; | ||
import IncidentItem from './IncidentItem'; | ||
import RegulationItem from './RegulationItem'; | ||
import UserItem from './UserItem'; | ||
import TextItem from './TextItem'; | ||
import useDebounce from '../../lib/hooks/useDebounce'; | ||
import I18n from '../../lib/i18n'; | ||
import { fetchJsonOrError } from '../../lib/requests/fetchWithAuthenticityToken'; | ||
|
||
const classToComponent = { | ||
user: UserItem, | ||
person: UserItem, | ||
competition: CompetitionItem, | ||
regulation: RegulationItem, | ||
text: TextItem, | ||
incident: IncidentItem, | ||
}; | ||
|
||
function ItemFor({ item }) { | ||
const Component = classToComponent[item.class]; | ||
return ( | ||
<div className="selected-item"> | ||
<Component item={item} /> | ||
</div> | ||
); | ||
} | ||
|
||
const renderLabel = ({ item }) => ({ | ||
color: 'blue', | ||
content: <ItemFor item={item} />, | ||
className: 'omnisearch-item', | ||
as: 'div', | ||
}); | ||
|
||
export const itemToOption = (item) => ({ | ||
item, | ||
id: item.id, | ||
key: item.id, | ||
value: item.id, | ||
// 'text' is used by the search method from the component, we need to put | ||
// the text with a potential match here! | ||
text: [item.id, item.name, item.title, item.content_html, item.search, item.public_summary].join(' '), | ||
content: <ItemFor item={item} />, | ||
}); | ||
|
||
const createSearchItem = (search) => itemToOption({ | ||
class: 'text', | ||
id: 'search', | ||
url: `/search?q=${encodeURIComponent(search)}`, | ||
search, | ||
}); | ||
|
||
const DEBOUNCE_MS = 300; | ||
|
||
function MultiSearchInput({ | ||
url, | ||
goToItemOnSelect, | ||
placeholder, | ||
removeNoResultsMessage, | ||
selectedItems, | ||
disabled = false, | ||
multiple = true, | ||
onChange, | ||
}) { | ||
const [search, setSearch] = useState(''); | ||
const [results, setResults] = useState([]); | ||
const [loading, setLoading] = useState(false); | ||
|
||
const debouncedSearch = useDebounce(search, DEBOUNCE_MS); | ||
|
||
// wrap the 'onChange' handler because we want to reset the search string | ||
const onChangeInternal = useCallback((_, { value }) => { | ||
setSearch(''); | ||
onChange(value); | ||
}, [onChange, setSearch]); | ||
|
||
useEffect(() => { | ||
// Do nothing if search string is empty: we're just loading the page | ||
// or we just selected an item. | ||
// Either way, we want to keep the existing results. | ||
if (debouncedSearch.length === 0) return; | ||
if (debouncedSearch.length < 3) { | ||
setResults([]); | ||
} else { | ||
setLoading(true); | ||
// Note: we don't need to do any filtering on the results here, | ||
// FUI's dropdown will automatically remove selected items from the | ||
// options left for selection. | ||
fetchJsonOrError(url(debouncedSearch)) | ||
.then(({ data }) => setResults(data.result.map(itemToOption))) | ||
.finally(() => setLoading(false)); | ||
} | ||
}, [debouncedSearch, url]); | ||
|
||
const options = [...selectedItems, ...results].map((option) => ({ | ||
...option, | ||
text: <ItemFor item={option.item} />, | ||
})); | ||
|
||
// If we go to item on select, we want to give the user the option to go to | ||
// the search page. | ||
if (goToItemOnSelect && search.length > 0) { | ||
options.unshift(createSearchItem(search)); | ||
} | ||
|
||
// FIXME: the search filter from FUI is not the greatest: when searching for | ||
// "galerie lafa" it won't match the "galeries lafayette" competitions | ||
// (whereas searching for "galeries lafa" does). | ||
// We should try to set our own search method that would match word by word. | ||
return ( | ||
<Dropdown | ||
fluid | ||
selection | ||
multiple={multiple} | ||
search={(values) => values.slice(0, 5)} | ||
clearable={!multiple} | ||
icon="search" | ||
className="omnisearch-dropdown" | ||
disabled={disabled} | ||
value={multiple ? selectedItems.map((item) => item.id) : selectedItems[0]?.id} | ||
searchQuery={search} | ||
options={options} | ||
onChange={onChangeInternal} | ||
onSearchChange={(e, { searchQuery }) => setSearch(searchQuery)} | ||
loading={loading} | ||
noResultsMessage={removeNoResultsMessage ? null : I18n.t('search_results.index.not_found.generic')} | ||
placeholder={placeholder || I18n.t('common.search_site')} | ||
renderLabel={renderLabel} | ||
/> | ||
); | ||
} | ||
|
||
export default MultiSearchInput; |
52 changes: 52 additions & 0 deletions
52
WcaOnRails/app/webpacker/components/SearchWidget/UserSearch.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import React, { useCallback, useMemo } from 'react'; | ||
import { userSearchApiUrl, userApiUrl } from '../../lib/requests/routes.js.erb'; | ||
import Loading from '../Requests/Loading'; | ||
import MultiSearchInput, { itemToOption } from './MultiSearchInput'; | ||
import { useManyLoadedData } from '../../lib/hooks/useLoadedData'; | ||
|
||
export default function UserSearch({ | ||
value, | ||
onChange, | ||
delegateOnly = false, | ||
traineeOnly = false, | ||
multiple = true, | ||
}) { | ||
const userIds = useMemo(() => { | ||
if (multiple) return value || []; | ||
return value ? [value] : []; | ||
}, [value, multiple]); | ||
|
||
const queryParams = useMemo(() => { | ||
const params = new URLSearchParams(); | ||
|
||
if (delegateOnly) params.append('only_staff_delegates', true); | ||
if (traineeOnly) params.append('only_trainee_delegates', true); | ||
|
||
return params; | ||
}, [delegateOnly, traineeOnly]); | ||
|
||
const userSearchApiUrlFn = useCallback((query) => `${userSearchApiUrl(query)}&${queryParams.toString()}`, [queryParams]); | ||
|
||
const { | ||
data, | ||
anyLoading, | ||
} = useManyLoadedData(userIds, userApiUrl); | ||
|
||
const preSelected = useMemo( | ||
// the users API actually returns users in the format { "user": stuff_you_are_interested_in } | ||
() => Object.values(data).map((item) => itemToOption(item.user)), | ||
[data], | ||
); | ||
|
||
if (anyLoading) return <Loading />; | ||
|
||
return ( | ||
<MultiSearchInput | ||
url={userSearchApiUrlFn} | ||
goToItemOnSelect={false} | ||
selectedItems={preSelected} | ||
onChange={onChange} | ||
multiple={multiple} | ||
/> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters