Streamline and fix tournament team export 0b download

This commit is contained in:
Kalle
2026-09-27 10:36:21 +03:00
parent 3b30f9f112
commit 61d189f1fe
6 changed files with 119 additions and 61 deletions

View File

@@ -18,6 +18,11 @@
var(--layout-nav-height) +
env(safe-area-inset-bottom)
);
/* snapshots paint above the top layer, which would lift it over an open modal's backdrop */
:root:has(:modal) & {
view-transition-name: none;
}
}
@media screen and (min-width: 600px) {

View File

@@ -19,6 +19,11 @@
z-index: 10;
/* captured on its own so animating content slides under it instead of over it */
view-transition-name: layout-header;
/* snapshots paint above the top layer, which would lift it over an open modal's backdrop */
:root:has(:modal) & {
view-transition-name: none;
}
}
::view-transition-group(layout-header) {

View File

@@ -90,7 +90,7 @@ export function ExportDialog({
const [format, setFormat] = React.useState<ExportFormat>("list");
const [status, setStatus] = React.useState<ExportStatus>("all");
const [bracketIdx, setBracketIdx] = React.useState<number | null>(null);
const [bracketIdx, setBracketIdx] = React.useState(0);
const [sort, setSort] = React.useState<ExportSort>("seed");
const [roster, setRoster] = React.useState<ExportRoster>("full");
const [fields, setFields] = React.useState<Set<ExportField>>(
@@ -108,35 +108,37 @@ export function ExportDialog({
return next;
});
const selectedBracket = tournament.bracketsMeta[bracketIdx];
const bracketRequiresOwnCheckIn =
selectedBracket.requiresCheckIn && !selectedBracket.isStartingBracket;
const teams = scopedAndSortedTeams({
teams: allTeams,
status,
sort,
bracketIdx,
bracketRequiresOwnCheckIn,
bracketParticipantIds: new Set(
tournament.eligibleTeamIdsOfBracket(bracketIdx),
),
});
const content = buildContent({
teams,
format,
fields,
captainsOnly: roster === "captains",
bracketIdx,
bracketRequiresOwnCheckIn,
checkedInLabel: "Checked in",
notCheckedInLabel: "Not checked in",
});
const nothingToExportReason =
teams.length === 0
? "No teams match the selected bracket and status"
: fields.size === 0 || !content
? "Nothing to export with the selected fields"
: null;
const onDownload = () => {
const selectedBracket =
bracketIdx !== null ? tournament.bracketsMeta[bracketIdx] : null;
const bracketRequiresOwnCheckIn = Boolean(selectedBracket?.requiresCheckIn);
const teams = scopedAndSortedTeams({
teams: allTeams,
status,
sort,
bracketIdx,
bracketRequiresOwnCheckIn,
// a team belongs to a bracket whether or not it has checked in (the rest are pending check-in);
// both are scoped out of the export when they don't belong to it
bracketParticipantIds: selectedBracket
? new Set([
...selectedBracket.participantTournamentTeamIds,
...(selectedBracket.teamsPendingCheckIn ?? []),
])
: null,
});
const content = buildContent({
teams,
format,
fields,
captainsOnly: roster === "captains",
bracketIdx,
bracketRequiresOwnCheckIn,
checkedInLabel: "Checked in",
notCheckedInLabel: "Not checked in",
});
handleDownload({
filename: `participants.${format === "csv" ? "csv" : "txt"}`,
content,
@@ -187,17 +189,12 @@ export function ExportDialog({
{tournament.bracketsMeta.length > 1 ? (
<RadioRow
label="Bracket"
value={bracketIdx === null ? "all" : String(bracketIdx)}
onChange={(value) =>
setBracketIdx(value === "all" ? null : Number(value))
}
options={[
{ value: "all", label: "All brackets" },
...tournament.bracketsMeta.map((bracket, idx) => ({
value: String(idx),
label: bracket.name || `#${idx}`,
})),
]}
value={String(bracketIdx)}
onChange={(value) => setBracketIdx(Number(value))}
options={tournament.bracketsMeta.map((bracket, idx) => ({
value: String(idx),
label: bracket.name || `#${idx}`,
}))}
/>
) : null}
@@ -221,9 +218,17 @@ export function ExportDialog({
}))}
/>
<SendouButton onClick={onDownload} className="mx-auto">
Download
</SendouButton>
<div className="stack sm items-center">
<SendouButton
onClick={onDownload}
isDisabled={nothingToExportReason !== null}
>
Download
</SendouButton>
{nothingToExportReason ? (
<div className="text-sm text-error">{nothingToExportReason}</div>
) : null}
</div>
</div>
</SendouDialog>
);
@@ -264,10 +269,10 @@ function RadioRow<T extends string>({
function hasActiveCheckIn(
team: TournamentTeamFull,
bracketIdx: number | null,
bracketIdx: number,
bracketRequiresOwnCheckIn: boolean,
) {
if (bracketIdx !== null && bracketRequiresOwnCheckIn) {
if (bracketRequiresOwnCheckIn) {
return team.checkIns.some(
(checkIn) => checkIn.bracketIdx === bracketIdx && !checkIn.isCheckOut,
);
@@ -293,12 +298,12 @@ export function scopedAndSortedTeams({
teams: TournamentTeamFull[];
status: ExportStatus;
sort: ExportSort;
bracketIdx: number | null;
bracketIdx: number;
bracketRequiresOwnCheckIn: boolean;
bracketParticipantIds: Set<number> | null;
bracketParticipantIds: Set<number>;
}) {
const filtered = teams.filter((team) => {
if (bracketParticipantIds && !bracketParticipantIds.has(team.id)) {
if (!bracketParticipantIds.has(team.id)) {
return false;
}
switch (status) {
@@ -333,7 +338,7 @@ function teamFieldValue(
opts: {
checkedInLabel: string;
notCheckedInLabel: string;
bracketIdx: number | null;
bracketIdx: number;
bracketRequiresOwnCheckIn: boolean;
},
) {
@@ -389,7 +394,7 @@ function buildContent({
format: ExportFormat;
fields: Set<ExportField>;
captainsOnly: boolean;
bracketIdx: number | null;
bracketIdx: number;
bracketRequiresOwnCheckIn: boolean;
checkedInLabel: string;
notCheckedInLabel: string;

View File

@@ -449,6 +449,39 @@ describe("Adjusting team starting bracket", () => {
});
});
describe("eligibleTeamIdsOfBracket", () => {
const notCheckedIn = (teamId: number) =>
tournamentCtxTeam(teamId, { checkIns: [] });
test("includes teams not checked in for the starting bracket", () => {
const tournament = testTournament({
ctx: {
teams: [tournamentCtxTeam(1), tournamentCtxTeam(2), notCheckedIn(3)],
},
});
expect(tournament.eligibleTeamIdsOfBracket(0)).toEqual([1, 2, 3]);
});
test("includes only teams starting in the bracket when there are many starting brackets", () => {
const tournament = testTournament({
ctx: {
teams: [
tournamentCtxTeam(1, { startingBracketIdx: 0 }),
tournamentCtxTeam(2, { startingBracketIdx: 1 }),
notCheckedIn(3),
],
settings: {
bracketProgression: progressions.manyStartBrackets,
},
},
});
expect(tournament.eligibleTeamIdsOfBracket(0)).toEqual([1, 3]);
expect(tournament.eligibleTeamIdsOfBracket(1)).toEqual([2]);
});
});
describe("League divisions", () => {
const leagueTournament = (isLeague = true) =>
testTournament({

View File

@@ -284,23 +284,28 @@ export class Tournament {
}
/** Teams that can play in the bracket: its participants plus the ones still pending check-in. */
eligibleTeamsCountOfBracket(bracketIdx: number) {
eligibleTeamIdsOfBracket(bracketIdx: number) {
const bracket = this.bracketsMeta[bracketIdx];
if (bracket.sources) {
return (
(bracket.teamsPendingCheckIn ?? []).length +
bracket.participantTournamentTeamIds.length
);
return [
...bracket.participantTournamentTeamIds,
...(bracket.teamsPendingCheckIn ?? []),
];
}
if (!this.isMultiStartingBracket) {
return this.ctx.teams.length;
}
const teams = this.isMultiStartingBracket
? this.ctx.teams.filter(
(team) => (team.startingBracketIdx ?? 0) === bracketIdx,
)
: this.ctx.teams;
return this.ctx.teams.filter(
(team) => (team.startingBracketIdx ?? 0) === bracketIdx,
).length;
return teams.map((team) => team.id);
}
/** Count of {@link eligibleTeamIdsOfBracket}. */
eligibleTeamsCountOfBracket(bracketIdx: number) {
return this.eligibleTeamIdsOfBracket(bracketIdx).length;
}
/** Teams of the bracket: its participants, or every eligible team while it is a preview. */

View File

@@ -0,0 +1,5 @@
---
navItem: medal
type: bug
---
Participant export's "Not checked in" option now works for the starting bracket, the redundant "All brackets" option is removed, and downloading is disabled when there's nothing to export