Skip to content

Commit dd5d8f5

Browse files
committed
Merge main into nametag CSV fix
2 parents 4cfa780 + a0a53ac commit dd5d8f5

32 files changed

Lines changed: 1957 additions & 361 deletions

.github/workflows/lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
- name: Setup Node.js
2020
uses: actions/setup-node@v4
2121
with:
22-
node-version: '18'
22+
node-version-file: '.nvmrc'
2323
cache: 'yarn'
2424

2525
- name: Install dependencies

.github/workflows/type-check.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
- name: Setup Node.js
2020
uses: actions/setup-node@v4
2121
with:
22-
node-version: '18'
22+
node-version-file: '.nvmrc'
2323
cache: 'yarn'
2424

2525
- name: Install dependencies

src/components/RoundActionButtons.tsx

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1+
import { hasDistributedAttempts, parseActivityCode } from '../lib/domain/activities';
12
import { type ActivityWithParent } from '../lib/domain/types';
2-
import { Recipes } from '../lib/recipes';
3+
import Box from '@mui/material/Box';
34
import Button from '@mui/material/Button';
4-
import MenuItem from '@mui/material/MenuItem';
5-
import Select from '@mui/material/Select';
6-
import FormControl from '@mui/material/FormControl';
7-
import InputLabel from '@mui/material/InputLabel';
85
import { type Person } from '@wca/helpers';
96

107
interface RoundActionButtonsProps {
@@ -14,14 +11,14 @@ interface RoundActionButtonsProps {
1411
activityCode: string;
1512
onConfigureAssignments: () => void;
1613
onGenerateAssignments: () => void;
17-
recipeId: string;
18-
onChangeRecipeId: (recipeId: string) => void;
19-
onRunRecipe: () => void;
14+
onAssignToRoundAttempt: () => void;
15+
onResetAttemptAssignments: () => void;
2016
onConfigureStationNumbers: (activityCode: string) => void;
2117
onConfigureGroups: () => void;
2218
onResetAll: () => void;
2319
onResetNonScrambling: () => void;
2420
onConfigureGroupCounts: () => void;
21+
isDistributedAttemptRoundLevel: boolean;
2522
}
2623

2724
export const RoundActionButtons = ({
@@ -31,15 +28,52 @@ export const RoundActionButtons = ({
3128
activityCode,
3229
onConfigureAssignments,
3330
onGenerateAssignments,
34-
recipeId,
35-
onChangeRecipeId,
36-
onRunRecipe,
31+
onAssignToRoundAttempt,
32+
onResetAttemptAssignments,
3733
onConfigureStationNumbers,
3834
onConfigureGroups,
3935
onResetAll,
4036
onResetNonScrambling,
4137
onConfigureGroupCounts,
38+
isDistributedAttemptRoundLevel,
4239
}: RoundActionButtonsProps) => {
40+
const { attemptNumber } = parseActivityCode(activityCode);
41+
const isAttemptActivity = hasDistributedAttempts(activityCode) && attemptNumber !== undefined;
42+
43+
if (isDistributedAttemptRoundLevel) {
44+
return (
45+
<>
46+
<Button onClick={onConfigureAssignments}>Configure Round Attempt Assignments</Button>
47+
<Button onClick={onAssignToRoundAttempt}>Assign All</Button>
48+
<Box sx={{ display: 'flex', flex: 1 }} />
49+
<Button color="error" onClick={onResetAttemptAssignments}>
50+
Clear Round Attempt Assignments
51+
</Button>
52+
</>
53+
);
54+
}
55+
56+
if (groups.length === 0 && isAttemptActivity) {
57+
if (personsAssignedToCompete.length > 0) {
58+
return (
59+
<>
60+
<Button onClick={onAssignToRoundAttempt}>Assign to Round Attempt</Button>
61+
<Box sx={{ display: 'flex', flex: 1 }} />
62+
<Button color="error" onClick={onResetAttemptAssignments}>
63+
Reset Attempt Assignments
64+
</Button>
65+
</>
66+
);
67+
}
68+
69+
return (
70+
<>
71+
<Button onClick={onAssignToRoundAttempt}>Assign to Round Attempt</Button>
72+
<Button onClick={onConfigureGroupCounts}>Configure Group Counts</Button>
73+
</>
74+
);
75+
}
76+
4377
if (groups.length === 0) {
4478
return (
4579
<>
@@ -53,22 +87,6 @@ export const RoundActionButtons = ({
5387
<>
5488
<Button onClick={onConfigureAssignments}>Configure Assignments</Button>
5589
<Button onClick={onGenerateAssignments}>Assign Competitor and Judging Assignments</Button>
56-
<FormControl size="small" sx={{ minWidth: 220, marginLeft: 2 }}>
57-
<InputLabel id="recipe-select-label">Recipe</InputLabel>
58-
<Select
59-
labelId="recipe-select-label"
60-
label="Recipe"
61-
value={recipeId}
62-
onChange={(e) => onChangeRecipeId(String(e.target.value))}
63-
>
64-
{Recipes.map((r) => (
65-
<MenuItem key={r.id} value={r.id}>
66-
{r.name}
67-
</MenuItem>
68-
))}
69-
</Select>
70-
</FormControl>
71-
<Button onClick={onRunRecipe}>Run Recipe</Button>
7290
<div style={{ display: 'flex', flex: 1 }} />
7391
<Button onClick={onConfigureGroups}>Configure Groups</Button>
7492
<Button color="error" onClick={onResetAll}>

src/components/RoundSelector/RoundListItem.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,17 @@ interface RoundListItemProps {
2121
activityCode: string;
2222
round: Round;
2323
selected: boolean;
24+
nestingLevel?: number;
2425
in?: boolean;
2526
}
2627

27-
function RoundListItem({ activityCode, round, selected, ...props }: RoundListItemProps) {
28+
function RoundListItem({
29+
activityCode,
30+
round,
31+
selected,
32+
nestingLevel = 0,
33+
...props
34+
}: RoundListItemProps) {
2835
const ref = useRef<HTMLAnchorElement>(null);
2936
const wcif = useAppSelector((state) => state.wcif);
3037
const realGroups = wcif ? findGroupActivitiesByRound(wcif, activityCode) : [];
@@ -75,6 +82,7 @@ function RoundListItem({ activityCode, round, selected, ...props }: RoundListIte
7582
component={RouterLink}
7683
to={`/competitions/${wcif?.id}/events/${activityCode}`}
7784
selected={selected}
85+
sx={{ pl: 2 + nestingLevel * 4 }}
7886
ref={ref}>
7987
<ListItemAvatar>
8088
<span className={`cubing-icon event-${eventId}`} />

src/components/RoundSelector/_tests_/RoundSelector.test.tsx

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,21 @@ vi.mock('../../../lib/domain/activities', async () => {
2626
});
2727

2828
vi.mock('../RoundListItem', () => ({
29-
default: ({ activityCode, selected }: { activityCode: string; selected: boolean }) => (
30-
<div data-testid="round-item" data-selected={selected} data-code={activityCode} />
29+
default: ({
30+
activityCode,
31+
selected,
32+
nestingLevel,
33+
}: {
34+
activityCode: string;
35+
selected: boolean;
36+
nestingLevel?: number;
37+
}) => (
38+
<div
39+
data-testid="round-item"
40+
data-selected={selected}
41+
data-code={activityCode}
42+
data-nesting={nestingLevel ?? 0}
43+
/>
3144
),
3245
}));
3346

@@ -85,4 +98,31 @@ describe('RoundSelector', () => {
8598

8699
expect(toggle).toBeChecked();
87100
});
101+
102+
it('renders round-level entry plus attempts for distributed-attempt events', () => {
103+
const wcif = {
104+
id: 'TestComp',
105+
events: [
106+
{
107+
id: '333fm',
108+
rounds: [{ id: '333fm-r1', format: '3', results: [], timeLimit: null, cutoff: null }],
109+
},
110+
],
111+
};
112+
113+
const state = { wcif } as unknown as AppState;
114+
useAppSelector.mockImplementation((selector: (state: AppState) => unknown) => selector(state));
115+
116+
const { getAllByTestId } = renderWithProviders(
117+
<RoundSelector competitionId="TestComp" onSelected={() => undefined} />
118+
);
119+
120+
const activityCodes = getAllByTestId('round-item').map((item) => item.getAttribute('data-code'));
121+
const nestingLevels = getAllByTestId('round-item').map((item) =>
122+
item.getAttribute('data-nesting')
123+
);
124+
125+
expect(activityCodes).toEqual(['333fm-r1', '333fm-r1-a1', '333fm-r1-a2', '333fm-r1-a3']);
126+
expect(nestingLevels).toEqual(['0', '1', '1', '1']);
127+
});
88128
});

src/components/RoundSelector/index.tsx

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ const RoundSelector = ({ onSelected }: RoundSelectorProps) => {
2727
const [showAllRounds, setShowAllRounds] = useState(false);
2828
const [selectedId, setSelectedId] = useState(wcif?.events[0]?.rounds[0]?.id || null);
2929

30+
const attemptCountForRound = (round: Round) => (round.format === 'm' ? 3 : +round.format);
31+
3032
const shouldShowRound = (round: Round) => {
3133
if (!wcif) return false;
3234

@@ -54,13 +56,18 @@ const RoundSelector = ({ onSelected }: RoundSelectorProps) => {
5456
.filter(shouldShowRound)
5557
: [];
5658

57-
const roundIds = rounds.flatMap((r) =>
58-
hasDistributedAttempts(r.id)
59-
? new Array(r.format === 'm' ? 3 : +r.format)
60-
.fill(0)
61-
.map((_, index) => `${r.id}-a${index + 1}`)
62-
: r.id
63-
);
59+
const roundIds = rounds.flatMap((r) => {
60+
if (!hasDistributedAttempts(r.id)) {
61+
return r.id;
62+
}
63+
64+
return [
65+
r.id,
66+
...new Array(attemptCountForRound(r))
67+
.fill(0)
68+
.map((_, index) => `${r.id}-a${index + 1}`),
69+
];
70+
});
6471

6572
const handleKeyDown = (e: KeyboardEvent) => {
6673
if (commandPromptOpen) {
@@ -124,31 +131,45 @@ const RoundSelector = ({ onSelected }: RoundSelectorProps) => {
124131
<TransitionGroup>
125132
{!hasDistributedAttempts(event.id)
126133
? roundsForEvent.map((round) => (
127-
<RoundListItem
128-
key={round.id}
129-
activityCode={round.id}
130-
round={round}
131-
selected={round.id === selectedId}
132-
in
133-
/>
134+
<RoundListItem
135+
key={round.id}
136+
activityCode={round.id}
137+
round={round}
138+
nestingLevel={0}
139+
selected={round.id === selectedId}
140+
in
141+
/>
134142
))
135143
: roundsForEvent.flatMap((round) => {
136-
const attempts = new Array(round.format === 'm' ? 3 : +round.format) // TODO: create helper function to calculate attempts
137-
.fill(0);
138-
139-
return attempts.map((_, index) => {
144+
const attempts = new Array(attemptCountForRound(round)).fill(0);
145+
146+
const roundListItem = (
147+
<RoundListItem
148+
key={round.id}
149+
activityCode={round.id}
150+
round={round}
151+
nestingLevel={0}
152+
selected={round.id === selectedId}
153+
in
154+
/>
155+
);
156+
157+
const attemptListItems = attempts.map((_, index) => {
140158
const attemptActivityCode = `${round.id}-a${index + 1}`;
141159

142160
return (
143161
<RoundListItem
144162
key={attemptActivityCode}
145163
activityCode={attemptActivityCode}
146164
round={round}
165+
nestingLevel={1}
147166
selected={attemptActivityCode === selectedId}
148167
in
149168
/>
150169
);
151170
});
171+
172+
return [roundListItem, ...attemptListItems];
152173
})}
153174
</TransitionGroup>
154175
</React.Fragment>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { RoundActionButtons } from '../RoundActionButtons';
2+
import { renderWithProviders } from '../../test-utils';
3+
import { describe, expect, it, vi } from 'vitest';
4+
5+
const baseProps = {
6+
groups: [],
7+
personsAssignedToCompete: [],
8+
personsShouldBeInRound: [],
9+
activityCode: '333fm-r1',
10+
onConfigureAssignments: vi.fn(),
11+
onGenerateAssignments: vi.fn(),
12+
onAssignToRoundAttempt: vi.fn(),
13+
onResetAttemptAssignments: vi.fn(),
14+
onConfigureStationNumbers: vi.fn(),
15+
onConfigureGroups: vi.fn(),
16+
onResetAll: vi.fn(),
17+
onResetNonScrambling: vi.fn(),
18+
onConfigureGroupCounts: vi.fn(),
19+
};
20+
21+
describe('RoundActionButtons', () => {
22+
it('shows distributed round-level attempt actions', () => {
23+
const { getByText, queryByText } = renderWithProviders(
24+
<RoundActionButtons {...baseProps} isDistributedAttemptRoundLevel />
25+
);
26+
27+
expect(getByText('Configure Round Attempt Assignments')).toBeInTheDocument();
28+
expect(getByText('Assign All')).toBeInTheDocument();
29+
expect(getByText('Clear Round Attempt Assignments')).toBeInTheDocument();
30+
expect(queryByText('Configure Group Counts')).not.toBeInTheDocument();
31+
});
32+
});

0 commit comments

Comments
 (0)