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 the My Competition Table to React #10370

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 20 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
4 changes: 2 additions & 2 deletions app/controllers/competitions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -842,7 +842,7 @@ def my_competitions
registrations = current_user.registrations.includes(:competition).accepted.reject { |r| r.competition.results_posted? }
registrations.concat(current_user.registrations.includes(:competition).pending.select { |r| r.competition.upcoming? })
@registered_for_by_competition_id = registrations.uniq.to_h do |r|
[r.competition.id, r]
[r.competition.id, r.as_json({ only: %w[competing_status], include: %w[], methods: %w[] })]
FinnIckler marked this conversation as resolved.
Show resolved Hide resolved
end
competition_ids.concat(@registered_for_by_competition_id.keys)
if current_user.person
Expand All @@ -851,7 +851,7 @@ def my_competitions
# An organiser might still have duties to perform for a cancelled competition until the date of the competition has passed.
# For example, mailing all competitors about the cancellation.
# In general ensuring ease of access until it is certain that they won't need to frequently visit the page anymore.
competitions = Competition.includes(:delegate_report, :delegates)
competitions = Competition.includes(:delegate_report, :championships)
.where(id: competition_ids.uniq).where("cancelled_at is null or end_date >= curdate()")
.sort_by { |comp| comp.start_date || (Date.today + 20.year) }.reverse
@past_competitions, @not_past_competitions = competitions.partition(&:is_probably_over?)
Expand Down
5 changes: 5 additions & 0 deletions app/models/competition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,10 @@ def main_event
Event.c_find(main_event_id)
end

def report_posted?
delegate_report.posted?
end

def events_held?(desired_event_ids)
# rubocop:disable Style/BitwisePredicate
# We have to shut up Rubocop here because otherwise it thinks that
Expand Down Expand Up @@ -713,6 +717,7 @@ def build_clone

validate :dates_must_be_valid

alias_attribute :visible, :showAtAll
alias_attribute :latitude_microdegrees, :latitude
alias_attribute :longitude_microdegrees, :longitude
before_validation :compute_coordinates
Expand Down
1 change: 0 additions & 1 deletion app/models/registration.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# frozen_string_literal: true

class Registration < ApplicationRecord
# TODO: Reg-V3 Cleanup: Remove all these and use the competing_status_{status} scopes
scope :pending, -> { where(competing_status: 'pending') }
FinnIckler marked this conversation as resolved.
Show resolved Hide resolved
scope :accepted, -> { where(competing_status: 'accepted') }
scope :cancelled, -> { where(competing_status: 'cancelled') }
Expand Down
82 changes: 16 additions & 66 deletions app/views/competitions/my_competitions.html.erb
Original file line number Diff line number Diff line change
@@ -1,70 +1,20 @@
<% provide(:title, t('.title')) %>

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

<%= t '.disclaimer' %>

<%= render "my_competitions_table", competitions: @not_past_competitions, registrations_by_competition_id: @registered_for_by_competition_id, past: false %>

<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
<a data-toggle="collapse" href="#past-competitions" class="collapsed">
<span><%= t '.past_competitions' %> (<%= @past_competitions.length %>)</span>
<span class="collapse-indicator"></span>
</a>
</h3>
</div>

<div id="past-competitions" class="panel-collapse collapse">
<div class="panel-body">
<%= render "my_competitions_table", competitions: @past_competitions, registrations_by_competition_id: @registered_for_by_competition_id, past: true %>
</div>
</div>
</div>
<script>
(function() {
// Reflow table when its made visible. See:
// https://github.com/thewca/worldcubeassociation.org/issues/343
// changed to .trigger, see https://github.com/thewca/worldcubeassociation.org/issues/343#issuecomment-182916594
$('#past-competitions').on('show.bs.collapse', function(e) {
$('#past-competitions table:not(.floatThead-table)').trigger('reflow');
});
})();
</script>

<% if current_user.wca_id %>
<p>
<%= link_to t('layouts.navigation.my_results'), person_path(current_user.wca_id) %>
</p>
<% if current_user.senior_delegate? %>
<p><%= link_to "Upcoming competitions for your subordinate Delegates", competitions_for_senior_path %></p>
<% end %>
<% end %>

<h3>
<%= ui_icon('bookmark') %>
<%= t '.bookmarked_title' %>
</h3>

<%= t '.bookmarked_explanation' %>
<%= form_tag(my_comps_path, method: :get, class: "competition-select form-inline #{@display}", id: "bookmarked-competitions-query-form") do %>
<div id="registration-status" class="form-group registration-status-selector">
<%= check_box_tag(:show_registration_status, params[:show_registration_status], params[:show_registration_status] == "on") %>
<%= label_tag(name = :show_registration_status, content_or_options = t('competitions.index.show_registration_status')) %>
</div>
<% end %>
<%= render "my_competitions_table", competitions: @bookmarked_competitions, registrations_by_competition_id: @registered_for_by_competition_id, past: false, bookmarked: true %>
<% options = {
only: %w[id name website start_date end_date, registration_open],
methods: %w[url city country_iso2 registration_not_yet_opened? registration_full? registration_past? results_posted? visible? confirmed? cancelled? report_posted? date_range short_display_name],
include: %w[championships],
} %>

<div class="container">
<%= react_component("MyCompetitions", {
userInfo: current_user.as_json,
FinnIckler marked this conversation as resolved.
Show resolved Hide resolved
permissions: current_user.permissions.as_json,
FinnIckler marked this conversation as resolved.
Show resolved Hide resolved
competitions: {
pastCompetitions: @past_competitions.as_json(options),
futureCompetitions: @not_past_competitions.as_json(options),
bookmarkedCompetitions: @bookmarked_competitions.as_json(options),
registrationStatuses: @registered_for_by_competition_id,
},
}) %>
</div>

<script>
$(document).ready(function() {
var $form = $('#bookmarked-competitions-query-form');
function submitForm() {
$form.trigger('submit.rails');
}
$('#show_registration_status').on('change', submitForm);
});
</script>
57 changes: 57 additions & 0 deletions app/webpacker/components/MyCompetitions/PastCompetitionTable.jsx
FinnIckler marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
Icon, Popup, Table, TableBody, TableHeader,
} from 'semantic-ui-react';
import React from 'react';
import I18n from '../../lib/i18n';
import {
DateTableCell, LocationTableCell, NameTableCell, ReportTableCell,
} from './TableCells';

export default function PastCompetitionsTable({ competitions, permissions }) {
return (
<Table striped>
<TableHeader>
<Table.Row>
<Table.HeaderCell>
{I18n.t('competitions.competition_info.name')}
</Table.HeaderCell>
<Table.HeaderCell>
{I18n.t('competitions.competition_info.location')}
</Table.HeaderCell>
<Table.HeaderCell>
{I18n.t('competitions.competition_info.date')}
</Table.HeaderCell>
<Table.HeaderCell />
<Table.HeaderCell />
<Table.HeaderCell />
</Table.Row>
</TableHeader>

<TableBody>
{competitions.map((competition) => (
<Table.Row key={competition.id}>
<NameTableCell competition={competition} />
<LocationTableCell competition={competition} />
<DateTableCell competition={competition} />
<Table.Cell>
{!competition['results_posted?'] && (
<Icon name="calendar check" />
)}
</Table.Cell>
<Table.Cell>
{competition['results_posted?'] && (
<Popup
content={I18n.t('competitions.my_competitions_table.results_up')}
trigger={(
<Icon name="check circle" />
)}
/>
)}
</Table.Cell>
<ReportTableCell competitionId={competition.id} permissions={permissions} isReportPosted={competition['report_posted?']} />
</Table.Row>
))}
</TableBody>
</Table>
);
}
78 changes: 78 additions & 0 deletions app/webpacker/components/MyCompetitions/TableCells.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Icon, Popup, Table } from 'semantic-ui-react';
import React from 'react';
import I18n from '../../lib/i18n';
import { competitionReportUrl, competitionReportEditUrl } from '../../lib/requests/routes.js.erb';
import { countries } from '../../lib/wca-data.js.erb';

export function NameTableCell({ competition }) {
return (
<Table.Cell>
<a href={competition.url}>
{competition.short_display_name}
{' '}
{ competition.championships?.length > 0 && <Icon name="trophy" /> }
</a>
</Table.Cell>
);
}

export function LocationTableCell({ competition }) {
return (
<Table.Cell>
{competition.city}
{`, ${countries.byIso2[competition.country_iso2].name}`}
</Table.Cell>
);
}

export function DateTableCell({ competition }) {
return (
<Table.Cell>
{competition.date_range}
</Table.Cell>
);
}

export function ReportTableCell({
permissions, competitionId, isReportPosted, canAdminCompetitions,
}) {
if (permissions.can_administer_competitions.scope === '*' || permissions.can_administer_competitions.scope.includes(competitionId)) {
return (
<Table.Cell>
<>
<Popup
content={I18n.t('competitions.my_competitions_table.report')}
trigger={(
<a href={competitionReportUrl(competitionId)}>
<Icon name="file alternate" />
</a>
)}
/>
<Popup
content={I18n.t('competitions.my_competitions_table.edit_report')}
trigger={(
<a href={competitionReportEditUrl(competitionId)}>
<Icon name="edit" />
</a>
)}
/>
{ !isReportPosted
&& permissions.can_administer_competitions.scope.includes(competitionId) && (
<Popup
content={I18n.t('competitions.my_competitions_table.missing_report')}
trigger={(
<Icon name="warning" />
)}
/>
)}
</>
</Table.Cell>
);
}

// A user might be able to see only certain reports in the list, so we return an empty cell

if (canAdminCompetitions) {
return <Table.Cell />;
}
}
130 changes: 130 additions & 0 deletions app/webpacker/components/MyCompetitions/UpcomingCompetitionTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import {
Icon,
Popup, Table, TableBody, TableHeader,
} from 'semantic-ui-react';
import React from 'react';
import { DateTime } from 'luxon';
import I18n from '../../lib/i18n';
import { competitionStatusText } from '../../lib/utils/competition-table';
import { competitionRegistrationsUrl, editCompetitionsUrl } from '../../lib/requests/routes.js.erb';
import {
DateTableCell, LocationTableCell, NameTableCell, ReportTableCell,
} from './TableCells';

const registrationStatusIcon = (registrationStatus) => {
switch (registrationStatus?.competing_status) {
case 'pending': return <Icon name="hourglass" />;
case 'waiting_list': return <Icon name="hourglass" />;
case 'accepted': return <Icon name="check circle" />;
case 'cancelled': return <Icon name="trash" />;
case 'rejected': return <Icon name="trash" />;
default: return <Icon />;
FinnIckler marked this conversation as resolved.
Show resolved Hide resolved
}
};

const competitionStatusIconText = (competition) => {
if (competition['registration_not_yet_opened?']) {
return I18n.t('competitions.index.tooltips.registration.opens_in', { duration: DateTime.fromISO(competition.registration_open).toRelative() });
}
if (competition['registration_past?']) {
return I18n.t('competitions.index.tooltips.registration.closed', { days: DateTime.fromISO(competition.start_date).toRelative() });
}
if (competition['registration_full?']) {
return I18n.t('competitions.index.tooltips.registration.full');
}
return I18n.t('competitions.index.tooltips.registration.open');
};

const competitionStatusIcon = (competition) => {
if (competition['registration_not_yet_opened?']) {
return <Icon name="clock" color="blue" />;
}
if (competition['registration_past?']) {
return <Icon name="user times" color="red" />;
}
if (competition['registration_full?']) {
return <Icon name="user clock" color="orange" />;
}
return <Icon name="user plus" color="green" />;
};

export default function UpcomingCompetitionTable({
competitions, permissions, registrationStatuses, shouldShowRegistrationStatus = false,
}) {
const canAdminCompetitions = permissions.can_administer_competitions.scope === '*' || competitions.some((c) => permissions.can_administer_competitions.scope.includes(c.id));

return (
<Table>
<TableHeader>
<Table.Row>
{ shouldShowRegistrationStatus && <Table.HeaderCell /> }
<Table.HeaderCell>
{I18n.t('competitions.competition_info.name')}
</Table.HeaderCell>
<Table.HeaderCell>
{I18n.t('competitions.competition_info.location')}
</Table.HeaderCell>
<Table.HeaderCell>
{I18n.t('competitions.competition_info.date')}
</Table.HeaderCell>
<Table.HeaderCell />
{canAdminCompetitions
&& (
<>
<Table.HeaderCell />
<Table.HeaderCell />
<Table.HeaderCell />
</>
)}

</Table.Row>
</TableHeader>

<TableBody>
{competitions.map((competition) => (
<Popup
key={competition.id}
position="top center"
content={competitionStatusText(competition, registrationStatuses[competition.id])}
trigger={(
<Table.Row positive={competition['confirmed?'] && !competition['cancelled?']} negative={!competition['visible?']}>
{ shouldShowRegistrationStatus && (
<Popup
position="top left"
content={competitionStatusIconText(competition)}
trigger={(
<Table.Cell>
{competitionStatusIcon(competition)}
</Table.Cell>
)}
/>
)}
<NameTableCell competition={competition} />
<LocationTableCell competition={competition} />
<DateTableCell competition={competition} />
<Table.Cell>
{registrationStatusIcon(registrationStatuses[competition.id])}
</Table.Cell>
{ (permissions.can_organize_competitions.scope === '*' || permissions.can_organize_competitions.scope.includes(competition.id)) && (
<Table.Cell>
<a href={editCompetitionsUrl(competition.id)}>
{ I18n.t('competitions.my_competitions_table.edit') }
</a>
</Table.Cell>
)}
{ (permissions.can_organize_competitions.scope === '*' || permissions.can_organize_competitions.scope.includes(competition.id)) && (
<Table.Cell>
<a href={competitionRegistrationsUrl(competition.id)}>
{ I18n.t('competitions.my_competitions_table.registrations') }
</a>
</Table.Cell>
)}
<ReportTableCell competitionId={competition.id} permissions={permissions} canAdminCompetitions={canAdminCompetitions} />
</Table.Row>
)}
/>
))}
</TableBody>
</Table>
);
}
Loading
Loading