Skip to content
26 changes: 25 additions & 1 deletion client/src/components/admin/AdminRound/AdminRoundContent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,23 @@ function setStoredBatchResults(roundId, results) {
}
}

/**
* Combine results and batchResults to produce a temporary results-like array
* that has all batchResults changes applied. This allows us to display a
* warning if a result is being entered which matches a batched result exactly.
*/
function combineResultsAndBatchResults(results, batchResults) {
const combinedResults = [...results];
batchResults.forEach((batchedResult) => {
const i = combinedResults.findIndex((res) => res.id === batchedResult.id);
combinedResults[i] = {
...combinedResults[i],
attempts: batchedResult.attempts,
};
});
return combinedResults;
Comment on lines +85 to +93
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw. you can also write it like this:

Suggested change
const combinedResults = [...results];
batchResults.forEach((batchedResult) => {
const i = combinedResults.findIndex((res) => res.id === batchedResult.id);
combinedResults[i] = {
...combinedResults[i],
attempts: batchedResult.attempts,
};
});
return combinedResults;
return results.map((result) => {
const batchResult = batchResults.find((batchResult) => batchResult.id === result.id);
if (batchResult) {
return { ...result, attempts: batchResult.attempts };
} else {
return result;
}
);

I consider it slightly more direct in terms of readability, though either version is fine :)

}

function AdminRoundContent({ round, competitionId, officialWorldRecords }) {
const confirm = useConfirm();
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
Expand All @@ -91,6 +108,9 @@ function AdminRoundContent({ round, competitionId, officialWorldRecords }) {
const [isBatchMode, setIsBatchMode] = useState(
() => batchResults.length > 0 || getStoreIsBatchMode(),
);
const [combinedResults, setCombinedResults] = useState(() =>
combineResultsAndBatchResults(round.results, batchResults),
);
const formContainerRef = useRef(null);

const [enterResults, { loading }] = useMutation(ENTER_RESULTS, {
Expand Down Expand Up @@ -184,7 +204,10 @@ function AdminRoundContent({ round, competitionId, officialWorldRecords }) {

useEffect(() => {
setStoredBatchResults(round.id, batchResults);
}, [round.id, batchResults]);
setCombinedResults(
combineResultsAndBatchResults(round.results, batchResults),
);
}, [round, batchResults]);

return (
<>
Expand All @@ -193,6 +216,7 @@ function AdminRoundContent({ round, competitionId, officialWorldRecords }) {
<ResultAttemptsForm
result={editedResult}
results={round.results}
combinedResults={combinedResults}
onResultChange={setEditedResult}
eventId={round.competitionEvent.event.id}
format={round.format}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
function ResultAttemptsForm({
result,
results,
combinedResults, // combined results and batchResults array
onResultChange,
eventId,
format,
Expand Down Expand Up @@ -91,15 +92,23 @@ function ResultAttemptsForm({
const attempts = trimTrailingSkipped(attemptResults).map((result) => ({
result,
}));
onSubmit(attempts);
onSubmit(attempts, result.person);
});
}

function confirmSubmission() {
// We don't want to show a duplicate warning if the user is submitting
// already-entered results for the correct competitor. This is often used
// as a quick way to refresh data without refreshing the whole page.
const resultsToCheck = combinedResults.filter(
(res) => res.id !== result.id,
);
Comment on lines +100 to +105
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would compute combinedResults together with the filter here.

Computing them upfront, as in the current version, doesn't have any benefit, because it would be recomputed after every submission anyway.


const submissionWarning = attemptResultsWarning(
attemptResults,
eventId,
officialWorldRecords,
resultsToCheck,
);

if (submissionWarning) {
Expand Down
38 changes: 38 additions & 0 deletions client/src/lib/attempt-result.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ export function attemptResultsWarning(
attemptResults,
eventId,
officialWorldRecords = [],
results = [],
) {
const skippedGapIndex =
trimTrailingSkipped(attemptResults).indexOf(SKIPPED_VALUE);
Expand Down Expand Up @@ -482,6 +483,22 @@ export function attemptResultsWarning(
};
}
}

// Check whether this result is a duplicate of existing results in the same round.
// Excludes FMC and all-DNF results since ties are common.
if (eventId !== "333fm" && completeAttempts.length > 0) {
const matches = findAllMatchingResults(attemptResults, results);
if (matches.length > 0) {
const matchesString = matches
.map((match) => `${match.person.name} (${match.person.id})`)
.join(", ");
return {
description: `The result you're trying to submit matches all results for
the following competitor${matches.length > 1 ? "s" : ""}: ${matchesString}.
Please check that the results are accurate.`,
};
}
}
}
return null;
}
Expand Down Expand Up @@ -545,3 +562,24 @@ function checkForDnsFollowedByValidResult(attemptResults) {
index > dnsIndex && attempt !== SKIPPED_VALUE && attempt !== DNS_VALUE,
);
}

/**
* Check whether an attempt matches an existing attempt exactly.
*/
function findAllMatchingResults(attemptResults, results) {
const filteredAttemptResults = trimTrailingSkipped(attemptResults);

const matches = results.filter((result) => {
if (result.attempts.length !== filteredAttemptResults.length) {
return false;
}
for (let i = 0; i < result.attempts.length; i++) {
if (result.attempts[i].result !== filteredAttemptResults[i]) {
return false;
}
}
return true;
});

return matches;
}
55 changes: 55 additions & 0 deletions client/src/lib/tests/attempt-result.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,61 @@ describe("attemptResultsWarning", () => {
attemptResultsWarning(attemptResults, "333fm", worldRecords),
).toEqual(null);
});

it("complains about exact duplicate results", () => {
const attemptResults = [500, 600, 700, 800, 900];
const existingResults = [
{
attempts: [
{ result: 500 },
{ result: 600 },
{ result: 700 },
{ result: 800 },
{ result: 900 },
],
person: { id: 2, name: "Person 2" },
},
];
expect(
attemptResultsWarning(attemptResults, "333", [], existingResults),
).toMatchObject({
description: `The result you're trying to submit matches all results for
the following competitor: Person 2 (2).
Please check that the results are accurate.`,
});
});

it("does not check for duplicates in FMC", () => {
const attemptResults = [25, 26];
const existingResults = [
{
attempts: [{ result: 25 }, { result: 26 }],
person: { id: 2, name: "Person 2" },
},
];
expect(
attemptResultsWarning(attemptResults, "333fm", [], existingResults),
).toEqual(null);
});

it("does not warn about all-DNF duplicates", () => {
const attemptResults = [-1, -1, -1, -1, -1];
const existingResults = [
{
attempts: [
{ result: -1 },
{ result: -1 },
{ result: -1 },
{ result: -1 },
{ result: -1 },
],
person: { id: 2, name: "Person 2" },
},
];
expect(
attemptResultsWarning(attemptResults, "333", [], existingResults),
).toEqual(null);
});
});

describe("applyTimeLimit", () => {
Expand Down