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"