diff --git a/tnoodle-ui/src/main/api/wca.api.js b/tnoodle-ui/src/main/api/wca.api.js
index 1003d208d..0815bf839 100644
--- a/tnoodle-ui/src/main/api/wca.api.js
+++ b/tnoodle-ui/src/main/api/wca.api.js
@@ -151,7 +151,7 @@ async function wcaApiFetch(path, fetchOptions) {
const response = await fetch(`${baseApiUrl}${path}`, fetchOptions);
if (!response.ok) {
- throw new Error(`${response.status}: ${response.statusText}`);
+ return Promise.reject();
}
return response;
}
diff --git a/tnoodle-ui/src/main/components/EntryInterface.jsx b/tnoodle-ui/src/main/components/EntryInterface.jsx
index 451a71d69..101b19c8f 100644
--- a/tnoodle-ui/src/main/components/EntryInterface.jsx
+++ b/tnoodle-ui/src/main/components/EntryInterface.jsx
@@ -1,123 +1,81 @@
-import React, { Component } from "react";
+import React, { useState } from "react";
import {
updatePassword,
updateCompetitionName,
updateFileZipBlob,
} from "../redux/ActionCreators";
-import { connect } from "react-redux";
-import { getDefaultCompetitionName } from "../util/competition.name.util";
import { FaEye, FaEyeSlash } from "react-icons/fa";
+import { useSelector, useDispatch } from "react-redux";
-const mapStateToProps = (store) => ({
- editingDisabled: store.editingDisabled,
- competitionName: store.wcif.name,
- generatingScrambles: store.generatingScrambles,
-});
+const EntryInterface = () => {
+ const [showPassword, setShowPassword] = useState(false);
-const mapDispatchToProps = {
- updatePassword,
- updateCompetitionName,
- updateFileZipBlob,
-};
-
-const EntryInterface = connect(
- mapStateToProps,
- mapDispatchToProps
-)(
- class EntryInterface extends Component {
- constructor(props) {
- super(props);
+ const editingDisabled = useSelector((state) => state.editingDisabled);
+ const password = useSelector((state) => state.password);
+ const competitionName = useSelector((state) => state.wcif.name);
+ const generatingScrambles = useSelector(
+ (state) => state.generatingScrambles
+ );
- this.state = {
- editingDisabled: props.editingDisabled,
- password: "",
- showPassword: false,
- };
- }
+ const dispatch = useDispatch();
- componentDidMount() {
- this.props.updateCompetitionName(getDefaultCompetitionName());
- }
+ const handleCompetitionNameChange = (event) => {
+ dispatch(updateCompetitionName(event.target.value));
- handleCompetitionNameChange = (event) => {
- this.props.updateCompetitionName(event.target.value);
+ // Require another zip with the new name.
+ dispatch(updateFileZipBlob(null));
+ };
- // Require another zip with the new name.
- this.props.updateFileZipBlob(null);
- };
+ const handlePasswordChange = (evt) => {
+ dispatch(updatePassword(evt.target.value));
- handlePasswordChange = (event) => {
- let state = this.state;
- state.password = event.target.value;
- this.setState(state);
+ // Require another zip with the new password, in case there was a zip generated.
+ dispatch(updateFileZipBlob(null));
+ };
- this.props.updatePassword(this.state.password);
+ return (
+ <>
+
+
+
+
- // Require another zip with the new password, in case there was a zip generated.
- this.props.updateFileZipBlob(null);
- };
-
- toogleShowPassword = () => {
- let state = this.state;
- state.showPassword = !state.showPassword;
- this.setState(state);
- };
-
- render() {
- let competitionName = this.props.competitionName;
- let disabled = this.props.editingDisabled;
- return (
-
-
+ {`You selected ${mbld} cubes for Multi-Blind, but there's a competitor who already tried ${bestMbldAttempt} at a competition. Proceed if you are really certain of it.`}
+
- {`You selected ${this.props.mbld} cubes for Multi-Blind, but there's a competitor who already tried ${this.props.bestMbldAttempt} at a competition. Proceed if you are really certain of it.`}
-
+ );
+};
export default SideBar;
diff --git a/tnoodle-ui/src/main/components/VersionInfo.jsx b/tnoodle-ui/src/main/components/VersionInfo.jsx
index 104319be8..5600e6188 100644
--- a/tnoodle-ui/src/main/components/VersionInfo.jsx
+++ b/tnoodle-ui/src/main/components/VersionInfo.jsx
@@ -1,148 +1,112 @@
-import React, { Component } from "react";
-import { connect } from "react-redux";
+import React, { useEffect, useState } from "react";
+import { useDispatch } from "react-redux";
import { fetchRunningVersion } from "../api/tnoodle.api";
import { fetchVersionInfo } from "../api/wca.api";
import { updateOfficialZipStatus } from "../redux/ActionCreators";
-const mapDispatchToProps = {
- updateOfficialZipStatus,
-};
-
-const VersionInfo = connect(
- null,
- mapDispatchToProps
-)(
- class extends Component {
- constructor(props) {
- super(props);
- this.state = {
- currentTnoodle: null,
- allowedTnoodleVersions: null,
- runningVersion: null,
- signedBuild: null,
- signatureKeyBytes: null,
- wcaPublicKeyBytes: null,
- wcaResponse: false,
- tnoodleResponse: false,
- };
- }
+const VersionInfo = () => {
+ const [currentTnoodle, setCurrentTnoodle] = useState(null);
+ const [allowedTnoodleVersions, setAllowedTnoodleVersions] = useState(null);
+ const [runningVersion, setRunningVersion] = useState(null);
+ const [signedBuild, setSignedBuild] = useState(null);
+ const [signatureKeyBytes, setSignatureKeyBytes] = useState(null);
+ const [wcaPublicKeyBytes, setWcaPublicKeyBytes] = useState(null);
+ const [signatureValid, setSignatureValid] = useState(true);
- componentDidMount() {
- // Fetch from WCA API.
- fetchVersionInfo().then((response) => {
- if (!response) {
- return;
- }
- let { current, allowed, publicKeyBytes } = response;
- this.setState({
- ...this.state,
- currentTnoodle: current,
- allowedTnoodleVersions: allowed,
- wcaPublicKeyBytes: publicKeyBytes,
- wcaResponse: true,
- });
- this.analyzeVersion();
- });
+ useEffect(
+ () =>
+ setSignatureValid(
+ signedBuild && signatureKeyBytes === wcaPublicKeyBytes
+ ),
+ [signedBuild, signatureKeyBytes, wcaPublicKeyBytes]
+ );
- fetchRunningVersion().then((response) => {
- if (!response) {
- return;
- }
- let {
- projectName,
- projectVersion,
- signedBuild,
- signatureKeyBytes,
- } = response;
- this.setState({
- ...this.state,
- // Running version is based on projectName and projectVersion
- runningVersion:
- projectName != null && projectVersion != null
- ? `${projectName}-${projectVersion}`
- : "",
- signedBuild: signedBuild,
- signatureKeyBytes: signatureKeyBytes,
- tnoodleResponse: true,
- });
- this.analyzeVersion();
- });
- }
+ const dispatch = useDispatch();
- signatureValid() {
- return (
- this.state.signedBuild &&
- this.state.signatureKeyBytes === this.state.wcaPublicKeyBytes
- );
- }
+ useEffect(() => {
+ // Fetch from WCA API.
+ fetchVersionInfo().then((response) => {
+ if (!response) {
+ return;
+ }
+ setCurrentTnoodle(response.current);
+ setAllowedTnoodleVersions(response.allowed);
+ setWcaPublicKeyBytes(response.publicKeyBytes);
+ });
- // This method avoids global state update while rendering
- analyzeVersion() {
- // We wait until both wca and tnoodle answers
- if (!this.state.tnoodleResponse || !this.state.wcaResponse) {
+ fetchRunningVersion().then((response) => {
+ if (!response) {
return;
}
- let runningVersion = this.state.runningVersion;
- let allowedVersions = this.state.allowedTnoodleVersions;
- let signedBuild = this.signatureValid();
+ setRunningVersion(
+ !!response.projectName && !!response.projectVersion
+ ? `${response.projectName}-${response.projectVersion}`
+ : ""
+ );
+ setSignedBuild(response.signedBuild);
+ setSignatureKeyBytes(response.signatureKeyBytes);
+ });
+ }, []);
- if (!signedBuild || !allowedVersions.includes(runningVersion)) {
- this.props.updateOfficialZipStatus(false);
- }
+ // This avoids global state update while rendering
+ const analyzeVerion = () => {
+ // We wait until both wca and tnoodle answers
+ if (!allowedTnoodleVersions || !runningVersion) {
+ return;
}
- render() {
- let runningVersion = this.state.runningVersion;
- let allowedVersions = this.state.allowedTnoodleVersions;
- let currentTnoodle = this.state.currentTnoodle;
- let signedBuild = this.signatureValid();
+ dispatch(
+ updateOfficialZipStatus(
+ signatureValid &&
+ allowedTnoodleVersions.includes(runningVersion)
+ )
+ );
+ };
+ useEffect(analyzeVerion, [allowedTnoodleVersions, runningVersion]);
- // We cannot analyze TNoodle version here. We do not bother the user.
- if (!runningVersion || !allowedVersions) {
- return null;
- }
-
- // Running version is not the most recent release
- if (runningVersion !== currentTnoodle.name) {
- // Running version is allowed, but not the latest.
- if (allowedVersions.includes(runningVersion)) {
- return (
-
- You are running {runningVersion}, which is still
- allowed, but you should upgrade to{" "}
- {currentTnoodle.name} available{" "}
- here as soon
- as possible.
-
- );
- }
+ // We cannot analyze TNoodle version here. We do not bother the user.
+ if (!runningVersion || !allowedTnoodleVersions) {
+ return null;
+ }
- return (
-
- You are running {runningVersion}, which is not allowed.
- Do not use scrambles generated in any official
- competition and consider downloading the latest version{" "}
- here.
-
- );
- }
+ // Running version is not the most recent release
+ if (runningVersion !== currentTnoodle.name) {
+ // Running version is allowed, but not the latest.
+ if (allowedTnoodleVersions.includes(runningVersion)) {
+ return (
+
+ You are running {runningVersion}, which is still allowed,
+ but you should upgrade to {currentTnoodle.name} available{" "}
+ here as soon as
+ possible.
+
+ );
+ }
- // Generated version is not an officially signed jar
- if (!signedBuild) {
- return (
-
- You are running an unsigned TNoodle release. Do not use
- scrambles generated in any official competition and
- consider downloading the official program{" "}
- here
-
- );
- }
+ return (
+
+ You are running {runningVersion}, which is not allowed. Do not
+ use scrambles generated in any official competition and consider
+ downloading the latest version{" "}
+ here.
+
+ );
+ }
- return null;
- }
+ // Generated version is not an officially signed jar
+ if (!signatureValid) {
+ return (
+
+ You are running an unsigned TNoodle release. Do not use
+ scrambles generated in any official competition and consider
+ downloading the official program{" "}
+ here
+
+ );
}
-);
+
+ return null;
+};
export default VersionInfo;
diff --git a/tnoodle-ui/src/main/constants/default.wcif.js b/tnoodle-ui/src/main/constants/default.wcif.js
index 2a3ae9d5e..51b6020f8 100644
--- a/tnoodle-ui/src/main/constants/default.wcif.js
+++ b/tnoodle-ui/src/main/constants/default.wcif.js
@@ -1,4 +1,5 @@
import { getDefaultCopiesExtension } from "../helper/wcif.helper";
+import { getDefaultCompetitionName } from "../util/competition.name.util";
// Add 1 round of 3x3x3
let default333 = {
@@ -13,10 +14,11 @@ let default333 = {
],
};
+let name = getDefaultCompetitionName();
export const defaultWcif = {
formatVersion: "1.0",
- name: "",
- shortName: "",
+ name,
+ shortName: name,
id: "",
events: [default333],
persons: [],
diff --git a/tnoodle-ui/src/main/redux/Reducers.js b/tnoodle-ui/src/main/redux/Reducers.js
index b1bc683d2..f13c7e5f5 100644
--- a/tnoodle-ui/src/main/redux/Reducers.js
+++ b/tnoodle-ui/src/main/redux/Reducers.js
@@ -8,10 +8,10 @@ const defaultStore = {
wcif: defaultWcif,
mbld: MBLD_DEFAULT,
password: "",
- editingDisabled: false,
+ editingDisabled: false, // If we fetch competition info, some fields can't be changed
officialZip: true,
fileZipBlob: null,
- generatingScrambles: null,
+ generatingScrambles: false,
scramblingProgressTarget: {},
scramblingProgressCurrent: {},
cachedObjects: {},
diff --git a/tnoodle-ui/src/main/util/query.param.util.js b/tnoodle-ui/src/main/util/query.param.util.js
index 10bc05b64..901d6cddd 100644
--- a/tnoodle-ui/src/main/util/query.param.util.js
+++ b/tnoodle-ui/src/main/util/query.param.util.js
@@ -24,3 +24,24 @@ function parseQueryString(query) {
return params;
}, {});
}
+
+const setPageWithoutRedirect = (url) => window.history.pushState(null, "", url);
+
+const currentLocationWithoutQuery = () =>
+ window.location.origin + window.location.pathname;
+
+export const updateQueryParam = (name, value) => {
+ var searchParams = new URLSearchParams(window.location.search);
+ searchParams.set(name, value);
+ setPageWithoutRedirect(
+ currentLocationWithoutQuery() + "?" + searchParams.toString()
+ );
+};
+
+export const removeQueryParam = (name) => {
+ var searchParams = new URLSearchParams(window.location.search);
+ searchParams.delete(name);
+ setPageWithoutRedirect(
+ currentLocationWithoutQuery() + "?" + searchParams.toString()
+ );
+};
diff --git a/tnoodle-ui/src/test/EventPicker.test.js b/tnoodle-ui/src/test/EventPicker.test.js
index 793ad5542..f0190c870 100644
--- a/tnoodle-ui/src/test/EventPicker.test.js
+++ b/tnoodle-ui/src/test/EventPicker.test.js
@@ -62,14 +62,22 @@ it("Changing values from event", () => {
numberOfRounds
);
+ // There's a harmless change of type here. 1 -> "1"
+ // Initial value should be 1
+ expect(store.getState().wcif.events[0].rounds[0].scrambleSetCount).toBe(1);
+
+ // This should be numberOfRounds * 2, since each round has 2 inputs.
+ // It's not, probably because not updating DOM after dispatching
const inputs = Array.from(container.querySelectorAll("input"));
+ let roundToChange = 0;
+ let value = "10";
+
// Change last scramble sets to 10
- fireEvent.change(inputs[inputs.length - 2], { target: { value: 10 } });
+ fireEvent.change(inputs[roundToChange * 2], { target: { value } });
expect(
- store.getState().wcif.events[0].rounds[numberOfRounds - 1]
- .scrambleSetCount
- ).toEqual("10");
+ store.getState().wcif.events[0].rounds[roundToChange].scrambleSetCount
+ ).toEqual(value);
// Remove 1 round
numberOfRounds--;
@@ -77,10 +85,6 @@ it("Changing values from event", () => {
expect(store.getState().wcif.events[0].rounds.length).toEqual(
numberOfRounds
);
- expect(
- store.getState().wcif.events[0].rounds[numberOfRounds - 1]
- .scrambleSetCount
- ).not.toEqual("10");
const scrambleSets = inputs[0];
const copies = inputs[1];
@@ -89,11 +93,6 @@ it("Changing values from event", () => {
expect(scrambleSets.disabled).toBe(false);
expect(copies.disabled).toBe(false);
- // There's a harmless change of type here. 1 -> "1"
-
- // Initial value should be 1
- expect(store.getState().wcif.events[0].rounds[0].scrambleSetCount).toBe(1);
-
// Changes to scrambleSet should go to the store
const newScrambleSets = "3";
fireEvent.change(scrambleSets, { target: { value: newScrambleSets } });
diff --git a/tnoodle-ui/src/test/SideBar.test.js b/tnoodle-ui/src/test/SideBar.test.js
index 92b3433d6..611c9ce77 100644
--- a/tnoodle-ui/src/test/SideBar.test.js
+++ b/tnoodle-ui/src/test/SideBar.test.js
@@ -49,15 +49,20 @@ it("Each competition fetched from the website must become a button", async () =>
const buttons = Array.from(container.querySelectorAll("button"));
- // First button should be manual selection
- expect(buttons[0].innerHTML).toBe("Manual Selection");
+ // First button should be the collapse button
+ expect(buttons[0].innerHTML).toBe(
+ ``
+ );
+
+ // Second button should be manual selection
+ expect(buttons[1].innerHTML).toBe("Manual Selection");
// Last button should be Log Out
expect(buttons[buttons.length - 1].innerHTML).toBe("Log Out");
// Each competition must have a button
for (let i = 0; i < competitions.length; i++) {
- expect(competitions[i].name).toBe(buttons[i + 1].innerHTML);
+ expect(competitions[i].name).toBe(buttons[i + 2].innerHTML);
}
// We should welcome the user