Skip to content

Commit

Permalink
Improved Search in React & added support for persons model (thewca#8493)
Browse files Browse the repository at this point in the history
* Improved Search in React & added support for persons model

* Review changes
  • Loading branch information
danieljames-dj authored Nov 14, 2023
1 parent bcf6ea1 commit 3c1f4ab
Show file tree
Hide file tree
Showing 9 changed files with 78 additions and 136 deletions.
4 changes: 4 additions & 0 deletions WcaOnRails/app/controllers/api/v0/api_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ def users_search
search(User)
end

def persons_search
search(Person)
end

def regulations_search
search(Regulation)
end
Expand Down
2 changes: 1 addition & 1 deletion WcaOnRails/app/models/person.rb
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ def gender_visible?
%w(m f).include? gender
end

def self.search(query)
def self.search(query, params: {})
persons = Person.current.includes(:user)
query.split.each do |part|
persons = persons.where("name LIKE :part OR wca_id LIKE :part", part: "%#{part}%")
Expand Down
20 changes: 10 additions & 10 deletions WcaOnRails/app/webpacker/components/DelegateProbations/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import {
endDelegateProbationUrl,
} from '../../lib/requests/routes.js.erb';
import useSaveAction from '../../lib/hooks/useSaveAction';
import useInputState from '../../lib/hooks/useInputState';
import UserSearch from '../SearchWidget/UserSearch';
import WcaSearch from '../SearchWidget/WcaSearch';
import Errored from '../Requests/Errored';

const dateFormat = 'YYYY-MM-DD';
Expand Down Expand Up @@ -62,7 +61,7 @@ function ProbationListTable({
}

export default function DelegateProbations() {
const [userId, setUserId] = React.useState();
const [user, setUser] = React.useState();
const {
data, loading, error, sync,
} = useLoadedData(delegateProbationDataUrl);
Expand All @@ -76,18 +75,19 @@ export default function DelegateProbations() {
return (
<>
<h1>Delegate Probations</h1>
<UserSearch
value={userId}
onChange={setUserId}
<WcaSearch
selectedValue={user}
setSelectedValue={setUser}
multiple={false}
delegateOnly
model="user"
params={{ only_staff_delegates: true }}
/>
<Button
onClick={() => save(startDelegateProbationUrl, { userId }, () => {
onClick={() => save(startDelegateProbationUrl, { userId: user.id }, () => {
sync();
setUserId(null);
setUser(null);
}, { method: 'POST' })}
disabled={!userId}
disabled={!user}
>
Start Probation
</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect } from 'react';
import { Dropdown } from 'semantic-ui-react';

import CompetitionItem from './CompetitionItem';
Expand Down Expand Up @@ -55,27 +55,28 @@ const createSearchItem = (search) => itemToOption({

const DEBOUNCE_MS = 300;

function MultiSearchInput({
export default function MultiSearchInput({
url,
goToItemOnSelect,
// If multiple is true, selectedValue is an array of items, otherwise it's a single item.
selectedValue,
// If multiple is true, setSelectedValue is a function that takes an array of items, otherwise
// it's a function that takes a single item.
setSelectedValue,
showOptionToGoToSearchPage = false,
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 }) => {
useEffect(() => {
setSearch('');
onChange(value);
}, [onChange, setSearch]);
}, [selectedValue]);

useEffect(() => {
// Do nothing if search string is empty: we're just loading the page
Expand All @@ -95,21 +96,27 @@ function MultiSearchInput({
}
}, [debouncedSearch, url]);

const options = [...selectedItems, ...results].map((option) => ({
const dropDownOptions = [
...(showOptionToGoToSearchPage && search.length > 0 ? [createSearchItem(search)] : []),
...(multiple ? selectedValue : []),
...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));
}
const onChangeInternal = (_, { value, options }) => {
const map = {};
options.forEach((option) => {
map[option.value] = option;
});
if (multiple) {
setSelectedValue(value.map((id) => itemToOption(map[id].item)));
} else {
setSelectedValue(map[value] ? itemToOption(map[value].item) : null);
}
};

// 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
Expand All @@ -120,9 +127,9 @@ function MultiSearchInput({
icon="search"
className="omnisearch-dropdown"
disabled={disabled}
value={multiple ? selectedItems.map((item) => item.id) : selectedItems[0]?.id}
value={multiple ? selectedValue.map((item) => item.id) : selectedValue?.id}
searchQuery={search}
options={options}
options={dropDownOptions}
onChange={onChangeInternal}
onSearchChange={(e, { searchQuery }) => setSearch(searchQuery)}
loading={loading}
Expand All @@ -132,5 +139,3 @@ function MultiSearchInput({
/>
);
}

export default MultiSearchInput;
52 changes: 0 additions & 52 deletions WcaOnRails/app/webpacker/components/SearchWidget/UserSearch.jsx

This file was deleted.

32 changes: 32 additions & 0 deletions WcaOnRails/app/webpacker/components/SearchWidget/WcaSearch.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, {
useCallback,
} from 'react';

import { userSearchApiUrl, personSearchApiUrl } from '../../lib/requests/routes.js.erb';
import MultiSearchInput from './MultiSearchInput';

export default function WcaSearch({
selectedValue,
setSelectedValue,
multiple = true,
model,
params,
}) {
const urlFn = useCallback((query) => {
if (model === 'user') {
return `${userSearchApiUrl(query)}&${new URLSearchParams(params).toString()}`;
} if (model === 'person') {
return `${personSearchApiUrl(query)}&${new URLSearchParams(params).toString()}`;
}
return '';
}, [params, model]);

return (
<MultiSearchInput
url={urlFn}
selectedValue={multiple ? selectedValue || [] : selectedValue}
setSelectedValue={setSelectedValue}
multiple={multiple}
/>
);
}
50 changes: 1 addition & 49 deletions WcaOnRails/app/webpacker/lib/hooks/useLoadedData.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import {
useState, useEffect, useCallback, useMemo,
} from 'react';
import { useState, useEffect, useCallback } from 'react';
import { fetchJsonOrError } from '../requests/fetchWithAuthenticityToken';

// This is a hook that can be used to get a data from the website (as json)
Expand Down Expand Up @@ -35,50 +33,4 @@ const useLoadedData = (url) => {
};
};

export const useManyLoadedData = (ids, urlFn) => {
const [data, setData] = useState({});
const [headers, setHeaders] = useState({});
const [error, setError] = useState({});

const [anyLoading, setAnyLoading] = useState(true);

const promises = useMemo(() => ids.map(async (id) => {
const url = urlFn(id);

try {
const response = await fetchJsonOrError(url);
setData((prevData) => ({
...prevData,
[id]: response.data,
}));
setHeaders((prevHeaders) => ({
...prevHeaders,
[id]: response.headers,
}));
} catch (err) {
setError((prevError) => ({
...prevError,
[id]: err.message,
}));
}
}), [ids, urlFn, setData, setHeaders, setError]);

const syncAll = useCallback(() => {
setAnyLoading(true);
setData({});
setError({});
Promise.all(promises).finally(() => setAnyLoading(false));
}, [promises]);

useEffect(syncAll, [syncAll]);

return {
data,
headers,
anyLoading,
error,
syncAll,
};
};

export default useLoadedData;
2 changes: 1 addition & 1 deletion WcaOnRails/app/webpacker/lib/requests/routes.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const adminGenerateIds = `<%= CGI.unescape(Rails.application.routes.url_h

export const personApiUrl = (wcaId) => `<%= CGI.unescape(Rails.application.routes.url_helpers.api_v0_person_path("${wcaId}"))%>`;

export const userApiUrl = (id) => `<%= CGI.unescape(Rails.application.routes.url_helpers.api_v0_user_path("${id}"))%>`;
export const personSearchApiUrl = (query) => `<%= Rails.application.routes.url_helpers.api_v0_search_persons_path %>?q=${query}`;

export const userSearchApiUrl = (query) => `<%= Rails.application.routes.url_helpers.api_v0_search_users_path %>?q=${query}`;

Expand Down
1 change: 1 addition & 0 deletions WcaOnRails/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@
get '/search/posts' => 'api#posts_search'
get '/search/competitions' => 'api#competitions_search'
get '/search/users' => 'api#users_search', as: :search_users
get '/search/persons' => 'api#persons_search', as: :search_persons
get '/search/regulations' => 'api#regulations_search'
get '/search/incidents' => 'api#incidents_search'
get '/users/:id' => 'api#show_user_by_id', constraints: { id: /\d+/ }
Expand Down

0 comments on commit 3c1f4ab

Please sign in to comment.