mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-22 19:16:09 -05:00
Reset bracket feature Closes #1574
This commit is contained in:
@@ -465,7 +465,7 @@ export interface TournamentRound {
|
||||
groupId: number;
|
||||
id: GeneratedAlways<number>;
|
||||
number: number;
|
||||
stageId: StageId;
|
||||
stageId: number;
|
||||
}
|
||||
|
||||
export interface TournamentStage {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 ("{bracketToDeleteName}") 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user