diff --git a/package.json b/package.json
index f542405..715396d 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,7 @@
"@types/react-dom": "19.2.3",
"@uiw/react-codemirror": "^4.25.4",
"@vitejs/plugin-react": "^5.1.2",
- "@wca/helpers": "^1.1.6",
+ "@wca/helpers": "2.0.0",
"clsx": "^2.1.1",
"country-flag-icons": "^1.6.8",
"date-fns": "^4.1.0",
@@ -68,7 +68,6 @@
"vite-plugin-pwa": "^1.2.0",
"vite-plugin-svgr": "^4.3.0",
"vite-tsconfig-paths": "^6.0.5",
- "wca-group-generators": "^0.1.8",
"web-vitals": "^5.1.0"
},
"devDependencies": {
diff --git a/src/components/Errors/renderers/ValidationErrorRenderer.tsx b/src/components/Errors/renderers/ValidationErrorRenderer.tsx
index ba66a7b..ceeb38b 100644
--- a/src/components/Errors/renderers/ValidationErrorRenderer.tsx
+++ b/src/components/Errors/renderers/ValidationErrorRenderer.tsx
@@ -20,10 +20,15 @@ export const ValidationErrorRenderer = ({ error }: ValidationErrorRendererProps)
assignmentCode: string;
activity: Activity;
}) => {
+ const registrantId = error.data.person?.registrantId;
+ if (registrantId === null || registrantId === undefined) {
+ return;
+ }
+
dispatch(
bulkRemovePersonAssignments([
{
- registrantId: error.data.person?.registrantId,
+ registrantId,
activityId: activity.id,
assignmentCode,
},
diff --git a/src/components/RoundLimitInfo.tsx b/src/components/RoundLimitInfo.tsx
index 149b574..ff265b9 100644
--- a/src/components/RoundLimitInfo.tsx
+++ b/src/components/RoundLimitInfo.tsx
@@ -56,7 +56,7 @@ export const RoundLimitInfo = ({
Cutoff:
{round.cutoff.numberOfAttempts} attempts to get {'< '}
- {renderResultByEventId(eventId as EventId, 'average', round.cutoff.attemptResult)}
+ {renderResultByEventId(eventId as EventId, 'average', round.cutoff.resultValue)}
{personsShouldBeInRound.length > 0 && (
diff --git a/src/components/RoundSelector/_tests_/RoundListItem.test.tsx b/src/components/RoundSelector/_tests_/RoundListItem.test.tsx
index baded8f..ba8d71e 100644
--- a/src/components/RoundSelector/_tests_/RoundListItem.test.tsx
+++ b/src/components/RoundSelector/_tests_/RoundListItem.test.tsx
@@ -36,10 +36,11 @@ describe('RoundListItem', () => {
it('renders group and assignment summaries', () => {
const round: Round = {
id: '333-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: null,
results: [],
scrambleSetCount: 1,
extensions: [],
diff --git a/src/dialogs/ConfigureAssignmentsDialog/ConfigureAssignmentsDialog.test.tsx b/src/dialogs/ConfigureAssignmentsDialog/ConfigureAssignmentsDialog.test.tsx
index 4b06eab..5e6b19d 100644
--- a/src/dialogs/ConfigureAssignmentsDialog/ConfigureAssignmentsDialog.test.tsx
+++ b/src/dialogs/ConfigureAssignmentsDialog/ConfigureAssignmentsDialog.test.tsx
@@ -1,5 +1,10 @@
import ConfigureAssignmentsDialog from './ConfigureAssignmentsDialog';
-import { buildActivity, buildEvent, buildPerson, buildRound } from '../../store/reducers/_tests_/helpers';
+import {
+ buildActivity,
+ buildEvent,
+ buildPerson,
+ buildRound,
+} from '../../store/reducers/_tests_/helpers';
import INITIAL_STATE, { type AppState } from '../../store/initialState';
import reducer from '../../store/reducer';
import { renderWithProviders } from '../../test-utils';
@@ -72,7 +77,7 @@ const buildWcif = (persons: Person[]): Competition => ({
id: 'test-competition',
name: 'Test Competition',
shortName: 'Test',
- formatVersion: '1.0',
+ formatVersion: '2.0',
persons,
events: [
buildEvent({
@@ -115,11 +120,7 @@ const buildState = (persons: Person[] = [person]): AppState => ({
});
const renderWithStore = (ui: ReactElement, state = buildState()) => {
- const store = createStore(
- reducer as Reducer,
- state,
- applyMiddleware(thunk)
- );
+ const store = createStore(reducer as Reducer, state, applyMiddleware(thunk));
return {
store,
diff --git a/src/dialogs/ConfigureAssignmentsDialog/formatSeedResult.ts b/src/dialogs/ConfigureAssignmentsDialog/formatSeedResult.ts
index 9e452bb..e3271c2 100644
--- a/src/dialogs/ConfigureAssignmentsDialog/formatSeedResult.ts
+++ b/src/dialogs/ConfigureAssignmentsDialog/formatSeedResult.ts
@@ -1,5 +1,5 @@
import {
- type AttemptResult,
+ type ResultValue,
decodeMultiResult,
type EventId,
formatCentiseconds,
@@ -7,15 +7,15 @@ import {
type RankingType,
} from '@wca/helpers';
-const isValidResult = (result: AttemptResult | null | undefined) =>
+const isValidResult = (result: ResultValue | null | undefined) =>
result !== null && result !== undefined && !Number.isNaN(result as number);
-const formatFewestMovesAverage = (result: AttemptResult) => ((result as number) / 100).toFixed(2);
+const formatFewestMovesAverage = (result: ResultValue) => (result / 100).toFixed(2);
export const formatSeedResult = (
eventId: EventId,
rankingType: RankingType,
- result?: AttemptResult
+ result?: ResultValue
) => {
if (!result) {
return '-';
diff --git a/src/dialogs/ConfigureAssignmentsDialog/hooks/usePersonsData.ts b/src/dialogs/ConfigureAssignmentsDialog/hooks/usePersonsData.ts
index e28acf3..4763e18 100644
--- a/src/dialogs/ConfigureAssignmentsDialog/hooks/usePersonsData.ts
+++ b/src/dialogs/ConfigureAssignmentsDialog/hooks/usePersonsData.ts
@@ -2,6 +2,7 @@ import {
acceptedRegistration,
byPROrResult,
getSeedResult,
+ hasRegistrantId,
isOrganizerOrDelegate,
registeredForEvent,
shouldBeInRound,
@@ -30,6 +31,7 @@ export function usePersonsData(
}
const personsWithSeedResult = wcif.persons
+ .filter(hasRegistrantId)
.filter((p) => {
// if competitor does not have an accepted registration, do not show them
if (!acceptedRegistration(p)) {
diff --git a/src/dialogs/ConfigureAssignmentsDialog/types.ts b/src/dialogs/ConfigureAssignmentsDialog/types.ts
index cf2dc30..39dc6f5 100644
--- a/src/dialogs/ConfigureAssignmentsDialog/types.ts
+++ b/src/dialogs/ConfigureAssignmentsDialog/types.ts
@@ -1,13 +1,14 @@
import type { ActivityWithParent } from '../../lib/domain/activities';
-import type { AttemptResult, Person } from '@wca/helpers';
+import type { PersonWithRegistrantId } from '../../lib/domain/persons';
+import type { ResultValue } from '@wca/helpers';
export type CompetitorSort = 'speed' | 'name';
-export interface PersonWithSeedResult extends Person {
+export interface PersonWithSeedResult extends PersonWithRegistrantId {
seedResult: {
ranking: number;
- average?: AttemptResult;
- single?: AttemptResult;
+ average?: ResultValue;
+ single?: ResultValue;
};
}
diff --git a/src/dialogs/ConfigureAssignmentsDialog/utils/calcRanking.ts b/src/dialogs/ConfigureAssignmentsDialog/utils/calcRanking.ts
index a5b2455..f6720c5 100644
--- a/src/dialogs/ConfigureAssignmentsDialog/utils/calcRanking.ts
+++ b/src/dialogs/ConfigureAssignmentsDialog/utils/calcRanking.ts
@@ -1,17 +1,17 @@
-import type { AttemptResult, Person } from '@wca/helpers';
+import type { Person, ResultValue } from '@wca/helpers';
export function calcRanking(
person: Person & {
seedResult?: {
- average?: AttemptResult;
- single?: AttemptResult;
+ average?: ResultValue;
+ single?: ResultValue;
};
},
lastPerson?: Person & {
seedResult?: {
ranking?: number;
- average?: AttemptResult;
- single?: AttemptResult;
+ average?: ResultValue;
+ single?: ResultValue;
};
}
) {
diff --git a/src/dialogs/ConfigureStationNumbersDialog.tsx b/src/dialogs/ConfigureStationNumbersDialog.tsx
index 407dbe3..154e4cd 100644
--- a/src/dialogs/ConfigureStationNumbersDialog.tsx
+++ b/src/dialogs/ConfigureStationNumbersDialog.tsx
@@ -4,7 +4,7 @@ import {
parseActivityCode,
roomByActivity,
} from '../lib/domain/activities';
-import { byPROrResult, getSeedResult } from '../lib/domain/persons';
+import { byPROrResult, getSeedResult, type PersonWithRegistrantId } from '../lib/domain/persons';
import { useAppSelector } from '../store';
import { bulkUpsertPersonAssignments, upsertPersonAssignments } from '../store/actions';
import { selectPersonsAssignedForRound } from '../store/selectors';
@@ -26,7 +26,7 @@ import {
GridToolbarContainer,
} from '@mui/x-data-grid';
import { formatCentiseconds } from '@wca/helpers';
-import type { Activity, Assignment, Person, Room } from '@wca/helpers';
+import type { Activity, Assignment, Room } from '@wca/helpers';
import { useCallback, useMemo, useRef } from 'react';
import { useDispatch } from 'react-redux';
@@ -38,7 +38,7 @@ type ExtendedAssignment = Assignment & {
type SeedResult = ReturnType;
-type PersonWithAssignment = Person & {
+type PersonWithAssignment = PersonWithRegistrantId & {
assignment: ExtendedAssignment;
seedResult?: SeedResult;
};
@@ -129,7 +129,6 @@ const ConfigureStationNumbersDialog = ({
a.push({
...p,
assignment: competitorAssignment,
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
seedResult: getSeedResult(currentWcif, activityCode, p),
});
}
diff --git a/src/lib/assignmentGenerators/generateAssignmentsForRoundAttempt.ts b/src/lib/assignmentGenerators/generateAssignmentsForRoundAttempt.ts
index 735997f..45b64e2 100644
--- a/src/lib/assignmentGenerators/generateAssignmentsForRoundAttempt.ts
+++ b/src/lib/assignmentGenerators/generateAssignmentsForRoundAttempt.ts
@@ -1,6 +1,6 @@
import { byPROrResult, findRoundActivitiesById, parseActivityCode } from '../domain';
import { isCompetitorAssignment, missingCompetitorAssignments } from '../domain';
-import { personsShouldBeInRound } from '../domain';
+import { hasRegistrantId, personsShouldBeInRound } from '../domain';
import { type InProgressAssignmment } from '../types';
import { byName } from '../utils';
import { createGroupAssignment } from '../wcif';
@@ -73,6 +73,7 @@ export const generateAssignmentsForRoundAttempt = (
return (assignments: InProgressAssignmment[]): InProgressAssignmment[] => {
const persons = personsShouldBeInRound(round)(wcif.persons)
+ .filter(hasRegistrantId)
.filter(missingCompetitorAssignments({ assignments, groupIds: attemptActivityIds }))
.sort(byName)
.sort(byPROrResult(event, roundNumber));
diff --git a/src/lib/assignmentGenerators/generateCompetingAssignmentsForStaff.ts b/src/lib/assignmentGenerators/generateCompetingAssignmentsForStaff.ts
index 7c17d04..4443455 100644
--- a/src/lib/assignmentGenerators/generateCompetingAssignmentsForStaff.ts
+++ b/src/lib/assignmentGenerators/generateCompetingAssignmentsForStaff.ts
@@ -1,6 +1,6 @@
import { type ActivityWithParent, findGroupActivitiesByRound, parseActivityCode } from '../domain';
import { hasStaffAssignment, isStaffAssignment, missingCompetitorAssignments } from '../domain';
-import { personsShouldBeInRound } from '../domain';
+import { hasRegistrantId, personsShouldBeInRound } from '../domain';
import { type InProgressAssignmment } from '../types';
import { createGroupAssignment, previousGroupForActivity } from '../wcif';
import { type Competition, type Event, type Person } from '@wca/helpers';
@@ -64,6 +64,7 @@ export const generateCompetingAssignmentsForStaff = (
const groupIds = groups.map((g) => g.id);
const persons = personsShouldBeInRound(round)(wcif.persons)
+ .filter(hasRegistrantId)
.filter(hasStaffAssignment({ groupIds }))
.filter(missingCompetitorAssignments({ groupIds }));
diff --git a/src/lib/assignmentGenerators/generateCompetingGroupActitivitesForEveryone.ts b/src/lib/assignmentGenerators/generateCompetingGroupActitivitesForEveryone.ts
index 428cff9..5c447e2 100644
--- a/src/lib/assignmentGenerators/generateCompetingGroupActitivitesForEveryone.ts
+++ b/src/lib/assignmentGenerators/generateCompetingGroupActitivitesForEveryone.ts
@@ -1,6 +1,6 @@
import { findGroupActivitiesByRound, parseActivityCode } from '../domain';
import { isCompetitorAssignment, missingCompetitorAssignments } from '../domain';
-import { byPROrResult, personsShouldBeInRound } from '../domain';
+import { byPROrResult, hasRegistrantId, personsShouldBeInRound } from '../domain';
import { type InProgressAssignmment } from '../types';
import { byName } from '../utils';
import { createGroupAssignment } from '../wcif';
@@ -29,18 +29,21 @@ export const generateCompetingGroupActitivitesForEveryone = (
allExistingCompetiorAssignments[group.id] = 0;
}
- personsInRound.reduce((sizes, person) => {
- const competingAssignment = person.assignments?.find(
- (a) => groupIds.includes(+a.activityId) && isCompetitorAssignment(a)
- );
+ personsInRound.reduce(
+ (sizes, person) => {
+ const competingAssignment = person.assignments?.find(
+ (a) => groupIds.includes(+a.activityId) && isCompetitorAssignment(a)
+ );
- return {
- ...sizes,
- ...(competingAssignment
- ? { [competingAssignment.activityId]: sizes[+competingAssignment.activityId] + 1 }
- : {}),
- };
- }, {} as Record);
+ return {
+ ...sizes,
+ ...(competingAssignment
+ ? { [competingAssignment.activityId]: sizes[+competingAssignment.activityId] + 1 }
+ : {}),
+ };
+ },
+ {} as Record
+ );
const nextGroupActivityIdToAssign = (assignments: InProgressAssignmment[]) => {
const groupSizes: Record = assignments
@@ -69,6 +72,7 @@ export const generateCompetingGroupActitivitesForEveryone = (
return (assignments: InProgressAssignmment[]): InProgressAssignmment[] => {
const persons = personsShouldBeInRound(round)(wcif.persons)
+ .filter(hasRegistrantId)
.filter(missingCompetitorAssignments({ assignments, groupIds }))
.sort(byName)
.sort(byPROrResult(event, roundNumber));
diff --git a/src/lib/assignmentGenerators/generateGroupAssignmentsForDelegatesAndOrganizers.ts b/src/lib/assignmentGenerators/generateGroupAssignmentsForDelegatesAndOrganizers.ts
index 6dd6657..de00a53 100644
--- a/src/lib/assignmentGenerators/generateGroupAssignmentsForDelegatesAndOrganizers.ts
+++ b/src/lib/assignmentGenerators/generateGroupAssignmentsForDelegatesAndOrganizers.ts
@@ -1,6 +1,7 @@
import {
byPROrResult,
findRoundActivitiesById,
+ hasRegistrantId,
isOrganizerOrDelegate,
missingCompetitorAssignments,
parseActivityCode,
@@ -38,6 +39,7 @@ export const generateGroupAssignmentsForDelegatesAndOrganizers = (
return (assignments: InProgressAssignmment[]): InProgressAssignmment[] => {
const persons = personsShouldBeInRound(round)(wcif.persons)
+ .filter(hasRegistrantId)
.filter(missingCompetitorAssignments({ assignments, groupIds }))
.filter(isOrganizerOrDelegate)
.sort(byName)
diff --git a/src/lib/assignmentGenerators/generateJudgeAssignmentsFromCompetingAssignments.test.ts b/src/lib/assignmentGenerators/generateJudgeAssignmentsFromCompetingAssignments.test.ts
index 9f9b116..f74100e 100644
--- a/src/lib/assignmentGenerators/generateJudgeAssignmentsFromCompetingAssignments.test.ts
+++ b/src/lib/assignmentGenerators/generateJudgeAssignmentsFromCompetingAssignments.test.ts
@@ -14,10 +14,11 @@ const roundActivity: Activity = {
const round: Round = {
id: '333-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: null,
scrambleSetCount: 1,
results: [],
extensions: [],
@@ -53,7 +54,7 @@ const buildCompetition = (groupCount: number, persons: Person[]): Competition =>
const groups = createGroupActivities(groupCount);
return {
- formatVersion: 'v1.0',
+ formatVersion: '2.0',
id: 'test-comp',
name: 'Test Competition',
shortName: 'Test',
@@ -91,7 +92,7 @@ const buildCompetition = (groupCount: number, persons: Person[]): Competition =>
},
],
},
- series: [],
+ series: null,
competitorLimit: null,
extensions: [],
registrationInfo: {
diff --git a/src/lib/assignmentGenerators/generateJudgeAssignmentsFromCompetingAssignments.ts b/src/lib/assignmentGenerators/generateJudgeAssignmentsFromCompetingAssignments.ts
index 61570d3..25e38a6 100644
--- a/src/lib/assignmentGenerators/generateJudgeAssignmentsFromCompetingAssignments.ts
+++ b/src/lib/assignmentGenerators/generateJudgeAssignmentsFromCompetingAssignments.ts
@@ -4,7 +4,7 @@ import {
hasCompetitorAssignment,
missingStaffAssignments,
} from '../domain';
-import { isOrganizerOrDelegate, personsShouldBeInRound } from '../domain';
+import { hasRegistrantId, isOrganizerOrDelegate, personsShouldBeInRound } from '../domain';
import { type InProgressAssignmment } from '../types';
import { createGroupAssignment, nextGroupForActivity } from '../wcif';
import { type Competition, type Event } from '@wca/helpers';
@@ -34,6 +34,7 @@ export const generateJudgeAssignmentsFromCompetingAssignments = (
const personFilterContext = { assignments, groupIds };
const persons = personsShouldBeInRound(round)(wcif.persons)
+ .filter(hasRegistrantId)
.filter(hasCompetitorAssignment(personFilterContext))
.filter(missingStaffAssignments(personFilterContext))
.filter((p) => !isOrganizerOrDelegate(p));
diff --git a/src/lib/domain/persons.test.ts b/src/lib/domain/persons.test.ts
index b26ce69..3887bb6 100644
--- a/src/lib/domain/persons.test.ts
+++ b/src/lib/domain/persons.test.ts
@@ -149,10 +149,11 @@ describe('shouldBeInRound', () => {
it('uses results when available', () => {
const round: Round = {
id: '333-r2',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: null,
scrambleSetCount: 1,
results: [
{ personId: 1, ranking: 1, attempts: [], best: 0, average: 0 },
@@ -169,10 +170,11 @@ describe('shouldBeInRound', () => {
it('uses registration for round 1', () => {
const round: Round = {
id: '333-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: null,
scrambleSetCount: 1,
results: [],
extensions: [],
@@ -193,10 +195,11 @@ describe('shouldBeInRound', () => {
it('returns false for non-first rounds without results', () => {
const round: Round = {
id: '333-r2',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: null,
scrambleSetCount: 1,
results: [],
extensions: [],
@@ -209,10 +212,10 @@ describe('shouldBeInRound', () => {
it('uses registration for linked registration rounds beyond round 1', () => {
const round: Round = {
id: 'clock-r2',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
scrambleSetCount: 1,
results: [],
participationRuleset: {
@@ -241,10 +244,11 @@ describe('personsShouldBeInRound', () => {
it('filters persons for the round', () => {
const round: Round = {
id: '333-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: null,
scrambleSetCount: 1,
results: [],
extensions: [],
@@ -273,7 +277,7 @@ describe('findPR', () => {
{
eventId: '333' as const,
type: 'single' as const,
- best: 1000,
+ value: 1000,
worldRanking: 100,
continentalRanking: 50,
nationalRanking: 10,
@@ -281,7 +285,7 @@ describe('findPR', () => {
{
eventId: '333' as const,
type: 'average' as const,
- best: 1200,
+ value: 1200,
worldRanking: 150,
continentalRanking: 60,
nationalRanking: 15,
@@ -297,7 +301,7 @@ describe('findPR', () => {
{
eventId: '333' as const,
type: 'single' as const,
- best: 1000,
+ value: 1000,
worldRanking: 100,
continentalRanking: 50,
nationalRanking: 10,
@@ -310,19 +314,6 @@ describe('findPR', () => {
});
describe('getPersonalBestValue', () => {
- it('reads the v1 best field', () => {
- expect(
- getPersonalBestValue({
- eventId: '333',
- type: 'single',
- best: 1000,
- worldRanking: 1,
- continentalRanking: 1,
- nationalRanking: 1,
- })
- ).toBe(1000);
- });
-
it('reads the v2 value field', () => {
expect(
getPersonalBestValue({
@@ -332,7 +323,7 @@ describe('getPersonalBestValue', () => {
worldRanking: 1,
continentalRanking: 1,
nationalRanking: 1,
- } as never)
+ })
).toBe(1000);
});
});
@@ -359,7 +350,7 @@ describe('byPsychsheet', () => {
{
eventId: '333',
type: 'average',
- best: 1200,
+ value: 1200,
worldRanking: 100,
continentalRanking: 50,
nationalRanking: 10,
@@ -371,7 +362,7 @@ describe('byPsychsheet', () => {
{
eventId: '333',
type: 'average',
- best: 1400,
+ value: 1400,
worldRanking: 200,
continentalRanking: 60,
nationalRanking: 20,
@@ -389,7 +380,7 @@ describe('byPsychsheet', () => {
{
eventId: '333',
type: 'single',
- best: 1000,
+ value: 1000,
worldRanking: 150,
continentalRanking: 50,
nationalRanking: 10,
@@ -401,7 +392,7 @@ describe('byPsychsheet', () => {
{
eventId: '333',
type: 'single',
- best: 1100,
+ value: 1100,
worldRanking: 250,
continentalRanking: 60,
nationalRanking: 20,
@@ -418,7 +409,7 @@ describe('byPsychsheet', () => {
{
eventId: '333',
type: 'average',
- best: 1200,
+ value: 1200,
worldRanking: 200,
continentalRanking: 50,
nationalRanking: 10,
@@ -430,7 +421,7 @@ describe('byPsychsheet', () => {
{
eventId: '333',
type: 'single',
- best: 1000,
+ value: 1000,
worldRanking: 100,
continentalRanking: 50,
nationalRanking: 10,
@@ -551,10 +542,11 @@ describe('mayMakeTimeLimit', () => {
it('returns persons who may make time limit', () => {
const round: Round = {
id: '333-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: { centiseconds: 1500, cumulativeRoundIds: [] },
cutoff: null,
- advancementCondition: null,
+ participationRuleset: null,
scrambleSetCount: 1,
results: [],
extensions: [],
@@ -567,7 +559,7 @@ describe('mayMakeTimeLimit', () => {
{
eventId: '333',
type: 'single',
- best: 1000,
+ value: 1000,
worldRanking: 100,
continentalRanking: 50,
nationalRanking: 10,
@@ -580,7 +572,7 @@ describe('mayMakeTimeLimit', () => {
{
eventId: '333',
type: 'single',
- best: 2000,
+ value: 2000,
worldRanking: 200,
continentalRanking: 60,
nationalRanking: 20,
@@ -597,10 +589,11 @@ describe('mayMakeTimeLimit', () => {
it('returns empty array when no time limit', () => {
const round: Round = {
id: '333-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: null,
scrambleSetCount: 1,
results: [],
extensions: [],
@@ -614,10 +607,11 @@ describe('mayMakeCutoff', () => {
it('returns persons who may make cutoff', () => {
const round: Round = {
id: '333-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
- cutoff: { numberOfAttempts: 2, attemptResult: 1500 },
- advancementCondition: null,
+ cutoff: { numberOfAttempts: 2, resultValue: 1500 },
+ participationRuleset: null,
scrambleSetCount: 1,
results: [],
extensions: [],
@@ -630,7 +624,7 @@ describe('mayMakeCutoff', () => {
{
eventId: '333',
type: 'average',
- best: 1200,
+ value: 1200,
worldRanking: 100,
continentalRanking: 50,
nationalRanking: 10,
@@ -643,7 +637,7 @@ describe('mayMakeCutoff', () => {
{
eventId: '333',
type: 'average',
- best: 1800,
+ value: 1800,
worldRanking: 200,
continentalRanking: 60,
nationalRanking: 20,
@@ -660,10 +654,11 @@ describe('mayMakeCutoff', () => {
it('returns empty array when no cutoff', () => {
const round: Round = {
id: '333-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: null,
scrambleSetCount: 1,
results: [],
extensions: [],
diff --git a/src/lib/domain/persons.ts b/src/lib/domain/persons.ts
index 06decff..77d82a8 100644
--- a/src/lib/domain/persons.ts
+++ b/src/lib/domain/persons.ts
@@ -2,7 +2,6 @@ import { parseActivityCode } from './activities';
import {
type Activity,
type Assignment,
- type AttemptResult,
type Event,
type EventId,
type Person,
@@ -13,6 +12,11 @@ import {
} from '@wca/helpers';
import { usesRegistrationParticipation } from '../wcif/rounds';
+export type PersonWithRegistrantId = Person & { registrantId: number };
+
+export const hasRegistrantId = (person: Person): person is PersonWithRegistrantId =>
+ person.registrantId !== null;
+
/**
* @param {Person} person
*/
@@ -78,13 +82,7 @@ export const assignedInGroupsForRoles =
export const findPR = (personalBests: PersonalBest[], eventId: EventId, type: RankingType) =>
personalBests.find((pr) => pr.eventId === eventId && pr.type === type);
-type V2PersonalBest = Omit & { value: AttemptResult };
-
-/**
- * WCIF v1 calls this field `best`. WCIF v2 calls it `value`.
- */
-export const getPersonalBestValue = (personalBest: PersonalBest | V2PersonalBest) =>
- 'value' in personalBest ? personalBest.value : personalBest.best;
+export const getPersonalBestValue = (personalBest: PersonalBest) => personalBest.value;
/**
* Comparator for array.sort
@@ -242,7 +240,7 @@ export const mayMakeCutoff = (eventId: EventId, round?: Round, persons?: Person[
return false;
}
- return getPersonalBestValue(PR) <= cutoff.attemptResult;
+ return getPersonalBestValue(PR) <= cutoff.resultValue;
}) || []
);
};
diff --git a/src/lib/importExport/import.test.ts b/src/lib/importExport/import.test.ts
index e4ca4ce..190ce35 100644
--- a/src/lib/importExport/import.test.ts
+++ b/src/lib/importExport/import.test.ts
@@ -36,7 +36,7 @@ const buildCompetition = (rooms: any[], persons: Person[]): Competition => ({
id: 'comp',
shortName: 'comp',
name: 'Comp',
- formatVersion: 'v1.0',
+ formatVersion: '2.0',
competitorLimit: null,
extensions: [],
schedule: {
@@ -55,7 +55,7 @@ const buildCompetition = (rooms: any[], persons: Person[]): Competition => ({
},
],
},
- series: [],
+ series: null,
persons,
events: [
{
@@ -63,10 +63,11 @@ const buildCompetition = (rooms: any[], persons: Person[]): Competition => ({
rounds: [
{
id: '333-r1',
+ linkedRounds: null,
format: 'a',
cutoff: null,
timeLimit: null,
- advancementCondition: null,
+ participationRuleset: null,
results: [],
scrambleSetCount: 1,
extensions: [],
@@ -354,12 +355,14 @@ describe('import helpers', () => {
};
const wcif = buildCompetition([room], [competitor({})]);
- const withGroups = generateMissingGroupActivities(wcif, [{ activityCode: '333-r1-g1', roomId: 10 }]);
+ const withGroups = generateMissingGroupActivities(wcif, [
+ { activityCode: '333-r1-g1', roomId: 10 },
+ ]);
expect(withGroups.schedule.venues[0].rooms[0].activities[0].childActivities).toHaveLength(1);
- expect(withGroups.schedule.venues[0].rooms[0].activities[0].childActivities[0].activityCode).toBe(
- '333-r1-g1'
- );
+ expect(
+ withGroups.schedule.venues[0].rooms[0].activities[0].childActivities[0].activityCode
+ ).toBe('333-r1-g1');
expect(withGroups.schedule.venues[0].rooms[0].activities[1].childActivities).toHaveLength(0);
});
diff --git a/src/lib/importExport/import.ts b/src/lib/importExport/import.ts
index bc4417c..8431782 100644
--- a/src/lib/importExport/import.ts
+++ b/src/lib/importExport/import.ts
@@ -163,6 +163,10 @@ export const findCompetingAssignment = (
person: Person,
eventId: EventId
): ParsedAssignment => {
+ if (person.registrantId === null) {
+ throw new Error(`Cannot import assignments for ${person.name} without a registrant ID`);
+ }
+
const data = row[eventId]?.trim();
if (!data || data === '-') {
@@ -224,6 +228,10 @@ export const findStaffingAssignments = (
person: Person,
eventId: EventId
): ParsedAssignment[] | undefined => {
+ if (person.registrantId === null) {
+ throw new Error(`Cannot import assignments for ${person.name} without a registrant ID`);
+ }
+
const field = data.meta.fields.find((fieldName) => {
const split = fieldName.split('-');
return split[0] === eventId && split[1] === 'staff';
@@ -475,7 +483,9 @@ export const generateMissingGroupActivities = (
const venue = schedule.venues.find((v) => v.rooms.some((room) => room.id === roomId));
const room = venue?.rooms.find((room) => room.id === roomId);
- const roundActivity = room?.activities.find((activity) => activity.activityCode === eventRound);
+ const roundActivity = room?.activities.find(
+ (activity) => activity.activityCode === eventRound
+ );
if (!roundActivity) {
throw new Error(`Could not find round activity ${eventRound} in room ${roomId}`);
@@ -565,7 +575,7 @@ export const upsertCompetitorAssignments = (
assignments.forEach((assignment) => {
const person = persons.find((p) => p.registrantId === assignment.registrantId);
- if (!person) {
+ if (!person || person.registrantId === null) {
return;
}
person.assignments = person.assignments ?? [];
diff --git a/src/lib/recipes/activities.ts b/src/lib/recipes/activities.ts
index c8909fc..dbbc71f 100644
--- a/src/lib/recipes/activities.ts
+++ b/src/lib/recipes/activities.ts
@@ -1,7 +1,6 @@
-import { Competition, parseActivityCode } from '@wca/helpers';
-import { findAllActivities } from 'wca-group-generators';
-import { activityCodeIsChild } from '../domain/activities/activityCode';
-import { AssignmentStep } from './types';
+import { parseActivityCode, type Competition } from '@wca/helpers';
+import { activityCodeIsChild, findAllActivities } from '../domain/activities';
+import type { AssignmentStep } from './types';
const getBaseActivities = (
wcif: Competition,
diff --git a/src/lib/recipes/clusters.ts b/src/lib/recipes/clusters.ts
index 993ac53..d08e2b6 100644
--- a/src/lib/recipes/clusters.ts
+++ b/src/lib/recipes/clusters.ts
@@ -1,9 +1,14 @@
-import { Competition, Event, Person, Round, parseActivityCode } from '@wca/helpers';
+import {
+ parseActivityCode,
+ type Competition,
+ type Event,
+ type Person,
+ type Round,
+} from '@wca/helpers';
import Assignments from '../../config/assignments';
import { findGroupActivitiesByRound } from '../wcif/activities';
-import { acceptedRegistrations, personsShouldBeInRound } from '../domain/persons';
-import { ClusterDefinition } from './types';
-import { byPROrResult } from 'wca-group-generators';
+import { acceptedRegistrations, byPROrResult, personsShouldBeInRound } from '../domain/persons';
+import type { ClusterDefinition } from './types';
export const Filters = [
{
@@ -103,7 +108,12 @@ export const getBaseCluster = (
}
};
-export const sortCluster = (wcif: Competition, cluster: ClusterDefinition, persons: Person[], roundId: string) => {
+export const sortCluster = (
+ wcif: Competition,
+ cluster: ClusterDefinition,
+ persons: Person[],
+ roundId: string
+) => {
if (!cluster.sort) {
return persons;
}
@@ -119,8 +129,7 @@ export const sortCluster = (wcif: Competition, cluster: ClusterDefinition, perso
}
return persons;
-}
-
+};
export const getCluster = (wcif: Competition, cluster: ClusterDefinition, roundId: string) => {
const activityIds = findGroupActivitiesByRound(wcif, roundId).map((a) => a.id);
diff --git a/src/lib/utils/utils.ts b/src/lib/utils/utils.ts
index 02a044d..8528881 100644
--- a/src/lib/utils/utils.ts
+++ b/src/lib/utils/utils.ts
@@ -1,9 +1,4 @@
-import {
- type AttemptResult,
- type EventId,
- type RankingType,
- formatCentiseconds,
-} from '@wca/helpers';
+import { type ResultValue, type EventId, type RankingType, formatCentiseconds } from '@wca/helpers';
/**
* Returns a copy of the object with the value at the specified path transformed by the update function.
@@ -36,7 +31,6 @@ export const setIn = (
value: T[S]
): T => updateIn(object, properyChain, () => value);
-
/**
* Returns a copy of the object with the array at the specified path mapped with the given function.
*
@@ -87,15 +81,15 @@ export const byName = (a: { name: string }, b: { name: string }) => a.name.local
export const renderResultByEventId = (
eventId: EventId,
rankingType: RankingType,
- result: AttemptResult
+ result: ResultValue
) => {
if (eventId === '333fm') {
- return rankingType === 'average' ? ((result as number) / 100).toFixed(2).toString() : result;
+ return rankingType === 'average' ? (result / 100).toFixed(2) : result;
}
// if (eventId === '333mbf') {
// return formatMultiResult(decodeMultiResult('0' + (result as number).toString()));
// }
- return formatCentiseconds(result as number);
+ return formatCentiseconds(result);
};
diff --git a/src/lib/wcif/activities.test.ts b/src/lib/wcif/activities.test.ts
index cb384f9..fdfdf78 100644
--- a/src/lib/wcif/activities.test.ts
+++ b/src/lib/wcif/activities.test.ts
@@ -34,8 +34,8 @@ const buildWcif = (
id: 'TestComp',
name: 'Test Competition',
shortName: 'Test',
- formatVersion: '1.0',
- series: [],
+ formatVersion: '2.0',
+ series: null,
competitorLimit: null,
registrationInfo: null,
schedule: {
@@ -59,10 +59,11 @@ const buildWcif = (
rounds: [
{
id: '333-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: null,
results: [{ personId: 1, best: 1000, average: 1200, ranking: 1, attempts: [] }],
scrambleSetCount: 1,
extensions: [],
diff --git a/src/lib/wcif/extensions/wcif-extensions.d.ts b/src/lib/wcif/extensions/wcif-extensions.d.ts
deleted file mode 100644
index d518bd2..0000000
--- a/src/lib/wcif/extensions/wcif-extensions.d.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-// augment extensions module to make `data` generic
-
-declare module '@wca/helpers/lib/models/extension' {
- export interface Extension {
- data: T;
- id: string;
- specUrl: string;
- }
-}
diff --git a/src/lib/wcif/groups.test.ts b/src/lib/wcif/groups.test.ts
index 5369a4f..56eae93 100644
--- a/src/lib/wcif/groups.test.ts
+++ b/src/lib/wcif/groups.test.ts
@@ -62,9 +62,9 @@ const buildWcifWithRooms = (
id: 'test-competition',
shortName: 'test',
name: 'Test',
- formatVersion: 'v1.0',
+ formatVersion: '2.0',
competitorLimit: null,
- series: [],
+ series: null,
extensions: [],
registrationInfo: {
openTime: '2024-01-01T00:00:00Z',
diff --git a/src/lib/wcif/persons.test.ts b/src/lib/wcif/persons.test.ts
index e0aea1e..dc7490c 100644
--- a/src/lib/wcif/persons.test.ts
+++ b/src/lib/wcif/persons.test.ts
@@ -4,10 +4,11 @@ import type { Competition, Person, Result, Round } from '@wca/helpers';
const buildRound = (overrides: Partial): Round => ({
id: '333-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: null,
results: [],
scrambleSetCount: 1,
extensions: [],
@@ -19,8 +20,8 @@ const buildWcif = (rounds: Round[]): Competition =>
id: 'TestComp',
name: 'Test Competition',
shortName: 'Test',
- formatVersion: '1.0',
- series: [],
+ formatVersion: '2.0',
+ series: null,
competitorLimit: null,
registrationInfo: null,
schedule: {
@@ -102,7 +103,7 @@ describe('wcif persons helpers', () => {
{
eventId: '333',
type: 'average',
- best: 1500,
+ value: 1500,
worldRanking: 10,
continentalRanking: 5,
nationalRanking: 1,
@@ -110,7 +111,7 @@ describe('wcif persons helpers', () => {
{
eventId: '333',
type: 'single',
- best: 900,
+ value: 900,
worldRanking: 8,
continentalRanking: 3,
nationalRanking: 1,
diff --git a/src/lib/wcif/persons.ts b/src/lib/wcif/persons.ts
index 4f874d0..6b2af77 100644
--- a/src/lib/wcif/persons.ts
+++ b/src/lib/wcif/persons.ts
@@ -1,7 +1,7 @@
import { parseActivityCode } from '../domain/activities/activityCode';
import { roundFormatById } from '../domain/events';
import { findPR, getPersonalBestValue } from '../domain/persons';
-import { type Competition, type Person, type AttemptResult } from '@wca/helpers';
+import { type Competition, type Person, type ResultValue } from '@wca/helpers';
/** WCIF Person Lookup Functions */
@@ -41,8 +41,8 @@ export const findResultFromRound = (
};
export type SeedResult = {
- average?: AttemptResult;
- single?: AttemptResult;
+ average?: ResultValue;
+ single?: ResultValue;
};
/**
@@ -84,5 +84,9 @@ export const getSeedResult = (
};
}
+ if (person.registrantId === null) {
+ return;
+ }
+
return findResultFromRound(wcif, `${eventId}-r${roundNumber - 1}`, person.registrantId);
};
diff --git a/src/lib/wcif/rounds.test.ts b/src/lib/wcif/rounds.test.ts
index 10299ac..6334651 100644
--- a/src/lib/wcif/rounds.test.ts
+++ b/src/lib/wcif/rounds.test.ts
@@ -29,7 +29,7 @@ describe('usesRegistrationParticipation', () => {
});
describe('getDerivedAdvancementCondition', () => {
- it('maps linked-round result conditions into legacy advancement conditions', () => {
+ it('returns linked-round result conditions', () => {
const round = buildRound({
id: 'clock-r3',
participationRuleset: {
@@ -48,15 +48,20 @@ describe('getDerivedAdvancementCondition', () => {
expect(getDerivedAdvancementCondition(round)).toEqual({
type: 'percent',
- level: 75,
+ scope: 'average',
+ value: 75,
});
});
});
describe('formatAdvancementCondition', () => {
it('formats ranking and percent advancement conditions', () => {
- expect(formatAdvancementCondition({ type: 'ranking', level: 14 })).toBe('Top 14');
- expect(formatAdvancementCondition({ type: 'percent', level: 40 })).toBe('Top 40%');
+ expect(formatAdvancementCondition({ type: 'ranking', scope: 'average', value: 14 })).toBe(
+ 'Top 14'
+ );
+ expect(formatAdvancementCondition({ type: 'percent', scope: 'average', value: 40 })).toBe(
+ 'Top 40%'
+ );
});
it('returns an empty string for missing advancement conditions', () => {
@@ -66,7 +71,7 @@ describe('formatAdvancementCondition', () => {
});
describe('getAdvancementConditionForRound', () => {
- it('returns true when a round has a v2 participation ruleset', () => {
+ it('returns true for source rounds referenced by a downstream participation ruleset', () => {
const event = buildEvent({
id: 'clock',
rounds: [
@@ -108,7 +113,7 @@ describe('getAdvancementConditionForRound', () => {
expect(getAdvancementConditionForRound(event, 'clock-r1')).toBe(true);
expect(getAdvancementConditionForRound(event, 'clock-r2')).toBe(true);
- expect(getAdvancementConditionForRound(event, 'clock-r3')).toBe(true);
+ expect(getAdvancementConditionForRound(event, 'clock-r3')).toBe(false);
});
});
@@ -155,11 +160,13 @@ describe('getDisplayAdvancementConditionForRound', () => {
expect(getDisplayAdvancementConditionForRound(event, 'clock-r1')).toEqual({
type: 'percent',
- level: 75,
+ scope: 'average',
+ value: 75,
});
expect(getDisplayAdvancementConditionForRound(event, 'clock-r2')).toEqual({
type: 'percent',
- level: 75,
+ scope: 'average',
+ value: 75,
});
});
});
@@ -231,19 +238,28 @@ describe('getDualRoundDetails', () => {
});
describe('getParticipationConditionTextForRound', () => {
- it('formats legacy advancement text toward the next round', () => {
+ it('formats advancement text toward the next round', () => {
const event = buildEvent({
id: '333',
rounds: [
buildRound({
id: '333-r1',
- advancementCondition: { type: 'ranking', level: 14 },
}),
- buildRound({ id: '333-r2' }),
+ buildRound({
+ id: '333-r2',
+ participationRuleset: {
+ participationSource: {
+ type: 'round',
+ roundId: '333-r1',
+ resultCondition: { type: 'ranking', scope: 'average', value: 14 },
+ },
+ reservedPlaces: null,
+ },
+ }),
],
});
- expect(getParticipationConditionTextForRound(event, '333-r1')).toBe('Top 14 to next round');
+ expect(getParticipationConditionTextForRound(event, '333-r1')).toBe('Top 14 to round 2');
});
it('formats dual-round participation text for linked source and target rounds', () => {
@@ -389,18 +405,27 @@ describe('getParticipationSourceTextForRound', () => {
],
});
- expect(getParticipationSourceTextForRound(event, 'clock-r3')).toBe('Top 40% from dual rounds R1 & R2');
+ expect(getParticipationSourceTextForRound(event, 'clock-r3')).toBe(
+ 'Top 40% from dual rounds R1 & R2'
+ );
});
- it('formats legacy participation source for downstream rounds', () => {
+ it('formats participation source for downstream rounds', () => {
const event = buildEvent({
id: 'sq1',
rounds: [
+ buildRound({ id: 'sq1-r1' }),
buildRound({
- id: 'sq1-r1',
- advancementCondition: { type: 'ranking', level: 14 },
+ id: 'sq1-r2',
+ participationRuleset: {
+ participationSource: {
+ type: 'round',
+ roundId: 'sq1-r1',
+ resultCondition: { type: 'ranking', scope: 'average', value: 14 },
+ },
+ reservedPlaces: null,
+ },
}),
- buildRound({ id: 'sq1-r2' }),
],
});
@@ -409,15 +434,22 @@ describe('getParticipationSourceTextForRound', () => {
});
describe('getNextRoundParticipationTextForRound', () => {
- it('returns the downstream round participation for legacy rounds', () => {
+ it('returns the downstream round participation', () => {
const event = buildEvent({
id: 'sq1',
rounds: [
+ buildRound({ id: 'sq1-r1' }),
buildRound({
- id: 'sq1-r1',
- advancementCondition: { type: 'ranking', level: 14 },
+ id: 'sq1-r2',
+ participationRuleset: {
+ participationSource: {
+ type: 'round',
+ roundId: 'sq1-r1',
+ resultCondition: { type: 'ranking', scope: 'average', value: 14 },
+ },
+ reservedPlaces: null,
+ },
}),
- buildRound({ id: 'sq1-r2' }),
],
});
diff --git a/src/lib/wcif/rounds.ts b/src/lib/wcif/rounds.ts
index f222503..ce7016b 100644
--- a/src/lib/wcif/rounds.ts
+++ b/src/lib/wcif/rounds.ts
@@ -1,9 +1,9 @@
import { parseActivityCode } from '../domain/activities';
import {
formatCentiseconds,
- type AdvancementCondition,
type Event,
type ParticipationRuleset,
+ type ResultCondition,
type Round,
} from '@wca/helpers';
@@ -14,13 +14,8 @@ export interface DualRoundDetails {
isTargetRound: boolean;
}
-const hasLegacyAdvancementCondition = (
- round: Round
-): round is Round & { advancementCondition: AdvancementCondition } =>
- round.advancementCondition != null;
-
export const getParticipationRuleset = (round: Round): ParticipationRuleset | null =>
- round.participationRuleset ?? null;
+ round.participationRuleset;
export const usesRegistrationParticipation = (round: Round): boolean => {
const participationSource = getParticipationRuleset(round)?.participationSource;
@@ -32,80 +27,71 @@ export const usesRegistrationParticipation = (round: Round): boolean => {
return parseActivityCode(round.id).roundNumber === 1;
};
-export const getDerivedAdvancementCondition = (round: Round): AdvancementCondition | null => {
+export const getDerivedAdvancementCondition = (round: Round): ResultCondition | null => {
const participationSource = getParticipationRuleset(round)?.participationSource;
if (participationSource?.type !== 'linkedRounds' && participationSource?.type !== 'round') {
return null;
}
- return {
- type: participationSource.resultCondition.type,
- level: participationSource.resultCondition.value,
- };
+ return participationSource.resultCondition;
};
export const formatAdvancementCondition = (
- advancementCondition: AdvancementCondition | null | undefined
+ advancementCondition: ResultCondition | null | undefined
): string => {
if (!advancementCondition) {
return '';
}
- const { type, level } = advancementCondition;
+ const { type, value } = advancementCondition;
switch (type) {
case 'ranking':
- return `Top ${level}`;
+ return `Top ${value}`;
case 'percent':
- return `Top ${level}%`;
- case 'attemptResult':
- if (level === -2) {
+ return `Top ${value}%`;
+ case 'resultAchieved':
+ if (value === null) {
+ return 'Any result';
+ }
+
+ if (value === -2) {
return '> DNS';
}
- if (level === -1) {
+ if (value === -1) {
return '> DNF';
}
- return `< ${formatCentiseconds(level)}`;
+ return `< ${formatCentiseconds(value)}`;
default:
return '';
}
};
export const getAdvancementConditionForRound = (event: Event, roundId: string): boolean => {
- const round = event.rounds.find((candidate) => candidate.id === roundId);
-
- if (!round) {
- return false;
- }
-
- if (hasLegacyAdvancementCondition(round)) {
- return true;
- }
+ return event.rounds.some((candidate) => {
+ const participationSource = getParticipationRuleset(candidate)?.participationSource;
- return getParticipationRuleset(round) !== null;
+ return (
+ (participationSource?.type === 'round' && participationSource.roundId === roundId) ||
+ (participationSource?.type === 'linkedRounds' &&
+ participationSource.roundIds.includes(roundId))
+ );
+ });
};
export const getDisplayAdvancementConditionForRound = (
event: Event,
roundId: string
-): AdvancementCondition | null => {
- const round = event.rounds.find((candidate) => candidate.id === roundId);
-
- if (!round) {
- return null;
- }
-
- if (hasLegacyAdvancementCondition(round)) {
- return round.advancementCondition;
- }
-
+): ResultCondition | null => {
const nextRound = event.rounds.find((candidate) => {
const participationSource = getParticipationRuleset(candidate)?.participationSource;
return (
- participationSource?.type === 'linkedRounds' && participationSource.roundIds.includes(roundId)
+ (participationSource?.type === 'round' && participationSource.roundId === roundId) ||
+ (participationSource?.type === 'linkedRounds' &&
+ participationSource.roundIds.includes(roundId))
);
});
@@ -122,26 +108,18 @@ const formatLongRoundLabel = (roundId: string): string => {
return roundNumber ? `round ${roundNumber}` : roundId;
};
-const findLinkedTargetRound = (event: Event, roundId: string): Round | undefined =>
+const findSeededTargetRound = (event: Event, roundId: string): Round | undefined =>
event.rounds.find((candidate) => {
const participationSource = getParticipationRuleset(candidate)?.participationSource;
return (
- participationSource?.type === 'linkedRounds' && participationSource.roundIds.includes(roundId)
+ (participationSource?.type === 'round' && participationSource.roundId === roundId) ||
+ (participationSource?.type === 'linkedRounds' &&
+ participationSource.roundIds.includes(roundId))
);
});
-const findPreviousRound = (event: Event, roundId: string): Round | undefined => {
- const currentRoundIndex = event.rounds.findIndex((candidate) => candidate.id === roundId);
- return currentRoundIndex > 0 ? event.rounds[currentRoundIndex - 1] : undefined;
-};
-
-const findNextSequentialRound = (event: Event, roundId: string): Round | undefined => {
- const currentRoundIndex = event.rounds.findIndex((candidate) => candidate.id === roundId);
- return currentRoundIndex >= 0 ? event.rounds[currentRoundIndex + 1] : undefined;
-};
-
-const findNextSeededRound = (event: Event, roundId: string): Round | undefined =>
- findLinkedTargetRound(event, roundId) ?? findNextSequentialRound(event, roundId);
+const conditionText = (condition: ResultCondition | null): string =>
+ formatAdvancementCondition(condition);
export const getParticipationSourceTextForRound = (
event: Event,
@@ -157,37 +135,18 @@ export const getParticipationSourceTextForRound = (
if (participationSource?.type === 'linkedRounds') {
const sourceRounds = participationSource.roundIds.map(formatShortRoundLabel).join(' & ');
- return `${formatAdvancementCondition({
- type: participationSource.resultCondition.type,
- level: participationSource.resultCondition.value,
- })} from dual rounds ${sourceRounds}`;
+ return `${conditionText(participationSource.resultCondition)} from dual rounds ${sourceRounds}`;
}
if (participationSource?.type === 'round') {
- return `${formatAdvancementCondition({
- type: participationSource.resultCondition.type,
- level: participationSource.resultCondition.value,
- })} from previous round`;
+ return `${conditionText(participationSource.resultCondition)} from previous round`;
}
if (participationSource?.type === 'registrations') {
return 'Open to all registered competitors';
}
- const previousRound = findPreviousRound(event, roundId);
-
- if (previousRound && hasLegacyAdvancementCondition(previousRound)) {
- const advancementText = formatAdvancementCondition(previousRound.advancementCondition);
- return advancementText ? `${advancementText} from previous round` : null;
- }
-
- const linkedTargetRound = findLinkedTargetRound(event, roundId);
-
- if (!linkedTargetRound) {
- return null;
- }
-
- return getParticipationSourceTextForRound(event, linkedTargetRound.id);
+ return null;
};
export const getParticipationConditionTextForRound = (
@@ -200,28 +159,7 @@ export const getParticipationConditionTextForRound = (
return null;
}
- const linkedTargetRound = findLinkedTargetRound(event, roundId);
-
- if (linkedTargetRound) {
- const linkedTargetSource = getParticipationRuleset(linkedTargetRound)?.participationSource;
-
- if (linkedTargetSource?.type === 'linkedRounds') {
- const sourceRounds = linkedTargetSource.roundIds.map(formatShortRoundLabel).join(' & ');
- return `${formatAdvancementCondition({
- type: linkedTargetSource.resultCondition.type,
- level: linkedTargetSource.resultCondition.value,
- })} from dual rounds ${sourceRounds} to ${formatLongRoundLabel(linkedTargetRound.id)}`;
- }
-
- if (linkedTargetSource?.type === 'round') {
- return `${formatAdvancementCondition({
- type: linkedTargetSource.resultCondition.type,
- level: linkedTargetSource.resultCondition.value,
- })} to ${formatLongRoundLabel(linkedTargetRound.id)}`;
- }
- }
-
- const nextRound = findNextSequentialRound(event, roundId);
+ const nextRound = findSeededTargetRound(event, roundId);
if (!nextRound) {
return null;
@@ -231,22 +169,11 @@ export const getParticipationConditionTextForRound = (
if (nextParticipationSource?.type === 'linkedRounds') {
const sourceRounds = nextParticipationSource.roundIds.map(formatShortRoundLabel).join(' & ');
- return `${formatAdvancementCondition({
- type: nextParticipationSource.resultCondition.type,
- level: nextParticipationSource.resultCondition.value,
- })} from dual rounds ${sourceRounds} to ${formatLongRoundLabel(nextRound.id)}`;
+ return `${conditionText(nextParticipationSource.resultCondition)} from dual rounds ${sourceRounds} to ${formatLongRoundLabel(nextRound.id)}`;
}
if (nextParticipationSource?.type === 'round') {
- return `${formatAdvancementCondition({
- type: nextParticipationSource.resultCondition.type,
- level: nextParticipationSource.resultCondition.value,
- })} to ${formatLongRoundLabel(nextRound.id)}`;
- }
-
- if (hasLegacyAdvancementCondition(round)) {
- const advancementText = formatAdvancementCondition(round.advancementCondition);
- return advancementText ? `${advancementText} to next round` : null;
+ return `${conditionText(nextParticipationSource.resultCondition)} to ${formatLongRoundLabel(nextRound.id)}`;
}
return null;
@@ -256,7 +183,7 @@ export const getNextRoundParticipationTextForRound = (
event: Event,
roundId: string
): string | null => {
- const nextRound = findNextSeededRound(event, roundId);
+ const nextRound = findSeededTargetRound(event, roundId);
if (!nextRound) {
return null;
diff --git a/src/lib/wcif/v2-types.d.ts b/src/lib/wcif/v2-types.d.ts
deleted file mode 100644
index 504b2a6..0000000
--- a/src/lib/wcif/v2-types.d.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import '@wca/helpers';
-
-declare module '@wca/helpers' {
- export interface RegistrationsParticipationSource {
- type: 'registrations';
- }
-
- export interface ParticipationResultCondition {
- type: 'ranking' | 'percent' | 'attemptResult';
- scope?: 'average' | 'single';
- value: number;
- }
-
- export interface LinkedRoundsParticipationSource {
- type: 'linkedRounds';
- roundIds: string[];
- resultCondition: ParticipationResultCondition;
- }
-
- export interface RoundParticipationSource {
- type: 'round';
- roundId: string;
- resultCondition: ParticipationResultCondition;
- }
-
- export type ParticipationSource =
- | RegistrationsParticipationSource
- | LinkedRoundsParticipationSource
- | RoundParticipationSource;
-
- export interface ParticipationRuleset {
- participationSource: ParticipationSource;
- reservedPlaces: unknown | null;
- }
-
- export interface Round {
- linkedRounds?: string[] | null;
- participationRuleset?: ParticipationRuleset | null;
- }
-}
diff --git a/src/lib/wcif/validation/eventRoundValidation.test.ts b/src/lib/wcif/validation/eventRoundValidation.test.ts
index d86156f..0a8be92 100644
--- a/src/lib/wcif/validation/eventRoundValidation.test.ts
+++ b/src/lib/wcif/validation/eventRoundValidation.test.ts
@@ -13,7 +13,7 @@ import type { Competition, Event } from '@wca/helpers';
import { describe, it, expect } from 'vitest';
const mockWcif: Competition = {
- formatVersion: '1.0',
+ formatVersion: '2.0',
id: 'TestComp2024',
name: 'Test Competition 2024',
shortName: 'Test 2024',
@@ -24,7 +24,7 @@ const mockWcif: Competition = {
numberOfDays: 1,
venues: [],
},
- series: [],
+ series: null,
competitorLimit: 100,
extensions: [],
registrationInfo: {
@@ -61,10 +61,11 @@ describe('validateEventHasRounds', () => {
rounds: [
{
id: '333-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: null,
results: [],
scrambleSetCount: 1,
extensions: [],
@@ -88,20 +89,22 @@ describe('validateAdvancementConditions', () => {
rounds: [
{
id: '333-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null, // Missing!
+ participationRuleset: null, // Missing!
results: [],
scrambleSetCount: 1,
extensions: [],
},
{
id: '333-r2',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null, // This is final, so OK
+ participationRuleset: null, // This is final, so OK
results: [],
scrambleSetCount: 1,
extensions: [],
@@ -126,20 +129,32 @@ describe('validateAdvancementConditions', () => {
rounds: [
{
id: '333-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: { type: 'ranking', level: 12 },
+ participationRuleset: {
+ participationSource: { type: 'registrations' },
+ reservedPlaces: null,
+ },
results: [],
scrambleSetCount: 1,
extensions: [],
},
{
id: '333-r2',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: {
+ participationSource: {
+ type: 'round',
+ roundId: '333-r1',
+ resultCondition: { type: 'ranking', scope: 'average', value: 12 },
+ },
+ reservedPlaces: null,
+ },
results: [],
scrambleSetCount: 1,
extensions: [],
@@ -164,7 +179,6 @@ describe('validateAdvancementConditions', () => {
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
results: [],
scrambleSetCount: 2,
linkedRounds: ['clock-r1', 'clock-r2'],
@@ -181,7 +195,6 @@ describe('validateAdvancementConditions', () => {
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
results: [],
scrambleSetCount: 2,
linkedRounds: ['clock-r1', 'clock-r2'],
@@ -195,13 +208,12 @@ describe('validateAdvancementConditions', () => {
},
{
id: 'clock-r3',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
results: [],
scrambleSetCount: 2,
- linkedRounds: null,
participationRuleset: {
participationSource: {
type: 'linkedRounds',
@@ -227,12 +239,13 @@ describe('validateAdvancementConditions', () => {
expect(errors).toHaveLength(0);
});
- it('should accept registration participation rulesets for non-final rounds', () => {
+ it('should accept v2 sequential-round advancement', () => {
const event: Event = {
id: 'sq1',
rounds: [
{
id: 'sq1-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: {
centiseconds: 12000,
@@ -240,12 +253,10 @@ describe('validateAdvancementConditions', () => {
},
cutoff: {
numberOfAttempts: 2,
- attemptResult: 6000,
+ resultValue: 6000,
},
- advancementCondition: null,
results: [],
scrambleSetCount: 2,
- linkedRounds: null,
participationRuleset: {
participationSource: {
type: 'registrations',
@@ -256,10 +267,18 @@ describe('validateAdvancementConditions', () => {
},
{
id: 'sq1-r2',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: {
+ participationSource: {
+ type: 'round',
+ roundId: 'sq1-r1',
+ resultCondition: { type: 'ranking', scope: 'average', value: 12 },
+ },
+ reservedPlaces: null,
+ },
results: [],
scrambleSetCount: 1,
extensions: [],
@@ -281,10 +300,11 @@ describe('validateAdvancementConditions', () => {
rounds: [
{
id: '333-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: null,
results: [],
scrambleSetCount: 1,
extensions: [],
@@ -308,10 +328,11 @@ describe('validateRoundsHaveScheduleActivities', () => {
rounds: [
{
id: '333-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: null,
results: [],
scrambleSetCount: 1,
extensions: [],
@@ -364,10 +385,11 @@ describe('validateRoundsHaveScheduleActivities', () => {
rounds: [
{
id: '333-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: null,
results: [],
scrambleSetCount: 1,
extensions: [],
@@ -440,20 +462,22 @@ describe('validateEventRounds', () => {
rounds: [
{
id: '222-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: null,
results: [],
scrambleSetCount: 1,
extensions: [],
},
{
id: '222-r2',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: null,
results: [],
scrambleSetCount: 1,
extensions: [],
diff --git a/src/lib/wcif/validation/index.test.ts b/src/lib/wcif/validation/index.test.ts
index 5295696..20bbae0 100644
--- a/src/lib/wcif/validation/index.test.ts
+++ b/src/lib/wcif/validation/index.test.ts
@@ -10,7 +10,7 @@ import type { Competition } from '@wca/helpers';
import { describe, it, expect } from 'vitest';
const mockWcif: Competition = {
- formatVersion: '1.0',
+ formatVersion: '2.0',
id: 'TestComp2024',
name: 'Test Competition 2024',
shortName: 'Test 2024',
@@ -21,7 +21,7 @@ const mockWcif: Competition = {
numberOfDays: 1,
venues: [],
},
- series: [],
+ series: null,
competitorLimit: 100,
extensions: [],
registrationInfo: {
@@ -44,10 +44,11 @@ describe('validateWcif', () => {
rounds: [
{
id: '333-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: null,
results: [],
scrambleSetCount: 1,
extensions: [],
@@ -148,20 +149,22 @@ describe('validateWcif', () => {
rounds: [
{
id: '222-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null, // Missing!
+ participationRuleset: null, // Missing!
results: [],
scrambleSetCount: 1,
extensions: [],
},
{
id: '222-r2',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: null,
results: [],
scrambleSetCount: 1,
extensions: [],
@@ -267,10 +270,11 @@ describe('validateWcif', () => {
rounds: [
{
id: '333-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: null,
results: [],
scrambleSetCount: 1,
extensions: [],
@@ -377,10 +381,11 @@ describe('validateWcif', () => {
rounds: [
{
id: '333-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: null,
results: [],
scrambleSetCount: 1,
extensions: [],
diff --git a/src/lib/wcif/validation/personAssignmentValidation.test.ts b/src/lib/wcif/validation/personAssignmentValidation.test.ts
index 8fbba4b..ef6c445 100644
--- a/src/lib/wcif/validation/personAssignmentValidation.test.ts
+++ b/src/lib/wcif/validation/personAssignmentValidation.test.ts
@@ -11,7 +11,7 @@ import type { Competition, Person } from '@wca/helpers';
import { describe, it, expect } from 'vitest';
const mockWcif: Competition = {
- formatVersion: '1.0',
+ formatVersion: '2.0',
id: 'TestComp2024',
name: 'Test Competition 2024',
shortName: 'Test 2024',
@@ -69,7 +69,7 @@ const mockWcif: Competition = {
},
],
},
- series: [],
+ series: null,
competitorLimit: 100,
extensions: [],
registrationInfo: {
diff --git a/src/pages/Competition/Checks/FirstTimers.tsx b/src/pages/Competition/Checks/FirstTimers.tsx
index 5401917..2c416ac 100644
--- a/src/pages/Competition/Checks/FirstTimers.tsx
+++ b/src/pages/Competition/Checks/FirstTimers.tsx
@@ -1,6 +1,6 @@
import { getLocalStorage, setLocalStorage, searchPersons } from '../../../lib/api';
import { type WcaPersonSearchResult } from '../../../lib/api/types';
-import { acceptedRegistrations } from '../../../lib/domain/persons';
+import { acceptedRegistrations, hasRegistrantId } from '../../../lib/domain/persons';
import { useAppSelector } from '../../../store';
import FirstTimerCard from './FirstTimerCard';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
@@ -44,7 +44,9 @@ export default function FirstTimers() {
});
const [loading, setLoading] = useState(false);
- const firstTimers = acceptedRegistrations(wcif?.persons ?? []).filter((p) => !p.wcaId);
+ const firstTimers = acceptedRegistrations(wcif?.persons ?? [])
+ .filter(hasRegistrantId)
+ .filter((p) => !p.wcaId);
const incrementIndex = (i = 0) => {
if (i >= firstTimers.length) {
diff --git a/src/pages/Competition/Export/index.tsx b/src/pages/Competition/Export/index.tsx
index 72baad2..f529419 100644
--- a/src/pages/Competition/Export/index.tsx
+++ b/src/pages/Competition/Export/index.tsx
@@ -1,6 +1,6 @@
import { findGroupActivitiesByRound, parseActivityCode } from '../../../lib/domain/activities';
import { eventNameById, roundFormatShortById } from '../../../lib/domain/events';
-import { acceptedRegistrations } from '../../../lib/domain/persons';
+import { acceptedRegistrations, hasRegistrantId } from '../../../lib/domain/persons';
import { type ActivityWithParent } from '../../../lib/domain/types';
import {
getGroupifierActivityConfig,
@@ -22,7 +22,10 @@ import { download, generateCsv, mkConfig } from 'export-to-csv';
import { flatten } from 'lodash';
import { useCallback } from 'react';
import Grid from '@mui/material/GridLegacy';
-import { formatAdvancementCondition, getDisplayAdvancementConditionForRound } from '../../../lib/wcif/rounds';
+import {
+ formatAdvancementCondition,
+ getDisplayAdvancementConditionForRound,
+} from '../../../lib/wcif/rounds';
type AssignmentWithActivity = Assignment & { activity: ActivityWithParent };
@@ -136,6 +139,7 @@ const ExportPage = () => {
const headers = ['registrantId', 'name', 'wcaId', 'role', 'country_iso', ...assignmentHeaders];
const data = acceptedRegistrations(wcif.persons)
+ .filter(hasRegistrantId)
.sort((a, b) => a.name.localeCompare(b.name))
.map((person) => {
const assignments = assignmentsToObj(person);
@@ -237,7 +241,7 @@ const ExportPage = () => {
? `DNF if > ${formatCentiseconds(round.timeLimit.centiseconds)}`
: '',
cutoff_time: round.cutoff
- ? `1 or 2 < ${formatCentiseconds(round.cutoff.attemptResult)}`
+ ? `1 or 2 < ${formatCentiseconds(round.cutoff.resultValue)}`
: '',
round_format: roundFormatShortById(round.format),
advancement_condition: (() => {
@@ -253,11 +257,13 @@ const ExportPage = () => {
// Add scorecards group by group
groupAssignmentsByEventAndRound.forEach((groupActivity) => {
- const people = acceptedRegistrations(wcif.persons).filter((person) =>
- (person.assignments ?? []).some(
- (a) => a.activityId === groupActivity.id && a.assignmentCode === 'competitor'
- )
- );
+ const people = acceptedRegistrations(wcif.persons)
+ .filter(hasRegistrantId)
+ .filter((person) =>
+ (person.assignments ?? []).some(
+ (a) => a.activityId === groupActivity.id && a.assignmentCode === 'competitor'
+ )
+ );
const featuredCompetitors =
getGroupifierActivityConfig(groupActivity)?.featuredCompetitorWcaUserIds || [];
diff --git a/src/pages/Competition/Import/index.test.tsx b/src/pages/Competition/Import/index.test.tsx
index 6f5e01b..74fd005 100644
--- a/src/pages/Competition/Import/index.test.tsx
+++ b/src/pages/Competition/Import/index.test.tsx
@@ -44,10 +44,10 @@ describe('ImportPage', () => {
id: 'comp-123',
name: 'Test Competition',
shortName: 'Test Competition',
- formatVersion: '1.0',
+ formatVersion: '2.0',
competitorLimit: null,
extensions: [],
- series: [],
+ series: null,
persons: [],
events: [
{
@@ -73,7 +73,7 @@ describe('ImportPage', () => {
},
...overrides,
},
- } as unknown as AppState);
+ }) as unknown as AppState;
const uploadCsv = (
container: HTMLElement,
@@ -81,7 +81,9 @@ describe('ImportPage', () => {
) => {
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
fireEvent.change(fileInput, { target: { files: [new File(['csv'], 'assignments.csv')] } });
- fireEvent.submit(getByRole('button', { name: 'IMPORT CSV' }).closest('form') as HTMLFormElement);
+ fireEvent.submit(
+ getByRole('button', { name: 'IMPORT CSV' }).closest('form') as HTMLFormElement
+ );
};
it('tells users the CSV importer is limited to Round 1 assignments', () => {
@@ -93,9 +95,7 @@ describe('ImportPage', () => {
const { getByText } = renderWithProviders();
- expect(
- getByText(/This import flow only supports Round 1 assignments\./)
- ).toBeInTheDocument();
+ expect(getByText(/This import flow only supports Round 1 assignments\./)).toBeInTheDocument();
expect(setBreadcrumbsMock).toHaveBeenCalledWith([{ text: 'Import' }]);
});
@@ -109,7 +109,10 @@ describe('ImportPage', () => {
(
_csv: string,
options: {
- complete: (result: { meta: { fields: string[] }; data: Array> }) => void;
+ complete: (result: {
+ meta: { fields: string[] };
+ data: Array>;
+ }) => void;
}
) => {
options.complete({
@@ -122,9 +125,7 @@ describe('ImportPage', () => {
const { container, getByRole, getByText } = renderWithProviders();
uploadCsv(container, getByRole);
- expect(
- getByText(/This only generates and imports Round 1 assignments\./)
- ).toBeInTheDocument();
+ expect(getByText(/This only generates and imports Round 1 assignments\./)).toBeInTheDocument();
expect(getByText('1 people found')).toBeInTheDocument();
});
@@ -215,7 +216,10 @@ describe('ImportPage', () => {
(
_csv: string,
options: {
- complete: (result: { meta: { fields: string[] }; data: Array> }) => void;
+ complete: (result: {
+ meta: { fields: string[] };
+ data: Array>;
+ }) => void;
}
) => {
options.complete({
@@ -312,7 +316,10 @@ describe('ImportPage', () => {
(
_csv: string,
options: {
- complete: (result: { meta: { fields: string[] }; data: Array> }) => void;
+ complete: (result: {
+ meta: { fields: string[] };
+ data: Array>;
+ }) => void;
}
) => {
options.complete({
diff --git a/src/pages/Competition/Person/index.tsx b/src/pages/Competition/Person/index.tsx
index 7533eab..ef211c5 100644
--- a/src/pages/Competition/Person/index.tsx
+++ b/src/pages/Competition/Person/index.tsx
@@ -23,7 +23,7 @@ const PersonPage = () => {
const person =
registrantId &&
wcif &&
- wcif.persons.find((i) => i.registrantId.toString() === registrantId.toString());
+ wcif.persons.find((i) => i.registrantId?.toString() === registrantId.toString());
const [assignments, setAssignments] = useState<
(Assignment & {
diff --git a/src/pages/Competition/Rooms/Room.tsx b/src/pages/Competition/Rooms/Room.tsx
index 357dde0..c293a7e 100644
--- a/src/pages/Competition/Rooms/Room.tsx
+++ b/src/pages/Competition/Rooms/Room.tsx
@@ -164,17 +164,20 @@ const Room = ({ room }: RoomProps) => {
}
const participationSource = getParticipationRuleset(round)?.participationSource;
+ const resultCondition =
+ participationSource?.type === 'linkedRounds'
+ ? participationSource.resultCondition
+ : null;
const estimatedCompetitors =
participationSource?.type === 'registrations' || roundNumber === 1
? (eventRegistrationCounts[eventId] ?? 0)
- : participationSource?.type === 'linkedRounds' &&
- (participationSource.resultCondition.type === 'percent' ||
- participationSource.resultCondition.type === 'ranking')
+ : resultCondition?.value != null &&
+ (resultCondition.type === 'percent' || resultCondition.type === 'ranking')
? advancingCompetitors(
{
- type: participationSource.resultCondition.type,
- level: participationSource.resultCondition.value,
+ type: resultCondition.type,
+ level: resultCondition.value,
},
eventRegistrationCounts[eventId] ?? 0
)
diff --git a/src/pages/Competition/Round/hooks/useRoundData.test.tsx b/src/pages/Competition/Round/hooks/useRoundData.test.tsx
index 118db16..2117e48 100644
--- a/src/pages/Competition/Round/hooks/useRoundData.test.tsx
+++ b/src/pages/Competition/Round/hooks/useRoundData.test.tsx
@@ -1,4 +1,9 @@
-import { buildActivity, buildEvent, buildPerson, buildRound } from '../../../../store/reducers/_tests_/helpers';
+import {
+ buildActivity,
+ buildEvent,
+ buildPerson,
+ buildRound,
+} from '../../../../store/reducers/_tests_/helpers';
import INITIAL_STATE, { type AppState } from '../../../../store/initialState';
import reducer from '../../../../store/reducer';
import { useRoundData } from './useRoundData';
@@ -36,7 +41,7 @@ const buildWcif = (): Competition => ({
id: 'test-competition',
name: 'Test Competition',
shortName: 'Test',
- formatVersion: '1.0',
+ formatVersion: '2.0',
persons: [
buildPerson({
registrantId: 1,
@@ -102,11 +107,7 @@ const renderUseRoundData = () => {
wcif: buildWcif(),
changedKeys: new Set(),
};
- const store = createStore(
- reducer as Reducer,
- state,
- applyMiddleware(thunk)
- );
+ const store = createStore(reducer as Reducer, state, applyMiddleware(thunk));
return renderHook(() => useRoundData('333fm-r1', round), {
wrapper: ({ children }: { children: ReactNode }) => (
@@ -124,11 +125,15 @@ describe('useRoundData', () => {
expect(result.current.distributedAttemptGroups).toEqual([
{
attemptNumber: 1,
- activities: [expect.objectContaining({ id: 101, room: expect.objectContaining({ name: 'Room A' }) })],
+ activities: [
+ expect.objectContaining({ id: 101, room: expect.objectContaining({ name: 'Room A' }) }),
+ ],
},
{
attemptNumber: 2,
- activities: [expect.objectContaining({ id: 102, room: expect.objectContaining({ name: 'Room B' }) })],
+ activities: [
+ expect.objectContaining({ id: 102, room: expect.objectContaining({ name: 'Room B' }) }),
+ ],
},
]);
});
diff --git a/src/pages/Competition/Round/utils/dualRoundAssignments.ts b/src/pages/Competition/Round/utils/dualRoundAssignments.ts
index 9f6487f..1f42854 100644
--- a/src/pages/Competition/Round/utils/dualRoundAssignments.ts
+++ b/src/pages/Competition/Round/utils/dualRoundAssignments.ts
@@ -35,6 +35,11 @@ export const buildCopyRoundAssignments = (
let skippedCount = 0;
wcif.persons.forEach((person) => {
+ if (person.registrantId === null) {
+ return;
+ }
+ const registrantId = person.registrantId;
+
person.assignments?.forEach((assignment) => {
const sourceActivity = sourceGroupsById.get(+assignment.activityId);
@@ -52,7 +57,7 @@ export const buildCopyRoundAssignments = (
}
assignments.push({
- registrantId: person.registrantId,
+ registrantId,
assignment: {
...assignment,
activityId: targetActivity.id,
diff --git a/src/pages/Competition/ScramblerSchedule/index.tsx b/src/pages/Competition/ScramblerSchedule/index.tsx
index 970c35e..c27995a 100644
--- a/src/pages/Competition/ScramblerSchedule/index.tsx
+++ b/src/pages/Competition/ScramblerSchedule/index.tsx
@@ -69,16 +69,20 @@ export default function ScramblerSchedule() {
const assignments = useMemo(
() =>
- flatMap(wcif?.persons, (person) =>
- (person.assignments || [])
+ flatMap(wcif?.persons, (person) => {
+ if (person.registrantId === null) {
+ return [];
+ }
+ const registrantId = person.registrantId;
+ return (person.assignments || [])
.filter((assignment) => assignment.assignmentCode === 'staff-scrambler')
.map((assignment) => ({
...assignment,
name: person.name,
- registrantId: person.registrantId,
+ registrantId,
activity: getActivity(assignment),
- }))
- ),
+ }));
+ }),
[getActivity, wcif?.persons]
);
diff --git a/src/pages/Competition/Staff/index.tsx b/src/pages/Competition/Staff/index.tsx
index 08da218..aec6ee2 100644
--- a/src/pages/Competition/Staff/index.tsx
+++ b/src/pages/Competition/Staff/index.tsx
@@ -213,7 +213,11 @@ const Staff = () => {
disabled={!acceptedRegistration(person)}
color="primary"
checked={(person.roles ?? []).includes(role.id)}
- onChange={(e) => handleChange(e, person.registrantId, role.id)}
+ onChange={(e) => {
+ if (person.registrantId !== null) {
+ handleChange(e, person.registrantId, role.id);
+ }
+ }}
/>
))}
diff --git a/src/store/initialState.ts b/src/store/initialState.ts
index 1022f7a..1d0f13e 100644
--- a/src/store/initialState.ts
+++ b/src/store/initialState.ts
@@ -52,7 +52,7 @@ const INITIAL_STATE: AppState = {
startDate: '',
numberOfDays: 0,
},
- series: [],
+ series: null,
extensions: [],
competitorLimit: 0,
registrationInfo: {
diff --git a/src/store/reducers/_tests_/helpers.ts b/src/store/reducers/_tests_/helpers.ts
index 9b9346a..9532659 100644
--- a/src/store/reducers/_tests_/helpers.ts
+++ b/src/store/reducers/_tests_/helpers.ts
@@ -1,8 +1,9 @@
import INITIAL_STATE, { type AppState } from '../../initialState';
+import type { PersonWithRegistrantId } from '../../../lib/domain/persons';
import type { Activity, Competition, Event, Person, Round } from '@wca/helpers';
type ActivityOverrides = Partial;
-type PersonOverrides = Partial;
+type PersonOverrides = Partial;
type RoundOverrides = Partial;
type EventOverrides = Partial;
@@ -17,7 +18,7 @@ export const buildActivity = (overrides: ActivityOverrides = {}): Activity => ({
...overrides,
});
-export const buildPerson = (overrides: PersonOverrides = {}): Person => ({
+export const buildPerson = (overrides: PersonOverrides = {}): PersonWithRegistrantId => ({
name: 'Test Person',
registrantId: 1,
wcaUserId: 1,
@@ -42,7 +43,7 @@ export const buildPerson = (overrides: PersonOverrides = {}): Person => ({
});
export const buildWcif = (activities: Activity[], persons: Person[] = []): Competition => ({
- formatVersion: 'v1',
+ formatVersion: '2.0',
name: 'Test Competition',
shortName: 'test',
id: 'test-comp',
@@ -74,7 +75,7 @@ export const buildWcif = (activities: Activity[], persons: Person[] = []): Compe
},
extensions: [],
competitorLimit: null,
- series: [],
+ series: null,
registrationInfo: {
openTime: '2024-01-01T00:00:00Z',
closeTime: '2024-01-02T00:00:00Z',
@@ -87,10 +88,11 @@ export const buildWcif = (activities: Activity[], persons: Person[] = []): Compe
export const buildRound = (overrides: RoundOverrides = {}): Round => ({
id: '333-r1',
+ linkedRounds: null,
format: 'a',
timeLimit: null,
cutoff: null,
- advancementCondition: null,
+ participationRuleset: null,
results: [],
scrambleSetCount: 1,
extensions: [],
diff --git a/src/store/reducers/competitorAssignments.ts b/src/store/reducers/competitorAssignments.ts
index 3d4e36a..601573c 100644
--- a/src/store/reducers/competitorAssignments.ts
+++ b/src/store/reducers/competitorAssignments.ts
@@ -232,7 +232,9 @@ export const bulkRemovePersonAssignments = (
const isBeingRemoved = filtersApplicable.length > 0;
if (isBeingRemoved) {
- affectedRegistrantIds.add(person.registrantId);
+ if (person.registrantId !== null) {
+ affectedRegistrantIds.add(person.registrantId);
+ }
}
// At least 1 filter is filtering them out
@@ -272,7 +274,9 @@ export const bulkUpsertPersonAssignments = (
}));
if (personAssignments.length > 0) {
- affectedRegistrantIds.add(person.registrantId);
+ if (person.registrantId !== null) {
+ affectedRegistrantIds.add(person.registrantId);
+ }
return upsertAssignmentsOnPerson(person, personAssignments);
}
diff --git a/src/store/selectors.ts b/src/store/selectors.ts
index b571007..ba6bfc7 100644
--- a/src/store/selectors.ts
+++ b/src/store/selectors.ts
@@ -4,9 +4,14 @@ import {
parseActivityCode,
findRooms,
} from '../lib/domain/activities';
-import { acceptedRegistrations, personsShouldBeInRound } from '../lib/domain/persons';
+import {
+ acceptedRegistrations,
+ hasRegistrantId,
+ personsShouldBeInRound,
+ type PersonWithRegistrantId,
+} from '../lib/domain/persons';
import { type AppState } from './initialState';
-import { type Activity, type Event, type Person, type Round, type Room } from '@wca/helpers';
+import { type Activity, type Event, type Round, type Room } from '@wca/helpers';
import { createSelector } from 'reselect';
const selectWcif = (state: AppState) => state.wcif;
@@ -18,8 +23,8 @@ export const selectWcifRooms = createSelector(selectWcif, (wcif): Room[] =>
/**
* Return a filtered array of all persons who's registration is defined and status is `accepted`
*/
-export const selectAcceptedPersons = createSelector(selectWcif, (wcif): Person[] =>
- acceptedRegistrations(wcif?.persons || [])
+export const selectAcceptedPersons = createSelector(selectWcif, (wcif): PersonWithRegistrantId[] =>
+ acceptedRegistrations(wcif?.persons || []).filter(hasRegistrantId)
);
/**
@@ -68,7 +73,7 @@ export const selectActivityById = createSelector(
export const selectPersonsAssignedForRound = createSelector(
[selectWcif, selectAcceptedPersons, (_: AppState, roundId: string) => roundId],
- (wcif, acceptedPersons, roundId: string): Person[] => {
+ (wcif, acceptedPersons, roundId: string): PersonWithRegistrantId[] => {
return acceptedPersons.filter((p) =>
p.assignments?.find((a) => {
const activity = wcif ? findActivityById(wcif, a.activityId) : null;
@@ -86,7 +91,7 @@ export const selectPersonsAssignedForRound = createSelector(
export const selectPersonsHavingCompetitorAssignmentsForRound = createSelector(
[selectWcif, selectAcceptedPersons, (_: AppState, roundId: string) => roundId],
- (wcif, acceptedPersons, roundId: string): Person[] => {
+ (wcif, acceptedPersons, roundId: string): PersonWithRegistrantId[] => {
return acceptedPersons.filter((p) =>
p.assignments?.find((a) => {
const activity = wcif ? findActivityById(wcif, a.activityId) : null;
@@ -107,8 +112,8 @@ export const selectPersonsHavingCompetitorAssignmentsForRound = createSelector(
export const selectPersonsShouldBeInRound = createSelector(
[selectAcceptedPersons],
(acceptedPersons) =>
- (round: Round): Person[] =>
- personsShouldBeInRound(round)(acceptedPersons)
+ (round: Round): PersonWithRegistrantId[] =>
+ personsShouldBeInRound(round)(acceptedPersons).filter(hasRegistrantId)
);
/**
@@ -116,7 +121,7 @@ export const selectPersonsShouldBeInRound = createSelector(
*/
export const selectPersonsAssignedToActivitiyId = createSelector(
[selectAcceptedPersons, (_: AppState, activityId: number) => activityId],
- (persons, activityId): Person[] =>
+ (persons, activityId): PersonWithRegistrantId[] =>
persons.filter(({ assignments }) => assignments?.some((a) => a.activityId === activityId))
);
@@ -125,7 +130,7 @@ export const selectPersonsAssignedToActivitiyId = createSelector(
*/
export const selectFirstTimers = createSelector(
selectAcceptedPersons,
- (acceptedPersons): Person[] => acceptedPersons.filter((p) => !p.wcaId)
+ (acceptedPersons): PersonWithRegistrantId[] => acceptedPersons.filter((p) => !p.wcaId)
);
/**
diff --git a/yarn.lock b/yarn.lock
index 9bc5b22..f63deea 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2914,15 +2914,10 @@
"@vitest/pretty-format" "4.0.18"
tinyrainbow "^3.0.3"
-"@wca/helpers@^1.1.5":
- version "1.1.5"
- resolved "https://registry.npmjs.org/@wca/helpers/-/helpers-1.1.5.tgz"
- integrity sha512-Sr/XxkEXctrRKFJuqyAy9KH7q//YevEo+zSCESpVQqieB4Y3KjHYOl54kPaocwAHkWniGMOwZWD6O1Q9MR1d3w==
-
-"@wca/helpers@^1.1.6":
- version "1.1.6"
- resolved "https://registry.yarnpkg.com/@wca/helpers/-/helpers-1.1.6.tgz#9157225955df954cd4cd0ab1e7fdad24cd39d0fb"
- integrity sha512-Y5zsRpQ6Bt/aOuqm1dbkb1awYmR3D5hrHHLXYmWLiJzfb8wo4fxyKLIEnK4LrfjWpO+rmictG99cWORhaby5Lg==
+"@wca/helpers@2.0.0":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@wca/helpers/-/helpers-2.0.0.tgz#ace1af3446b552ef54f3a83cc35cc05a74c04ea9"
+ integrity sha512-VYcby20Y9Uf7FWSxvzHbJRp6eOW7Gb4z7Nvy+8ggjxIKqfndAewxysiYop2LZ6VXnJavtV4SPy2AFhAH+OT4Bg==
acorn-jsx@^5.3.2:
version "5.3.2"
@@ -6802,11 +6797,6 @@ ts-api-utils@^2.4.0:
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.4.0.tgz#2690579f96d2790253bdcf1ca35d569ad78f9ad8"
integrity sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==
-tsc@^2.0.4:
- version "2.0.4"
- resolved "https://registry.npmjs.org/tsc/-/tsc-2.0.4.tgz"
- integrity sha512-fzoSieZI5KKJVBYGvwbVZs/J5za84f2lSTLPYf6AGiIf43tZ3GNrI1QzTLcjtyDDP4aLxd46RTZq1nQxe7+k5Q==
-
tsconfck@^3.0.3:
version "3.1.6"
resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.6.tgz#da1f0b10d82237ac23422374b3fce1edb23c3ead"
@@ -6930,11 +6920,6 @@ typescript-eslint@^8.53.1:
"@typescript-eslint/typescript-estree" "8.53.1"
"@typescript-eslint/utils" "8.53.1"
-typescript@^5.0.4:
- version "5.2.2"
- resolved "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz"
- integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
-
typescript@^5.9.3:
version "5.9.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
@@ -7133,15 +7118,6 @@ w3c-xmlserializer@^5.0.0:
dependencies:
xml-name-validator "^5.0.0"
-wca-group-generators@^0.1.8:
- version "0.1.8"
- resolved "https://registry.yarnpkg.com/wca-group-generators/-/wca-group-generators-0.1.8.tgz#a23ab29a5b5d62a6d5d0f538e301d65d56be79ee"
- integrity sha512-ZRIepwTKg6Egsoqq9EK1FTqpJ6x2NlCigsK7YUL1kbQbG1ry0AmmxxQ0fPJYYE4pWqjj23vstBHRuf3W6z7h8A==
- dependencies:
- "@wca/helpers" "^1.1.5"
- tsc "^2.0.4"
- typescript "^5.0.4"
-
web-vitals@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-5.1.0.tgz#2f117e92c8c4eeb107cb163cbb482ac20d685ebd"