Skip to content
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

Migrate Users List to React #10402

Merged
merged 4 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 21 additions & 80 deletions app/assets/javascripts/users.js
Original file line number Diff line number Diff line change
@@ -1,104 +1,45 @@
onPage('users#edit, users#update', function() {
onPage('users#edit, users#update', () => {
// Hide/show avatar picker based on if the user is trying to to remove
// the current avatar.
$('input#user_remove_avatar').on("change", function(e) {
var toDelete = e.currentTarget.checked;
$('input#user_remove_avatar').on('change', (e) => {
const toDelete = e.currentTarget.checked;
$('.form-group.user_avatar').toggle(!toDelete);
}).trigger("change");
}).trigger('change');

var $approve_wca_id = $('#approve-wca-id');
var $unconfirmed_wca_id = $("#user_unconfirmed_wca_id");
var $unconfirmed_wca_id_profile_link = $("a#unconfirmed-wca-id-profile");
$approve_wca_id.on("click", function(e) {
$("#user_wca_id").val($unconfirmed_wca_id.val());
const $approve_wca_id = $('#approve-wca-id');
const $unconfirmed_wca_id = $('#user_unconfirmed_wca_id');
const $unconfirmed_wca_id_profile_link = $('a#unconfirmed-wca-id-profile');
$approve_wca_id.on('click', (e) => {
$('#user_wca_id').val($unconfirmed_wca_id.val());
$unconfirmed_wca_id.val('');
$unconfirmed_wca_id.trigger('input');
});
$unconfirmed_wca_id.on("input", function(e) {
var unconfirmed_wca_id = $unconfirmed_wca_id.val();
$approve_wca_id.prop("disabled", !unconfirmed_wca_id);
$unconfirmed_wca_id.on('input', (e) => {
const unconfirmed_wca_id = $unconfirmed_wca_id.val();
$approve_wca_id.prop('disabled', !unconfirmed_wca_id);
$unconfirmed_wca_id_profile_link.parent().toggle(!!unconfirmed_wca_id);
$unconfirmed_wca_id_profile_link.attr('href', "/persons/" + unconfirmed_wca_id);
$unconfirmed_wca_id_profile_link.attr('href', `/persons/${unconfirmed_wca_id}`);
});
$unconfirmed_wca_id.trigger('input');

// Change the 'section' parameter when a tab is switched.
$('a[data-toggle="tab"]').on('show.bs.tab', function() {
var section = $(this).attr('href').slice(1);
window.wca.setUrlParams({ section: section });
$('a[data-toggle="tab"]').on('show.bs.tab', function () {
const section = $(this).attr('href').slice(1);
window.wca.setUrlParams({ section });
});
// Require the user to confirm reading guidelines.
$('#upload-avatar-form input[type="submit"]').on('click', function(event) {
var $confirmation = $('#guidelines-confirmation');
if(!$confirmation[0].checked) {
$('#upload-avatar-form input[type="submit"]').on('click', (event) => {
const $confirmation = $('#guidelines-confirmation');
if (!$confirmation[0].checked) {
event.preventDefault();
alert($confirmation.data('alert'));
}
});

// Show avatar removal confirmation form
$(document).ready(function(){
$('.remove-avatar').click(function(){
$(document).ready(() => {
$('.remove-avatar').click(() => {
$('.remove-avatar-confirm').show();
});
});
});


// Add params from the search fields to the bootstrap-table for on Ajax request.
var usersTableAjax = {
queryParams: function(params) {
return $.extend(params || {}, {
region: $('#region').val(),
search: $('#search').val(),
});
},
doAjax: function(options) {
return window.wca.cancelPendingAjaxAndAjax('users-index', options);
},
};

onPage('users#index', function() {
var $table = $('.bs-table');
var options = $table.bootstrapTable('getOptions');
// Change bootstrap-table pagination description
options.formatRecordsPerPage = function(pageNumber) {
// Space after the input box with per page count
return pageNumber + ' users per page';
};
options.formatShowingRows = function(pageFrom, pageTo, totalRows) {
// Space before the input box with per page count
return 'Showing ' + pageFrom + ' to ' + pageTo + ' of ' + totalRows + ' users ';
};

// Set the table options from the url params.
var urlParams = window.wca.getUrlParams();
$.extend(options, {
pageNumber: parseInt(urlParams.page) || options.pageNumber,
sortOrder: urlParams.order || options.sortOrder,
sortName: urlParams.sort || options.sortName
});
// Load the data using the options set above.
$table.bootstrapTable('refresh');

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

$('#region').on('change', reloadUsers);
$('#search').on('input', window.wca.lodashDebounce(reloadUsers, 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 = usersTableAjax.queryParams({
page: options.pageNumber,
order: options.sortOrder,
sort: options.sortName
});
window.wca.setUrlParams(params);
});
});
6 changes: 3 additions & 3 deletions app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ def index
total: @users.size,
rows: @users.limit(params[:limit]).offset(params[:offset]).map do |user|
{
wca_id: user.wca_id ? view_context.link_to(user.wca_id, person_path(user.wca_id)) : "",
wca_id: user.wca_id,
name: ERB::Util.html_escape(user.name),
# Users don't have to provide a country upon registration
country: user.country&.id,
country: user.country&.iso2,
email: ERB::Util.html_escape(user.email),
edit: view_context.link_to("Edit", edit_user_path(user)),
user_id: user.id,
}
end,
}
Expand Down
34 changes: 1 addition & 33 deletions app/views/users/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,37 +1,5 @@
<% provide(:title, 'Users') %>

<div class="container">
<h2><%= yield(:title) %></h2>

<div id="users-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",
data: { toggle: "tooltip", placement: "top" },
title: "Type name, WCA ID, or email. Use a space to separate them." %>
<%= text_field_tag :search, params[:search], class: "form-control" %>
</div>
</div>
</div>

<%= wca_table table_class: "users-table",
data: { toggle: "table", pagination: "true", side_pagination: "server", url: users_url,
query_params: "usersTableAjax.queryParams", ajax: "usersTableAjax.doAjax",
sort_name: "name", undefined_text: "" } do %>
<thead>
<tr>
<th class="wca-id" data-field="wca_id" data-sortable="true">WCA ID</th>
<th class="name" data-field="name" data-sortable="true">Name</th>
<th class="country" data-field="country" data-sortable="true">Country</th>
<th class="email" data-field="email" data-sortable="true">Email</th>
<th class="edit" data-field="edit"></th>

<!-- Extra column for .table-greedy-last-column -->
<th></th>
</tr>
</thead>
<% end %>
<%= react_component("Users/List") %>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export function EventSelector({
);
}

function RegionSelector({ region, dispatchFilter }) {
export function RegionSelector({ region, dispatchFilter }) {
const regionsOptions = [
{ key: 'all', text: I18n.t('common.all_regions'), value: 'all' },
{
Expand Down
93 changes: 93 additions & 0 deletions app/webpacker/components/Users/List/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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/getUsers';
import Loading from '../../Requests/Loading';
import WCAQueryClientProvider from '../../../lib/providers/WCAQueryClientProvider';
import { RegionSelector } from '../../CompetitionsOverview/CompetitionsFilters';
import { personUrl, editPersonUrl } 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, 1000);
FinnIckler marked this conversation as resolved.
Show resolved Hide resolved

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

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

return (
<>
<Header>
Users
</Header>
<RegionSelector region={region} dispatchFilter={({ region: r }) => setRegion(r)} />
<Input type="text" placeholder="Type name, WCA ID, or email. Use a space to separate them." value={query} onChange={(d) => setQuery(d.target.value)} />
<Table striped>
<Table.Header>
<Table.Row>
<Table.HeaderCell>
WCA ID
</Table.HeaderCell>
<Table.HeaderCell>
Name
</Table.HeaderCell>
<Table.HeaderCell>
Country
</Table.HeaderCell>
<Table.HeaderCell>
Email
</Table.HeaderCell>
<Table.HeaderCell />
</Table.Row>
</Table.Header>
<Table.Body>
{data.rows.map((row) => (
<Table.Row key={`${row.wca_id}-${row.name}`}>
<Table.Cell>
{row.wca_id}
</Table.Cell>
<Table.Cell>
<a href={personUrl(row.wca_id)}>{row.name}</a>
</Table.Cell>
<Table.Cell>
{countries.byIso2[row.country]?.name}
</Table.Cell>
<Table.Cell>
{row.email}
</Table.Cell>
<Table.Cell>
<a href={editPersonUrl(row.user_id)}>Edit</a>
</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/Users/api/getUsers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { fetchJsonOrError } from '../../../lib/requests/fetchWithAuthenticityToken';
import { usersUrl } 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(
`${usersUrl}?search=${query}&sort=name&order=asc&offset=${(page - 1) * 10}&limit=10&region=${region ?? 'all'}`,
{
headers: {
Accept: 'application/json',
},
},
);
return data;
}
1 change: 1 addition & 0 deletions app/webpacker/lib/requests/routes.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const editPersonUrl = (userId) => `<%= CGI.unescape(Rails.application.rou
export const personUrl = (wcaId) => `<%= CGI.unescape(Rails.application.routes.url_helpers.person_path("${wcaId}"))%>`;

export const personsUrl = `<%= CGI.unescape(Rails.application.routes.url_helpers.persons_path) %>`;
export const usersUrl = `<%= CGI.unescape(Rails.application.routes.url_helpers.users_path) %>`;

export const newResultUrl = (competitionId, roundId) => `<%= CGI.unescape(Rails.application.routes.url_helpers.competition_new_result_path("${competitionId}", "${roundId}"))%>`;
export const resultUrl = (id) => `<%= CGI.unescape(Rails.application.routes.url_helpers.result_path("${id}"))%>`;
Expand Down
Loading