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

Add a page for editing scrambles #10129

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
95 changes: 95 additions & 0 deletions app/controllers/admin/scrambles_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# frozen_string_literal: true

module Admin
class ScramblesController < AdminController
# NOTE: authentication is performed by admin controller

def new
competition = Competition.find(params[:competition_id])
round = Round.find(params[:round_id])
# Create some basic attributes for that empty scramble.
# Using Scramble.new wouldn't work here: we have no idea what the country
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by "country" here?

# could be and so on, so serialization would fail.
@scramble = {
competitionId: competition.id,
roundTypeId: round.round_type_id,
eventId: round.event.id,
}
end

def show
respond_to do |format|
format.json { render json: Scramble.find(params.require(:id)) }
end
end

def edit
@scramble = Scramble.includes(:competition).find(params[:id])
end

def create
json = {}
# Build a brand new scramble, validations will make sure the specified round
# data are valid.
scramble = Scramble.new(scramble_params)
if scramble.save
# We just inserted a new scramble, make sure we at least give it correct information.
validator = ResultsValidators::ScramblesValidator.new(apply_fixes: true)
validator.validate(competition_ids: [scramble.competitionId])
json[:messages] = ["Scramble inserted!"].concat(validator.infos.map(&:to_s))
else
json[:errors] = scramble.errors.map(&:full_message)
end
render json: json
end

def update
scramble = Scramble.find(params.require(:id))
# Since we may move the scramble to another competition, we want to validate
# both competitions if needed.
competitions_to_validate = [scramble.competitionId]
if scramble.update(scramble_params)
competitions_to_validate << scramble.competitionId
competitions_to_validate.uniq!
validator = ResultsValidators::ScramblesValidator.new(apply_fixes: true)
validator.validate(competition_ids: competitions_to_validate)
info = if scramble.saved_changes.empty?
["It looks like you submitted the exact same scramble, so no changes were made."]
else
["The scramble was saved."]
end
if competitions_to_validate.size > 1
info << "The scrambles was moved to another competition, make sure to check the competition validators for both of them."
end
render json: {
# Make sure we emit the competition's id next to the info, because we
# may validate multiple competitions at the same time.
messages: info.concat(validator.infos.map { |i| "[#{i.competition_id}]#{i}" }),
}
else
render json: {
errors: scramble.errors.map(&:full_message),
}
end
end

def destroy
scramble = Scramble.find(params.require(:id))
competition_id = scramble.competitionId
scramble.destroy!

# Create a results validator to fix information if needed
validator = ResultsValidators::ScramblesValidator.new(apply_fixes: true)
validator.validate(competition_ids: [competition_id])

render json: {
messages: ["Scramble deleted!"].concat(validator.infos.map(&:to_s)),
}
end

private def scramble_params
params.require(:scramble).permit(:competitionId, :roundTypeId, :eventId, :groupId,
:isExtra, :scrambleNum, :scramble)
end
end
end
7 changes: 7 additions & 0 deletions app/views/admin/scrambles/edit.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<% provide(:title, "Edit a scramble") %>

<div class="container">
<%= react_component("EditScramble", {
id: @scramble.id,
}) %>
</div>
7 changes: 7 additions & 0 deletions app/views/admin/scrambles/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<% provide(:title, "Add a scramble") %>

<div class="container">
<%= react_component("EditScramble/Create", {
scramble: @scramble,
}) %>
</div>
1 change: 1 addition & 0 deletions app/webpacker/components/EditResult/Create.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ function NewResult({
return (
<CreateEntry
initDataItem={result}
dataType="result"
EditForm={InlineEditForm}
/>
);
Expand Down
1 change: 1 addition & 0 deletions app/webpacker/components/EditResult/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ function EditResult({
<EditEntry
id={id}
dataUrlFn={resultUrl}
dataType="result"
DisplayTable={ShowSingleResult}
EditForm={InlineEditForm}
/>
Expand Down
17 changes: 17 additions & 0 deletions app/webpacker/components/EditScramble/Create.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import CreateEntry from '../ResultsData/Panel/CreateEntry';
import { InlineEditForm } from './index';

function NewScramble({
scramble,
}) {
return (
<CreateEntry
initDataItem={scramble}
dataType="scramble"
EditForm={InlineEditForm}
/>
);
}

export default NewScramble;
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';
import { Message, List } from 'semantic-ui-react';

import {
adminCheckExistingResultsUrl,
competitionScramblesUrl,
competitionUrl,
} from '../../../lib/requests/routes.js.erb';

function AfterActionMessage({
eventId,
competitionId,
response,
}) {
return (
<>
<Message
positive
header={(
<>
Action performed for:
{' '}
<a href={competitionUrl(competitionId)} target="_blank" rel="noreferrer">{competitionId}</a>
</>
)}
list={response.messages}
/>
<Message positive>
<div>
Please make sure to:
<List ordered>
<List.Item>
<a
href={adminCheckExistingResultsUrl(competitionId)}
target="_blank"
rel="noreferrer"
>
Check Competition Validators
</a>
</List.Item>
</List>
You can also
{' '}
<a href={competitionScramblesUrl(competitionId, eventId)}>go back to the scrambles</a>
.
</div>
</Message>
</>
);
}

export default AfterActionMessage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { useState, useCallback } from 'react';

import { Button, Checkbox } from 'semantic-ui-react';

function DeleteScrambleButton({ deleteAction }) {
const [confirmed, setConfirmed] = useState(false);
const updater = useCallback(() => setConfirmed((prev) => !prev), [setConfirmed]);
return (
<div>
<Button
negative
className="delete-scramble-button"
disabled={!confirmed}
onClick={deleteAction}
>
Delete the scramble
</Button>
<Checkbox
label="Yes, I want to delete that scramble"
checked={confirmed}
onChange={updater}
/>
</div>
);
}

export default DeleteScrambleButton;
101 changes: 101 additions & 0 deletions app/webpacker/components/EditScramble/ScrambleForm/RoundForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React, { useState } from 'react';
import {
Form, Grid, Icon, Popup,
} from 'semantic-ui-react';

import _ from 'lodash';
import { events, roundTypes } from '../../../lib/wca-data.js.erb';
import useNestedInputUpdater from '../../../lib/hooks/useNestedInputUpdater';
import { competitionEventsDataUrl } from '../../../lib/requests/routes.js.erb';
import { fetchJsonOrError } from '../../../lib/requests/fetchWithAuthenticityToken';

const itemFromId = (id, items) => ({
key: id,
value: id,
text: items.byId[id].name,
});

const formatRoundData = ({ eventId, formatId, roundTypeId }) => ({
[eventId]: {
eventId,
rounds: [{ formatId, roundTypeId }],
},
});

const extractFromRoundData = (roundData, eventId, key, items) => {
const ids = _.uniq(roundData[eventId].rounds.map((r) => r[key]));
return ids.map((id) => itemFromId(id, items));
};

function RoundForm({ roundData, setRoundData }) {
const {
competitionId, roundTypeId, eventId,
} = roundData;

const setCompetition = useNestedInputUpdater(setRoundData, 'competitionId');
const setEvent = useNestedInputUpdater(setRoundData, 'eventId');
const setRoundType = useNestedInputUpdater(setRoundData, 'roundTypeId');

const [competitionIdError, setCompetitionIdError] = useState(null);

const [localRoundData, setLocalRoundData] = useState(formatRoundData(roundData));

const availableEvents = Object.keys(localRoundData).map((k) => itemFromId(k, events));
const availableRoundTypes = extractFromRoundData(localRoundData, eventId, 'roundTypeId', roundTypes);

const fetchDataForCompetition = (id) => {
setCompetitionIdError(null);
fetchJsonOrError(competitionEventsDataUrl(id)).then(({ data }) => {
setLocalRoundData(data);
}).catch((err) => setCompetitionIdError(err.message));
};

// FIXME: we use padded grid here because Bootstrap's row conflicts with
// FUI's row and messes up the negative margins... :(
return (
<Form>
<Grid stackable padded columns={3}>
<Grid.Column>
<Form.Input
label="Competition ID"
value={competitionId}
onChange={setCompetition}
error={competitionIdError}
icon={(
<Popup
trigger={(
<Icon
circular
link
onClick={() => fetchDataForCompetition(competitionId)}
name="sync"
/>
)}
content="Get the events and round data for that competition"
position="top right"
/>
)}
/>
</Grid.Column>
<Grid.Column>
<Form.Select
label="Event"
value={eventId}
onChange={setEvent}
options={availableEvents}
/>
</Grid.Column>
<Grid.Column>
<Form.Select
label="Round type"
value={roundTypeId}
onChange={setRoundType}
options={availableRoundTypes}
/>
</Grid.Column>
</Grid>
</Form>
);
}

export default RoundForm;
26 changes: 26 additions & 0 deletions app/webpacker/components/EditScramble/ScrambleForm/SaveMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';

import { Message } from 'semantic-ui-react';

function SaveMessage({ response }) {
return (
<>
{response.messages && (
<Message
positive
header="Save was successful!"
list={response.messages}
/>
)}
{response.errors && (
<Message
error
list={response.errors}
header="Something went wrong when saving the scramble."
/>
)}
</>
);
}

export default SaveMessage;
Loading