Skip to content

Commit

Permalink
Migrate Persons Index to React (#10378)
Browse files Browse the repository at this point in the history
* Add new Persons Page

* new lib code

* export strings

* remove jquery code

* remove XMLHTTPRequest

* undo persons.js linting

* Fix tests

* fix tests v2
  • Loading branch information
FinnIckler authored Dec 10, 2024
1 parent 5623850 commit 00dd39e
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 93 deletions.
50 changes: 0 additions & 50 deletions app/assets/javascripts/persons.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,3 @@

// Add params from the search fields to the bootstrap-table for on Ajax request.
var personsTableAjax = {
queryParams: function(params) {
if (personsTableAjax.queriesOK !== true) {
return false; // Stop Bootstrap Table's uncontrollable initial load
}

return $.extend(params || {}, {
region: $('#region').val(),
search: $('#search').val(),
});
},
doAjax: function(options) {
$('.pagination li').addClass('disabled');
return window.wca.cancelPendingAjaxAndAjax('persons-index', options);
},
};

onPage('persons#index', function() {
var $table = $('.persons-table');
var options = $table.bootstrapTable('getOptions');

function reloadPersons() {
$('#search-box i').removeClass('fa-search').addClass('fa-spinner fa-spin');
options.pageNumber = 1;
$table.bootstrapTable('refresh');
}

// Set the table options from the url params.
options.pageNumber = parseInt(window.wca.getUrlParams().page) || options.pageNumber;
// Load the data using the options set above.
personsTableAjax.queriesOK = true;
$table.bootstrapTable('refresh');

$('#region').on('change', reloadPersons);
$('#search').on('input', window.wca.lodashDebounce(reloadPersons, window.wca.TEXT_INPUT_DEBOUNCE_MS));

$table.on('load-success.bs.table', function(e, data) {
$('#search-box i').removeClass('fa-spinner fa-spin').addClass('fa-search');

// Update params in the url.
var params = personsTableAjax.queryParams({
// Extended with region and search params.
page: options.pageNumber
});
window.wca.setUrlParams(params);
});
});

onPage('persons#show', function() {
function scrollToTabs() {
var top = $('.nav.nav-tabs').offset().top;
Expand Down
6 changes: 3 additions & 3 deletions app/controllers/persons_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class PersonsController < ApplicationController
def index
respond_to do |format|
format.html
format.js do
format.json do
persons = Person.in_region(params[:region]).order(:name)
params[:search]&.split&.each do |part|
persons = persons.where("MATCH(Persons.name) AGAINST (:name_match IN BOOLEAN MODE) OR wca_id LIKE :wca_id_part", name_match: "#{part}*", wca_id_part: "#{part}%")
Expand All @@ -14,9 +14,9 @@ def index
total: persons.count,
rows: persons.limit(params[:limit]).offset(params[:offset]).map do |person|
{
name: view_context.link_to(person.name, person_path(person.wca_id)),
name: person.name,
wca_id: person.wca_id,
country: person.country.name,
country: person.country.iso2,
competitions_count: person.competitions.count,
podiums_count: person.results.podium.count,
}
Expand Down
36 changes: 1 addition & 35 deletions app/views/persons/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,39 +1,5 @@
<% provide(:title, t('layouts.navigation.persons')) %>

<div class="container" id="persons-index">
<h2><%= yield(:title) %></h2>

<div id="persons-query-fields" class="form-inline bs-table-query-fields">
<div class="form-group">
<%= select_tag(:region, region_option_tags(selected_id: params[:region], real_only: true), class: "form-control") %>
</div>
<div id="search-box" class="form-group">
<div class="input-group">
<%= content_tag :span, ui_icon("search"), class: "input-group-addon" %>
<%= text_field_tag :search, params[:search], placeholder: t('.name_or_wca_id'), class: "form-control" %>
</div>
</div>
</div>

<%= wca_table table_class: "persons-table", floatThead: false,
data: { toggle: "table", pagination: true, side_pagination: "server", url: persons_path,
query_params: "personsTableAjax.queryParams", mobile_responsive: true, ajax: "personsTableAjax.doAjax" } do %>
<thead>
<tr>
<th class="name" data-field="name"><%=t '.name' %></th>
<th class="wca-id" data-field="wca_id"><%=t 'common.user.wca_id' %></th>
<th class="country" data-field="country"><%=t '.country' %></th>
<th class="competitions-count" data-field="competitions_count"><%=t 'layouts.navigation.competitions' %></th>
<th class="podiums-count" data-field="podiums_count"><%=t '.podiums' %></th>
</tr>
</thead>
<% end %>
<%= react_component("Persons/List") %>
</div>

<script>
$(function() {
$('.persons-table').bootstrapTable('getOptions').formatNoMatches = function() {
return '<%=j t('.no_persons_found') %>';
}
});
</script>
100 changes: 100 additions & 0 deletions app/webpacker/components/Persons/List/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React, { useState } from 'react';
import {
Header, Input, Pagination, Table,
} from 'semantic-ui-react';
import { useQuery } from '@tanstack/react-query';
import I18n from '../../../lib/i18n';
import useDebounce from '../../../lib/hooks/useDebounce';
import { getPersons } from '../api/getPersons';
import Loading from '../../Requests/Loading';
import WCAQueryClientProvider from '../../../lib/providers/WCAQueryClientProvider';
import { RegionSelector } from '../../CompetitionsOverview/CompetitionsFilters';
import { personUrl } from '../../../lib/requests/routes.js.erb';
import { countries } from '../../../lib/wca-data.js.erb';

export default function Wrapper() {
return (
<WCAQueryClientProvider>
<PersonList />
</WCAQueryClientProvider>
);
}

function PersonList() {
const [query, setQuery] = useState('');
const [page, setPage] = useState(1);
const [region, setRegion] = useState('all');

const debouncedSearch = useDebounce(query, 600);

const { data, isLoading } = useQuery({
queryKey: ['persons', debouncedSearch, region, page],
queryFn: () => getPersons(page, countries.byIso2[region]?.id ?? region, debouncedSearch),
});

if (isLoading) {
return <Loading />;
}

return (
<>
<Header>
{I18n.t('layouts.navigation.persons')}
</Header>
<RegionSelector region={region} dispatchFilter={({ region: r }) => setRegion(r)} />
<Input type="text" placeholder={I18n.t('persons.index.name_or_wca_id')} value={query} onChange={(d) => setQuery(d.target.value)} />
<Table striped>
<Table.Header>
<Table.Row>
<Table.HeaderCell>
{I18n.t('persons.index.name')}
</Table.HeaderCell>
<Table.HeaderCell>
{I18n.t('common.user.wca_id')}
</Table.HeaderCell>
<Table.HeaderCell>
{I18n.t('persons.index.country')}
</Table.HeaderCell>
<Table.HeaderCell>
{I18n.t('layouts.navigation.competitions')}
</Table.HeaderCell>
<Table.HeaderCell>
{I18n.t('persons.index.podiums')}
</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{data.rows.length === 0 ? (
<Table.Row>
<Table.Cell>{I18n.t('persons.index.no_persons_found')}</Table.Cell>
</Table.Row>
) : data.rows.map((row) => (
<Table.Row key={row.wca_id}>
<Table.Cell>
<a href={personUrl(row.wca_id)}>{row.name}</a>
</Table.Cell>
<Table.Cell>
{row.wca_id}
</Table.Cell>
<Table.Cell>
{countries.byIso2[row.country].name}
</Table.Cell>

<Table.Cell>
{row.competitions_count}
</Table.Cell>
<Table.Cell>
{row.podiums_count}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
<Pagination
defaultActivePage={page}
totalPages={Math.ceil(data.total / 10)}
onPageChange={(e, p) => setPage(p.activePage)}
/>
</>
);
}
14 changes: 14 additions & 0 deletions app/webpacker/components/Persons/api/getPersons.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { fetchJsonOrError } from '../../../lib/requests/fetchWithAuthenticityToken';
import { personsUrl } from '../../../lib/requests/routes.js.erb';
// eslint-disable-next-line import/prefer-default-export
export async function getPersons(page, region, query) {
const { data } = await fetchJsonOrError(
`${personsUrl}?search=${query}&order=asc&offset=${(page - 1) * 10}&limit=10&region=${region ?? 'all'}`,
{
headers: {
Accept: 'application/json',
},
},
);
return data;
}
1 change: 1 addition & 0 deletions config/i18n.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,4 @@ translations:
- "*.attempts.*"
- "*.time_limit.*"
- "*.users.edit.*"
- "*.persons.index.*"
10 changes: 5 additions & 5 deletions spec/controllers/persons_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,33 @@
let!(:result) { FactoryBot.create(:result, pos: 1, roundTypeId: "f", competitionId: competition.id, person: person1) }

it "responds with correct JSON when region and search are specified" do
get :index, params: { search: "Jennifer", region: "USA" }, xhr: true
get :index, params: { search: "Jennifer", region: "USA" }, format: :json
json = JSON.parse(response.body)
expect(json['total']).to eq 1
json_person = json['rows'][0]
expect(json_person['name']).to include "Jennifer Lawrence"
expect(json_person['wca_id']).to eq "2016LAWR01"
expect(json_person['country']).to eq "United States"
expect(json_person['country']).to eq "US"
expect(json_person['competitions_count']).to eq 1
expect(json_person['podiums_count']).to eq 1
end

it "selecting continent works" do
get :index, params: { region: "_Europe" }, xhr: true
get :index, params: { region: "_Europe" }, format: :json
json = JSON.parse(response.body)
expect(json['total']).to eq 1
expect(json['rows'].count).to eq 1
end

it "searching by WCA ID works" do
get :index, params: { search: "2016" }, xhr: true
get :index, params: { search: "2016" }, format: :json
json = JSON.parse(response.body)
expect(json['total']).to eq 2
expect(json['rows'].count).to eq 2
end

it "works well when parts of the name are given" do
get :index, params: { search: "Law Jenn" }, xhr: true
get :index, params: { search: "Law Jenn" }, format: :json
json = JSON.parse(response.body)
expect(json['total']).to eq 1
expect(json['rows'].count).to eq 1
Expand Down

0 comments on commit 00dd39e

Please sign in to comment.