From d6ec4d5b7cbebbbf89642d545dd341497f622bb2 Mon Sep 17 00:00:00 2001 From: "Duncan P. N. Exon Smith" Date: Mon, 12 Feb 2024 10:16:33 -0800 Subject: [PATCH 1/4] Remove extra state variables to track what has loaded Instead of using extra state to track whether the tournament, rounds, and players have loaded, detect it: - Stop setting `tournament.id`, then test for `tournament.id === tournament_id`. - Initialize `raw_rounds` to null, then test for non-null. - Introduce `raw_players and initialize to null, then test for non-null. --- src/views/Tournament/Tournament.tsx | 43 ++++++++++++++--------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/views/Tournament/Tournament.tsx b/src/views/Tournament/Tournament.tsx index e350b1b914..f9fe5f9281 100644 --- a/src/views/Tournament/Tournament.tsx +++ b/src/views/Tournament/Tournament.tsx @@ -88,7 +88,7 @@ interface TournamentPlayers { } interface TournamentInterface { - id: number; + id?: number; name: string; director: player_cache.PlayerCacheEntry; time_start: string; @@ -141,13 +141,8 @@ export function Tournament(): JSX.Element { const ref_max_players = React.useRef(null); const [edit_save_state, setEditSaveState] = React.useState("none"); - const [tournament_loaded, setTournamentLoaded] = React.useState(false); - const [rounds_loaded, setRoundsLoaded] = React.useState(false); - const [players_loaded, setPlayersLoaded] = React.useState(false); - const loading = !rounds_loaded || !players_loaded || !tournament_loaded; const [tournament, setTournament] = React.useState({ - id: tournament_id, name: "", // TODO: replace {} with something that makes type sense. -bpj director: tournament_id === 0 ? user : ({} as any), @@ -187,19 +182,27 @@ export function Tournament(): JSX.Element { }); const [editing, setEditing] = React.useState(tournament_id === 0); - const [raw_rounds, setRawRounds] = React.useState([]); + const [raw_rounds, setRawRounds] = React.useState(null); const [explicitly_selected_round, setExplicitlySelectedRound] = React.useState< null | number | "standings" | "roster" >(null); - const [players, setPlayers] = React.useState({}); + const [raw_players, setRawPlayers] = React.useState(null); const [invite_result, setInviteResult] = React.useState(null); const [user_to_invite, setUserToInvite] = React.useState(null); + const tournament_loaded = tournament_id !== 0 && tournament.id === tournament_id; + const rounds_loaded = raw_rounds !== null; + const players_loaded = raw_players !== null; + const loading = !rounds_loaded || !players_loaded || !tournament_loaded; + const use_elimination_trees = is_elimination(tournament.tournament_type); - const rounds = React.useMemo( - () => computeRounds(raw_rounds, players, tournament.tournament_type), - [tournament.tournament_type, raw_rounds, players], - ); + + const players: TournamentPlayers = raw_players === null ? {} : raw_players; + const rounds = React.useMemo(() => { + return raw_rounds === null + ? [] + : computeRounds(raw_rounds, players, tournament.tournament_type); + }, [tournament.tournament_type, raw_rounds, players]); const sorted_players = React.useMemo( () => Object.keys(players) @@ -242,7 +245,9 @@ export function Tournament(): JSX.Element { : null; const raw_selected_round = - typeof selected_round_idx === "number" && rounds && rounds.length > selected_round_idx + typeof selected_round_idx === "number" && + raw_rounds && + raw_rounds.length > selected_round_idx ? raw_rounds[selected_round_idx] : null; @@ -274,13 +279,10 @@ export function Tournament(): JSX.Element { React.useEffect(() => { // Reset all other state if the user navigates to a new tournament. setEditing(tournament_id === 0); - setPlayersLoaded(false); - setRoundsLoaded(false); - setTournamentLoaded(false); setEditSaveState("none"); - setRawRounds([]); + setRawRounds(null); setExplicitlySelectedRound(null); - setPlayers({}); + setRawPlayers(null); setInviteResult(null); setUserToInvite(null); @@ -318,14 +320,12 @@ export function Tournament(): JSX.Element { get(`tournaments/${tournament_id}`) .then((t) => { setTournament(t); - setTournamentLoaded(true); }) .catch(errorAlerter); get(`tournaments/${tournament_id}/rounds`) .then((rounds) => { setRawRounds(rounds); - setRoundsLoaded(true); }) .catch(errorAlerter); @@ -351,8 +351,7 @@ export function Tournament(): JSX.Element { p.notes = _("Eliminated"); } } - setPlayers(players); - setPlayersLoaded(true); + setRawPlayers(players); }) .catch(errorAlerter); }; From b7b754175dcbcbb33d3b1f7cc1e0db5d5ef7c049 Mon Sep 17 00:00:00 2001 From: "Duncan P. N. Exon Smith" Date: Mon, 12 Feb 2024 10:25:26 -0800 Subject: [PATCH 2/4] Delay editing new tournaments until the group has loaded --- src/views/Tournament/Tournament.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/views/Tournament/Tournament.tsx b/src/views/Tournament/Tournament.tsx index f9fe5f9281..337a701526 100644 --- a/src/views/Tournament/Tournament.tsx +++ b/src/views/Tournament/Tournament.tsx @@ -195,6 +195,10 @@ export function Tournament(): JSX.Element { const players_loaded = raw_players !== null; const loading = !rounds_loaded || !players_loaded || !tournament_loaded; + const new_tournament_group_loaded = + !new_tournament_group_id || new_tournament_group_id === (tournament.group?.id ?? 0); + const ready_to_edit = editing && new_tournament_group_loaded; + const use_elimination_trees = is_elimination(tournament.tournament_type); const players: TournamentPlayers = raw_players === null ? {} : raw_players; @@ -921,7 +925,7 @@ export function Tournament(): JSX.Element { tournament.tournament_type === "opengotha" || null; - if (!tournament_loaded && !editing) { + if (!tournament_loaded && !ready_to_edit) { return ; } From 30251cbfbb24749f6fc32e13972232ab881627d9 Mon Sep 17 00:00:00 2001 From: "Duncan P. N. Exon Smith" Date: Fri, 9 Feb 2024 14:05:13 -0800 Subject: [PATCH 3/4] Allow tournament administrators to clone tournaments Add a "Clone Tournament" button to the tournament page that allows an existing tournament to be cloned. - Button shows up for anyone that can administer the tournament. - Takes the user to the "new tournament" page in the same group. - All tournament settings are pre-loaded to match the source tournament. This makes it easy to create new tournaments in a series, without the laborious and error-prone process of re-entering the settings. --- src/views/Tournament/Tournament.tsx | 102 +++++++++++++++++++++------- 1 file changed, 78 insertions(+), 24 deletions(-) diff --git a/src/views/Tournament/Tournament.tsx b/src/views/Tournament/Tournament.tsx index 337a701526..4d6f0015b5 100644 --- a/src/views/Tournament/Tournament.tsx +++ b/src/views/Tournament/Tournament.tsx @@ -87,6 +87,14 @@ interface TournamentPlayers { [k: string]: TournamentPlayer; } +interface TournamentSettings { + lower_bar: string; + upper_bar: string; + num_rounds: string; + group_size: string; + maximum_players: number | string; + active_round?: number; +} interface TournamentInterface { id?: number; name: string; @@ -108,14 +116,7 @@ interface TournamentInterface { first_pairing_method: string; subsequent_pairing_method: string; players_start: number; - settings: { - lower_bar: string; - upper_bar: string; - num_rounds: string; - group_size: string; - maximum_players: number | string; - active_round?: number; - }; + settings: TournamentSettings; lead_time_seconds: number; base_points: number; started?: string; @@ -127,6 +128,9 @@ interface TournamentInterface { group?: any; opengotha_standings?: boolean; } +interface LoadedTournamentInterface extends TournamentInterface { + id: number; +} type EditSaveState = "none" | "saving" | "reload"; @@ -142,7 +146,7 @@ export function Tournament(): JSX.Element { const [edit_save_state, setEditSaveState] = React.useState("none"); - const [tournament, setTournament] = React.useState({ + const default_tournament: TournamentInterface = { name: "", // TODO: replace {} with something that makes type sense. -bpj director: tournament_id === 0 ? user : ({} as any), @@ -179,7 +183,9 @@ export function Tournament(): JSX.Element { }, lead_time_seconds: 1800, base_points: 10.0, - }); + }; + const [tournament, setTournament] = React.useState(default_tournament); + const ref_tournament_to_clone = React.useRef(null); const [editing, setEditing] = React.useState(tournament_id === 0); const [raw_rounds, setRawRounds] = React.useState(null); @@ -190,7 +196,7 @@ export function Tournament(): JSX.Element { const [invite_result, setInviteResult] = React.useState(null); const [user_to_invite, setUserToInvite] = React.useState(null); - const tournament_loaded = tournament_id !== 0 && tournament.id === tournament_id; + const tournament_loaded = tournament_id !== 0 && tournament?.id === tournament_id; const rounds_loaded = raw_rounds !== null; const players_loaded = raw_players !== null; const loading = !rounds_loaded || !players_loaded || !tournament_loaded; @@ -259,7 +265,7 @@ export function Tournament(): JSX.Element { React.useEffect(() => { if (user.id === 1 && tournament_id === 0) { setTournament({ - ...tournament, + ...default_tournament, name: "Culture: join 4", time_start: moment(new Date()).add(1, "minute").format(), rules: "japanese", @@ -293,18 +299,19 @@ export function Tournament(): JSX.Element { setExtraActionCallback(renderExtraPlayerActions); if (tournament_id) { resolve(); - } - if (new_tournament_group_id) { - get(`groups/${new_tournament_group_id}`) - .then((group) => { - setTournament({ - ...tournament, - group: group, - rules: group?.rules ?? "japanese", - handicap: String(group?.handicap ?? 0), - }); - }) - .catch(errorAlerter); + } else if (ref_tournament_to_clone.current?.id) { + // Clone tournament. + copyTournamentToClone(ref_tournament_to_clone.current); + ref_tournament_to_clone.current = null; + } else { + // New tournament. + setTournament(default_tournament); + if (new_tournament_group_id) { + // New tournament in a group. + get(`groups/${new_tournament_group_id}`) + .then((group) => copyGroup(group)) + .catch(errorAlerter); + } } return () => { @@ -359,6 +366,39 @@ export function Tournament(): JSX.Element { }) .catch(errorAlerter); }; + const copyGroup = (group: any) => + setTournament({ + ...default_tournament, + group: group, + rules: group?.rules ?? "japanese", + handicap: String(group?.handicap ?? 0), + }); + const copyTournamentToClone = (src_tournament: LoadedTournamentInterface) => { + // Clean tournament settings. + const clean_settings: any = {}; + for (const key in src_tournament.settings) { + if (key in default_tournament.settings) { + clean_settings[key] = src_tournament.settings[key as keyof TournamentSettings]; + } + } + + // Clean tournament. + const clean_tournament: any = {}; + for (const key in src_tournament) { + if (key in default_tournament) { + clean_tournament[key] = src_tournament[key as keyof TournamentInterface]; + } + } + + setTournament({ + ...clean_tournament, + group: src_tournament.group, + settings: clean_settings, + director: default_tournament.director, + time_start: default_tournament.time_start, + }); + }; + const reloadTournament = () => { if (edit_save_state === "none") { resolve(); @@ -465,6 +505,14 @@ export function Tournament(): JSX.Element { }; const startEditing = () => setEditing(true); + const cloneTournament = () => { + ref_tournament_to_clone.current = tournament as LoadedTournamentInterface; + if (tournament?.group?.id) { + browserHistory.push(`/tournament/new/${tournament.group.id}`); + } else { + browserHistory.push(`/tournament/new`); + } + }; const save = () => { const clean_tournament: any = dup(tournament); const group = clean_tournament.group; @@ -1008,6 +1056,12 @@ export function Tournament(): JSX.Element { )} + {(tournament.can_administer || null) && ( + + )} + {((tournament.started == null && tournament.can_administer) || null) && (