Reset bracket feature Closes #1574

This commit is contained in:
Kalle
2024-03-07 17:45:53 +02:00
parent 54d00182f5
commit be87a88544
5 changed files with 160 additions and 4 deletions

View File

@@ -465,7 +465,7 @@ export interface TournamentRound {
groupId: number;
id: GeneratedAlways<number>;
number: number;
stageId: StageId;
stageId: number;
}
export interface TournamentStage {

View File

@@ -516,3 +516,27 @@ export function setMatchAsCasted({
.execute();
});
}
export function resetBracket(tournamentStageId: number) {
return db.transaction().execute(async (trx) => {
await trx
.deleteFrom("TournamentMatch")
.where("stageId", "=", tournamentStageId)
.execute();
await trx
.deleteFrom("TournamentRound")
.where("stageId", "=", tournamentStageId)
.execute();
await trx
.deleteFrom("TournamentGroup")
.where("stageId", "=", tournamentStageId)
.execute();
await trx
.deleteFrom("TournamentStage")
.where("id", "=", tournamentStageId)
.execute();
});
}

View File

@@ -37,6 +37,7 @@ import { adminActionSchema } from "../tournament-schemas.server";
import { tournamentIdFromParams } from "../tournament-utils";
import { useTournament } from "./to.$id";
import { findMapPoolByTeamId } from "~/features/tournament-bracket/queries/findMapPoolByTeamId.server";
import { Input } from "~/components/Input";
export const action: ActionFunction = async ({ request, params }) => {
const user = await requireUserId(request);
@@ -164,8 +165,6 @@ export const action: ActionFunction = async ({ request, params }) => {
});
break;
}
// TODO: could also handle the case of admin trying
// to add members from a checked in team
case "ADD_MEMBER": {
validateIsTournamentOrganizer();
const team = tournament.teamById(data.teamId);
@@ -226,6 +225,31 @@ export const action: ActionFunction = async ({ request, params }) => {
});
break;
}
case "RESET_BRACKET": {
validateIsTournamentOrganizer();
validate(!tournament.ctx.isFinalized, "Tournament is finalized");
const bracketToResetIdx = tournament.brackets.findIndex(
(b) => b.id === data.stageId,
);
const bracketToReset = tournament.brackets[bracketToResetIdx];
validate(bracketToReset, "Invalid bracket id");
validate(!bracketToReset.preview, "Bracket has not started");
const inProgressBrackets = tournament.brackets.filter((b) => !b.preview);
validate(
inProgressBrackets.every(
(b) =>
!b.sources ||
b.sources.every((s) => s.bracketIdx !== bracketToResetIdx),
),
"Some bracket that sources teams from this bracket has started",
);
await TournamentRepository.resetBracket(data.stageId);
break;
}
default: {
assertUnreachable(data);
}
@@ -287,6 +311,8 @@ export default function TournamentAdminPage() {
<CastTwitchAccounts />
<Divider smallText>Participant list download</Divider>
<DownloadParticipants />
<Divider smallText>Bracket reset</Divider>
<BracketReset />
{isAdmin(user) ? <EnableMapList /> : null}
</div>
);
@@ -762,3 +788,68 @@ function handleDownload({
document.body.appendChild(element);
element.click();
}
function BracketReset() {
const tournament = useTournament();
const fetcher = useFetcher();
const inProgressBrackets = tournament.brackets.filter((b) => !b.preview);
const [_bracketToDelete, setBracketToDelete] = React.useState(
inProgressBrackets[0]?.id,
);
const [confirmText, setConfirmText] = React.useState("");
if (inProgressBrackets.length === 0) {
return <div className="text-lighter text-sm">No brackets in progress</div>;
}
const bracketToDelete = _bracketToDelete ?? inProgressBrackets[0].id;
const bracketToDeleteName = inProgressBrackets.find(
(bracket) => bracket.id === bracketToDelete,
)?.name;
return (
<div>
<fetcher.Form method="post" className="stack horizontal sm items-end">
<div>
<label htmlFor="bracket">Bracket</label>
<select
id="bracket"
name="stageId"
value={bracketToDelete}
onChange={(e) => setBracketToDelete(Number(e.target.value))}
>
{inProgressBrackets.map((bracket) => (
<option key={bracket.name} value={bracket.id}>
{bracket.name}
</option>
))}
</select>
</div>
<div>
<label htmlFor="bracket-confirmation">
Type bracket name (&quot;{bracketToDeleteName}&quot;) to confirm
</label>
<Input
value={confirmText}
onChange={(e) => setConfirmText(e.target.value)}
id="bracket-confirmation"
/>
</div>
<SubmitButton
_action="RESET_BRACKET"
state={fetcher.state}
disabled={confirmText !== bracketToDeleteName}
testId="reset-bracket-button"
>
Reset
</SubmitButton>
</fetcher.Form>
<FormMessage type="error" className="mt-2">
Resetting a bracket will delete all the match results in it (but not
other brackets) and reset the bracket to its initial state allowing you
to change participating teams.
</FormMessage>
</div>
);
}

View File

@@ -104,6 +104,10 @@ export const adminActionSchema = z.union([
z.array(z.string()),
),
}),
z.object({
_action: _action("RESET_BRACKET"),
stageId: id,
}),
]);
export const joinSchema = z.object({

View File

@@ -1,4 +1,4 @@
import { type Page, test, expect } from "@playwright/test";
import { expect, test, type Page } from "@playwright/test";
import { ADMIN_DISCORD_ID } from "~/constants";
import { NZAP_TEST_ID } from "~/db/seed/constants";
import {
@@ -452,4 +452,41 @@ test.describe("Tournament bracket", () => {
await backToBracket(page);
await expect(page.getByText("🔴 LIVE")).toBeVisible();
});
test("resets bracket", async ({ page }) => {
const tournamentId = 1;
await seed(page);
await impersonate(page);
await navigate({
page,
url: tournamentBracketsPage({ tournamentId }),
});
await page.getByTestId("finalize-bracket-button").click();
await isNotVisible(page.locator('[data-match-id="1"]'));
await page.locator('[data-match-id="2"]').click();
await reportResult({
page,
amountOfMapsToReport: 2,
sidesWithMoreThanFourPlayers: ["last"],
});
await page.getByTestId("admin-tab").click();
await page
.getByLabel('Type bracket name ("Main bracket") to confirm')
.fill("Main bracket");
await page.getByTestId("reset-bracket-button").click();
await page.getByLabel("Action").selectOption("CHECK_IN");
await page.getByLabel("Team").selectOption("1");
await submit(page);
await page.getByTestId("brackets-tab").click();
await page.getByTestId("finalize-bracket-button").click();
// bye is gone
await expect(page.locator('[data-match-id="1"]')).toBeVisible();
});
});