]> git.localhorst.tv Git - alttp.git/commitdiff
round titles
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Fri, 21 Oct 2022 17:49:01 +0000 (19:49 +0200)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Fri, 21 Oct 2022 17:49:01 +0000 (19:49 +0200)
15 files changed:
app/Http/Controllers/RoundController.php
app/Models/Protocol.php
app/Policies/RoundPolicy.php
database/migrations/2022_10_21_151122_round_title.php [new file with mode: 0644]
resources/js/components/protocol/Item.js
resources/js/components/rounds/EditButton.js [new file with mode: 0644]
resources/js/components/rounds/EditDialog.js [new file with mode: 0644]
resources/js/components/rounds/EditForm.js [new file with mode: 0644]
resources/js/components/rounds/Item.js
resources/js/components/rounds/SeedDialog.js
resources/js/helpers/permissions.js
resources/js/i18n/de.js
resources/js/i18n/en.js
resources/sass/rounds.scss
routes/api.php

index 04e1b865ca0fb3560be7ac9231e5f998b3974897..2b6c617acfbaa03d93df72544f4881d2036eb812 100644 (file)
@@ -39,6 +39,31 @@ class RoundController extends Controller
                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);
 
index 9aaa5b08d1a8f5b519350ffa0ee2f85fd24543c1..ce081e77dded2c6bf7f92af76381d1a0364e54e6 100644 (file)
@@ -93,6 +93,19 @@ class Protocol extends Model
                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,
index 083454a7fceb80c4a228410e9d1a1f781d4e128c..1f872c57bf054edaaf7de6fbba5ea60857c1e40b 100644 (file)
@@ -53,7 +53,7 @@ class RoundPolicy
         */
        public function update(User $user, Round $round)
        {
-               return false;
+               return !$round->tournament->locked && $user->isTournamentAdmin($round->tournament);
        }
 
        /**
diff --git a/database/migrations/2022_10_21_151122_round_title.php b/database/migrations/2022_10_21_151122_round_title.php
new file mode 100644 (file)
index 0000000..40a0856
--- /dev/null
@@ -0,0 +1,32 @@
+<?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');
+               });
+       }
+};
index dba32f52a67533df289eb8ff9dd057d0b81c7976..00da2ef472c27afb8b8be1c55de2c9cd9e361e0a 100644 (file)
@@ -68,6 +68,7 @@ const getEntryDescription = entry => {
                        </Trans>;
                }
                case 'round.create':
+               case 'round.edit':
                case 'round.lock':
                case 'round.seed':
                case 'round.unlock':
diff --git a/resources/js/components/rounds/EditButton.js b/resources/js/components/rounds/EditButton.js
new file mode 100644 (file)
index 0000000..edc6fbf
--- /dev/null
@@ -0,0 +1,42 @@
+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);
diff --git a/resources/js/components/rounds/EditDialog.js b/resources/js/components/rounds/EditDialog.js
new file mode 100644 (file)
index 0000000..912a420
--- /dev/null
@@ -0,0 +1,35 @@
+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);
diff --git a/resources/js/components/rounds/EditForm.js b/resources/js/components/rounds/EditForm.js
new file mode 100644 (file)
index 0000000..57ee95a
--- /dev/null
@@ -0,0 +1,121 @@
+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));
index 1b1edb22111233a63d13023a26d94523850cafff..92f55a06791098adda7b299ebf241665874ccdff 100644 (file)
@@ -2,20 +2,21 @@ import PropTypes from 'prop-types';
 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 {
@@ -40,37 +41,47 @@ const Item = ({
        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 = {
@@ -81,6 +92,7 @@ Item.propTypes = {
                locked: PropTypes.bool,
                number: PropTypes.number,
                seed: PropTypes.string,
+               title: PropTypes.string,
        }),
        tournament: PropTypes.shape({
                participants: PropTypes.arrayOf(PropTypes.shape({
index e181b64b132157f58f5d2460f21dc4abd1e99ac2..2ee36588bb2a21fe617bc796a08f0320ac686dec 100644 (file)
@@ -8,7 +8,6 @@ import i18n from '../../i18n';
 
 const SeedDialog = ({
        onHide,
-       participant,
        round,
        show,
 }) =>
@@ -20,15 +19,12 @@ const SeedDialog = ({
        </Modal.Header>
        <SeedForm
                onCancel={onHide}
-               participant={participant}
                round={round}
        />
 </Modal>;
 
 SeedDialog.propTypes = {
        onHide: PropTypes.func,
-       participant: PropTypes.shape({
-       }),
        round: PropTypes.shape({
        }),
        show: PropTypes.bool,
index d7613256d8c2f8e2a569bd2f75514799a818cfb8..b1de05180d2b1b9ea04f83a4921a9cbd56190162 100644 (file)
@@ -67,6 +67,9 @@ export const mayReportResult = (user, tournament) => {
        return isRunner(user, tournament);
 };
 
+export const mayEditRound = (user, tournament) =>
+       !tournament.locked && isTournamentAdmin(user, tournament);
+
 export const mayLockRound = (user, tournament) =>
        !tournament.locked && isTournamentAdmin(user, tournament);
 
index 56b2d0699ca5dc9cec70a0ddade23aa2c67ed2bf..97958d6fceb480040012af05551a9c6dee3e06b4 100644 (file)
@@ -386,6 +386,7 @@ export default {
                                },
                                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',
@@ -425,6 +426,9 @@ export default {
                },
                rounds: {
                        date: '{{ date, L }}',
+                       edit: 'Runde bearbeiten',
+                       editError: 'Fehler beim Speichern',
+                       editSuccess: 'Gespeichert',
                        empty: 'Noch keine Runde gestartet',
                        heading: 'Runden',
                        new: 'Neue Runde',
@@ -440,6 +444,7 @@ export default {
                        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.',
index f91e63bdaa1fc3ce83146cf06ede3d9afd930496..e7e0c59d58c06cbc198051abf1f616a8552ea8fe 100644 (file)
@@ -385,7 +385,8 @@ export default {
                                        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',
@@ -425,6 +426,9 @@ export default {
                },
                rounds: {
                        date: '{{ date, L }}',
+                       edit: 'Edit round',
+                       editError: 'Error saving round',
+                       editSuccess: 'Saved successfully',
                        empty: 'No rounds yet',
                        heading: 'Rounds',
                        new: 'New round',
@@ -440,6 +444,7 @@ export default {
                        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.',
index 4ed13381d977a522b2c74a68776328c97dcbebdc..2d19cc438c34cfd81cb5cf54ace48b31506578e4 100644 (file)
@@ -8,6 +8,7 @@
                border-radius: $border-radius;
                background: $gray-700;
                padding: 1ex;
+               list-style: none;
 
                &.has-not-finished {
                        border-color: $light;
index 7ae308e223cdc6618f6f7cbd2fc6d6706386f1c7..d9f44ad4c1db850919998d7ae83f5f7dd2a9cddf 100644 (file)
@@ -38,6 +38,7 @@ Route::get('protocol/{tournament}', 'App\Http\Controllers\ProtocolController@for
 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');