From c3ea60bbd64bfbd0743359a6494c2075bf84df2f Mon Sep 17 00:00:00 2001 From: FinnIckler Date: Mon, 9 Dec 2024 13:40:24 +0100 Subject: [PATCH 1/4] Migrate Users List to React --- app/controllers/users_controller.rb | 6 +- app/views/users/index.html.erb | 34 +------ .../CompetitionsFilters.js | 2 +- app/webpacker/components/Users/List/index.jsx | 93 +++++++++++++++++++ .../components/Users/api/getUsers.js | 16 ++++ app/webpacker/lib/requests/routes.js.erb | 1 + 6 files changed, 115 insertions(+), 37 deletions(-) create mode 100644 app/webpacker/components/Users/List/index.jsx create mode 100644 app/webpacker/components/Users/api/getUsers.js diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index c22cdb65521..4ad709de5dd 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -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, } diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb index 940560777c6..e8ef36a3a39 100644 --- a/app/views/users/index.html.erb +++ b/app/views/users/index.html.erb @@ -1,37 +1,5 @@ <% provide(:title, 'Users') %>
-

<%= yield(:title) %>

- -
-
- <%= select_tag(:region, region_option_tags(selected_id: params[:region], real_only: true), class: "form-control") %> -
- -
- - <%= 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 %> - - - WCA ID - Name - Country - Email - - - - - - - <% end %> + <%= react_component("Users/List") %>
diff --git a/app/webpacker/components/CompetitionsOverview/CompetitionsFilters.js b/app/webpacker/components/CompetitionsOverview/CompetitionsFilters.js index 98b05d96b66..38807b97c4d 100644 --- a/app/webpacker/components/CompetitionsOverview/CompetitionsFilters.js +++ b/app/webpacker/components/CompetitionsOverview/CompetitionsFilters.js @@ -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' }, { diff --git a/app/webpacker/components/Users/List/index.jsx b/app/webpacker/components/Users/List/index.jsx new file mode 100644 index 00000000000..377267978c4 --- /dev/null +++ b/app/webpacker/components/Users/List/index.jsx @@ -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 ( + + + + ); +} + +function PersonList() { + const [query, setQuery] = useState(''); + const [page, setPage] = useState(1); + const [region, setRegion] = useState('all'); + + const debouncedSearch = useDebounce(query, 1000); + + const { data, isLoading } = useQuery({ + queryKey: ['persons', debouncedSearch, region, page], + queryFn: () => getPersons(page, countries.byIso2[region]?.id ?? region, debouncedSearch), + }); + + if (isLoading) { + return ; + } + + return ( + <> +
+ Users +
+ setRegion(r)} /> + setQuery(d.target.value)} /> + + + + + WCA ID + + + Name + + + Country + + + Email + + + + + + {data.rows.map((row) => ( + + + {row.wca_id} + + + {row.name} + + + {countries.byIso2[row.country]?.name} + + + {row.email} + + + Edit + + + ))} + +
+ setPage(p.activePage)} + /> + + ); +} diff --git a/app/webpacker/components/Users/api/getUsers.js b/app/webpacker/components/Users/api/getUsers.js new file mode 100644 index 00000000000..2205915b535 --- /dev/null +++ b/app/webpacker/components/Users/api/getUsers.js @@ -0,0 +1,16 @@ +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®ion=${region ?? 'all'}`, + { + headers: { + Accept: 'application/json, text/javascript, */*; q=0.01', + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + }, + }, + ); + return data; +} diff --git a/app/webpacker/lib/requests/routes.js.erb b/app/webpacker/lib/requests/routes.js.erb index b61bad7bb12..5b182eb110a 100644 --- a/app/webpacker/lib/requests/routes.js.erb +++ b/app/webpacker/lib/requests/routes.js.erb @@ -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}"))%>`; From 6b5f3a58f089a678e7baea931aca9e2015ace371 Mon Sep 17 00:00:00 2001 From: FinnIckler Date: Mon, 9 Dec 2024 13:43:46 +0100 Subject: [PATCH 2/4] remove js code --- app/assets/javascripts/users.js | 101 +++++++------------------------- 1 file changed, 21 insertions(+), 80 deletions(-) diff --git a/app/assets/javascripts/users.js b/app/assets/javascripts/users.js index 114d7034934..255e9f891ef 100644 --- a/app/assets/javascripts/users.js +++ b/app/assets/javascripts/users.js @@ -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); - }); -}); From 465ed9a263704911ed1af4d4172827fe40b05ac2 Mon Sep 17 00:00:00 2001 From: FinnIckler Date: Mon, 9 Dec 2024 14:39:03 +0100 Subject: [PATCH 3/4] remove unnecessary headers --- app/webpacker/components/Users/api/getUsers.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/webpacker/components/Users/api/getUsers.js b/app/webpacker/components/Users/api/getUsers.js index 2205915b535..004e3ea5def 100644 --- a/app/webpacker/components/Users/api/getUsers.js +++ b/app/webpacker/components/Users/api/getUsers.js @@ -6,9 +6,7 @@ export async function getPersons(page, region, query) { `${usersUrl}?search=${query}&sort=name&order=asc&offset=${(page - 1) * 10}&limit=10®ion=${region ?? 'all'}`, { headers: { - Accept: 'application/json, text/javascript, */*; q=0.01', - 'Content-Type': 'application/json', - 'X-Requested-With': 'XMLHttpRequest', + Accept: 'application/json', }, }, ); From 5a264595b1b11e1bdca4d864c5c23a875a897de1 Mon Sep 17 00:00:00 2001 From: FinnIckler Date: Mon, 9 Dec 2024 14:54:34 +0100 Subject: [PATCH 4/4] ignore old assets in linting --- .eslintrc.json | 3 ++ app/assets/javascripts/users.js | 42 +++++++++---------- app/webpacker/components/Users/List/index.jsx | 2 +- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 7db1103a108..7a1a4ff4bbb 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,6 +4,9 @@ "es6": true, "jquery": true }, + "ignorePatterns": [ + "app/assets/javascripts/*" + ], "extends": [ "plugin:react/recommended", "plugin:react-hooks/recommended", diff --git a/app/assets/javascripts/users.js b/app/assets/javascripts/users.js index 255e9f891ef..7d6d6f0bfc3 100644 --- a/app/assets/javascripts/users.js +++ b/app/assets/javascripts/users.js @@ -1,44 +1,44 @@ -onPage('users#edit, users#update', () => { +onPage('users#edit, users#update', function() { // Hide/show avatar picker based on if the user is trying to to remove // the current avatar. - $('input#user_remove_avatar').on('change', (e) => { - const toDelete = e.currentTarget.checked; + $('input#user_remove_avatar').on("change", function(e) { + var toDelete = e.currentTarget.checked; $('.form-group.user_avatar').toggle(!toDelete); - }).trigger('change'); + }).trigger("change"); - 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()); + 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()); $unconfirmed_wca_id.val(''); $unconfirmed_wca_id.trigger('input'); }); - $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.on("input", function(e) { + var 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 () { - const section = $(this).attr('href').slice(1); - window.wca.setUrlParams({ section }); + $('a[data-toggle="tab"]').on('show.bs.tab', function() { + var section = $(this).attr('href').slice(1); + window.wca.setUrlParams({ section: section }); }); // Require the user to confirm reading guidelines. - $('#upload-avatar-form input[type="submit"]').on('click', (event) => { - const $confirmation = $('#guidelines-confirmation'); - if (!$confirmation[0].checked) { + $('#upload-avatar-form input[type="submit"]').on('click', function(event) { + var $confirmation = $('#guidelines-confirmation'); + if(!$confirmation[0].checked) { event.preventDefault(); alert($confirmation.data('alert')); } }); // Show avatar removal confirmation form - $(document).ready(() => { - $('.remove-avatar').click(() => { + $(document).ready(function(){ + $('.remove-avatar').click(function(){ $('.remove-avatar-confirm').show(); }); }); diff --git a/app/webpacker/components/Users/List/index.jsx b/app/webpacker/components/Users/List/index.jsx index 377267978c4..4e0466bed11 100644 --- a/app/webpacker/components/Users/List/index.jsx +++ b/app/webpacker/components/Users/List/index.jsx @@ -25,7 +25,7 @@ function PersonList() { const [page, setPage] = useState(1); const [region, setRegion] = useState('all'); - const debouncedSearch = useDebounce(query, 1000); + const debouncedSearch = useDebounce(query, 600); const { data, isLoading } = useQuery({ queryKey: ['persons', debouncedSearch, region, page],