return $round->toJson();
}
+ public function update(Request $request, Round $round) {
+ $this->authorize('update', $round);
+
+ $validatedData = $request->validate([
+ 'seed' => 'url',
+ 'title' => 'string',
+ ]);
+
+ $round->seed = $validatedData['seed'];
+ $round->title = $validatedData['title'];
+ $round->update();
+
+ Protocol::roundEdited(
+ $round->tournament,
+ $round,
+ $request->user(),
+ );
+
+ RoundChanged::dispatch($round);
+
+ $round->load(['results', 'results.user']);
+
+ return $round->toJson();
+ }
+
public function setSeed(Request $request, Round $round) {
$this->authorize('setSeed', $round);
ProtocolAdded::dispatch($protocol);
}
+ public static function roundEdited(Tournament $tournament, Round $round, User $user) {
+ $protocol = static::create([
+ 'tournament_id' => $tournament->id,
+ 'user_id' => $user->id,
+ 'type' => 'round.edit',
+ 'details' => [
+ 'tournament' => static::tournamentMemo($tournament),
+ 'round' => static::roundMemo($round),
+ ],
+ ]);
+ ProtocolAdded::dispatch($protocol);
+ }
+
public static function roundLocked(Tournament $tournament, Round $round, User $user = null) {
$protocol = static::create([
'tournament_id' => $tournament->id,
*/
public function update(User $user, Round $round)
{
- return false;
+ return !$round->tournament->locked && $user->isTournamentAdmin($round->tournament);
}
/**
--- /dev/null
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::table('rounds', function(Blueprint $table) {
+ $table->string('title')->default('');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table('rounds', function(Blueprint $table) {
+ $table->dropColumn('title');
+ });
+ }
+};
</Trans>;
}
case 'round.create':
+ case 'round.edit':
case 'round.lock':
case 'round.seed':
case 'round.unlock':
--- /dev/null
+import PropTypes from 'prop-types';
+import React, { useState } from 'react';
+import { Button } from 'react-bootstrap';
+import { withTranslation } from 'react-i18next';
+
+import EditDialog from './EditDialog';
+import Icon from '../common/Icon';
+import i18n from '../../i18n';
+
+const EditButton = ({
+ round,
+ tournament,
+}) => {
+ const [showDialog, setShowDialog] = useState(false);
+
+ return <>
+ <EditDialog
+ onHide={() => setShowDialog(false)}
+ round={round}
+ show={showDialog}
+ tournament={tournament}
+ />
+ <Button
+ onClick={() => setShowDialog(true)}
+ size="sm"
+ title={i18n.t('rounds.edit')}
+ variant="outline-secondary"
+ >
+ <Icon.EDIT title="" />
+ </Button>
+ </>;
+};
+
+EditButton.propTypes = {
+ round: PropTypes.shape({
+ locked: PropTypes.bool,
+ }),
+ tournament: PropTypes.shape({
+ }),
+};
+
+export default withTranslation()(EditButton);
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Modal } from 'react-bootstrap';
+import { withTranslation } from 'react-i18next';
+
+import EditForm from './EditForm';
+import i18n from '../../i18n';
+
+const EditDialog = ({
+ onHide,
+ round,
+ show,
+}) =>
+<Modal className="edit-dialog" onHide={onHide} show={show}>
+ <Modal.Header closeButton>
+ <Modal.Title>
+ {i18n.t('rounds.edit')}
+ </Modal.Title>
+ </Modal.Header>
+ <EditForm
+ onCancel={onHide}
+ round={round}
+ />
+</Modal>;
+
+EditDialog.propTypes = {
+ onHide: PropTypes.func,
+ round: PropTypes.shape({
+ }),
+ show: PropTypes.bool,
+ tournament: PropTypes.shape({
+ }),
+};
+
+export default withTranslation()(EditDialog);
--- /dev/null
+import axios from 'axios';
+import { withFormik } from 'formik';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button, Col, Form, Modal, Row } from 'react-bootstrap';
+import { withTranslation } from 'react-i18next';
+import toastr from 'toastr';
+
+import laravelErrorsToFormik from '../../helpers/laravelErrorsToFormik';
+import i18n from '../../i18n';
+import yup from '../../schema/yup';
+
+const EditForm = ({
+ errors,
+ handleBlur,
+ handleChange,
+ handleSubmit,
+ onCancel,
+ touched,
+ values,
+}) =>
+<Form noValidate onSubmit={handleSubmit}>
+ <Modal.Body>
+ <Row>
+ <Form.Group as={Col} controlId="round.title">
+ <Form.Label>{i18n.t('rounds.title')}</Form.Label>
+ <Form.Control
+ isInvalid={!!(touched.title && errors.title)}
+ name="title"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ type="text"
+ value={values.title || ''}
+ />
+ {touched.title && errors.title ?
+ <Form.Control.Feedback type="invalid">
+ {i18n.t(errors.title)}
+ </Form.Control.Feedback>
+ : null}
+ </Form.Group>
+ </Row>
+ <Row>
+ <Form.Group as={Col} controlId="round.seed">
+ <Form.Label>{i18n.t('rounds.seed')}</Form.Label>
+ <Form.Control
+ isInvalid={!!(touched.seed && errors.seed)}
+ name="seed"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ type="text"
+ value={values.seed || ''}
+ />
+ {touched.seed && errors.seed ?
+ <Form.Control.Feedback type="invalid">
+ {i18n.t(errors.seed)}
+ </Form.Control.Feedback>
+ : null}
+ </Form.Group>
+ </Row>
+ </Modal.Body>
+ <Modal.Footer>
+ {onCancel ?
+ <Button onClick={onCancel} variant="secondary">
+ {i18n.t('button.cancel')}
+ </Button>
+ : null}
+ <Button type="submit" variant="primary">
+ {i18n.t('button.save')}
+ </Button>
+ </Modal.Footer>
+</Form>;
+
+EditForm.propTypes = {
+ errors: PropTypes.shape({
+ seed: PropTypes.string,
+ title: PropTypes.string,
+ }),
+ handleBlur: PropTypes.func,
+ handleChange: PropTypes.func,
+ handleSubmit: PropTypes.func,
+ onCancel: PropTypes.func,
+ touched: PropTypes.shape({
+ seed: PropTypes.bool,
+ title: PropTypes.bool,
+ }),
+ values: PropTypes.shape({
+ seed: PropTypes.string,
+ title: PropTypes.string,
+ }),
+};
+
+export default withFormik({
+ displayName: 'EditForm',
+ enableReinitialize: true,
+ handleSubmit: async (values, actions) => {
+ const { round_id } = values;
+ const { setErrors } = actions;
+ const { onCancel } = actions.props;
+ try {
+ await axios.put(`/api/rounds/${round_id}`, values);
+ toastr.success(i18n.t('rounds.editSuccess'));
+ if (onCancel) {
+ onCancel();
+ }
+ } catch (e) {
+ toastr.error(i18n.t('rounds.editError'));
+ if (e.response && e.response.data && e.response.data.errors) {
+ setErrors(laravelErrorsToFormik(e.response.data.errors));
+ }
+ }
+ },
+ mapPropsToValues: ({ round }) => ({
+ round_id: round.id,
+ seed: round.seed || '',
+ title: round.title || '',
+ }),
+ validationSchema: yup.object().shape({
+ seed: yup.string().url(),
+ title: yup.string(),
+ }),
+})(withTranslation()(EditForm));
import React from 'react';
import { withTranslation } from 'react-i18next';
+import EditButton from './EditButton';
import LockButton from './LockButton';
import SeedButton from './SeedButton';
import SeedCode from './SeedCode';
import SeedRolledBy from './SeedRolledBy';
import List from '../results/List';
import ReportButton from '../results/ReportButton';
-import { mayReportResult, isRunner } from '../../helpers/permissions';
+import { mayEditRound, mayReportResult, isRunner } from '../../helpers/permissions';
import { isComplete } from '../../helpers/Round';
import { hasFinishedRound } from '../../helpers/User';
import { withUser } from '../../helpers/UserContext';
import i18n from '../../i18n';
const getClassName = (round, tournament, user) => {
- const classNames = ['round', 'd-flex'];
+ const classNames = ['round'];
if (round.locked) {
classNames.push('is-locked');
} else {
user,
}) =>
<li className={getClassName(round, tournament, user)}>
- <div className="info">
- <p className="date">
- {round.number ? `#${round.number} ` : '#?'}
- {i18n.t('rounds.date', { date: new Date(round.created_at) })}
- </p>
- <p className="seed">
- {round.code ?
- <>
- <SeedCode code={round.code} game={round.game || 'alttpr'} />
- <br />
- </>
- : null}
- <SeedButton
- round={round}
- tournament={tournament}
- />
- {' '}
- <SeedRolledBy round={round} />
- </p>
- {mayReportResult(user, tournament) ?
- <p className="report">
- <ReportButton
+ {round.title ?
+ <h3>{round.title}</h3>
+ : null}
+ <div className="d-flex">
+ <div className="info">
+ <p className="date">
+ {round.number ? `#${round.number} ` : '#?'}
+ {i18n.t('rounds.date', { date: new Date(round.created_at) })}
+ </p>
+ <p className="seed">
+ {round.code ?
+ <>
+ <SeedCode code={round.code} game={round.game || 'alttpr'} />
+ <br />
+ </>
+ : null}
+ <SeedButton
round={round}
tournament={tournament}
- user={user}
/>
+ {' '}
+ <SeedRolledBy round={round} />
</p>
- : null}
- <LockButton round={round} tournament={tournament} />
+ {mayReportResult(user, tournament) ?
+ <p className="report">
+ <ReportButton
+ round={round}
+ tournament={tournament}
+ user={user}
+ />
+ </p>
+ : null}
+ <div className="button-bar">
+ <LockButton round={round} tournament={tournament} />
+ {mayEditRound(user, tournament, round) ?
+ <EditButton round={round} tournament={tournament} />
+ : null}
+ </div>
+ </div>
+ <List round={round} tournament={tournament} />
</div>
- <List round={round} tournament={tournament} />
</li>;
Item.propTypes = {
locked: PropTypes.bool,
number: PropTypes.number,
seed: PropTypes.string,
+ title: PropTypes.string,
}),
tournament: PropTypes.shape({
participants: PropTypes.arrayOf(PropTypes.shape({
const SeedDialog = ({
onHide,
- participant,
round,
show,
}) =>
</Modal.Header>
<SeedForm
onCancel={onHide}
- participant={participant}
round={round}
/>
</Modal>;
SeedDialog.propTypes = {
onHide: PropTypes.func,
- participant: PropTypes.shape({
- }),
round: PropTypes.shape({
}),
show: PropTypes.bool,
return isRunner(user, tournament);
};
+export const mayEditRound = (user, tournament) =>
+ !tournament.locked && isTournamentAdmin(user, tournament);
+
export const mayLockRound = (user, tournament) =>
!tournament.locked && isTournamentAdmin(user, tournament);
},
round: {
create: 'Runde #{{number}} hinzugefügt',
+ edit: 'Runde #{{number}} bearbeitet',
lock: 'Runde #{{number}} gesperrt',
seed: 'Seed für Runde #{{number}} eingetragen',
unlock: 'Runde #{{number}} entsperrt',
},
rounds: {
date: '{{ date, L }}',
+ edit: 'Runde bearbeiten',
+ editError: 'Fehler beim Speichern',
+ editSuccess: 'Gespeichert',
empty: 'Noch keine Runde gestartet',
heading: 'Runden',
new: 'Neue Runde',
setSeed: 'Seed eintragen',
setSeedError: 'Seed konnte nicht eintragen werden',
setSeedSuccess: 'Seed eingetragen',
+ title: 'Titel',
unlock: 'Runde entsperren',
unlockDescription: 'Die Runde wird wieder freigegeben und Runner können wieder Änderungen an ihrem Ergebnis vornehmen.',
unlocked: 'Die Runde ist offen für Änderungen am Ergebnis.',
report: 'Result of <1>{{time}}</1> reported for round {{number}}',
},
round: {
- create: 'Round #{{number}} added',
+ create: 'Added round #{{number}}',
+ edit: 'Edited round #{{number}}',
lock: 'Round #{{number}} locked',
seed: 'Set seed for round #{{number}}',
unlock: 'Round #{{number}} unlocked',
},
rounds: {
date: '{{ date, L }}',
+ edit: 'Edit round',
+ editError: 'Error saving round',
+ editSuccess: 'Saved successfully',
empty: 'No rounds yet',
heading: 'Rounds',
new: 'New round',
setSeed: 'Set seed',
setSeedError: 'Seed could not be set',
setSeedSuccess: 'Seed set',
+ title: 'Title',
unlock: 'Unock round',
unlockDescription: 'The round is unlocked and runers are free to submit or change their results again.',
unlocked: 'Results for this round are subject to change.',
border-radius: $border-radius;
background: $gray-700;
padding: 1ex;
+ list-style: none;
&.has-not-finished {
border-color: $light;
Route::post('results', 'App\Http\Controllers\ResultController@create');
Route::post('rounds', 'App\Http\Controllers\RoundController@create');
+Route::put('rounds/{round}', 'App\Http\Controllers\RoundController@update');
Route::post('rounds/{round}/lock', 'App\Http\Controllers\RoundController@lock');
Route::post('rounds/{round}/setSeed', 'App\Http\Controllers\RoundController@setSeed');
Route::post('rounds/{round}/unlock', 'App\Http\Controllers\RoundController@unlock');