Skip to content

CC-2: Improve Pairing Workflow#322

Merged
ianpaschal merged 11 commits into
mainfrom
ian/cc-2-improve-pairing-workflow
May 26, 2026
Merged

CC-2: Improve Pairing Workflow#322
ianpaschal merged 11 commits into
mainfrom
ian/cc-2-improve-pairing-workflow

Conversation

@ianpaschal
Copy link
Copy Markdown
Owner

@ianpaschal ianpaschal commented May 8, 2026

Fixes CC-2: Improve Pairing Workflow

Summary by CodeRabbit

  • New Features

    • Added drag-and-drop pairing configuration interface for manual tournament pairings
    • Unified competitors view combining team rosters and player rankings with round selection
    • Explicit competitor selection for draft pairing generation
    • Enhanced tournament action menu and header layout
  • Improvements

    • Dynamic table count calculation based on competitor count in pairing configuration
    • Streamlined competitor identity component and faction indicator display
  • Testing

    • Added comprehensive end-to-end test suite with Playwright
    • Introduced CI/CD workflow for automated testing

Review Change Stack

@vercel
Copy link
Copy Markdown

vercel Bot commented May 8, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
combat-command Ready Ready Preview, Comment May 26, 2026 5:29pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 88d08ab1-f77e-40da-9ae3-4374ef77e357

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Local pairing config/schema/defaults with tableCount were added, plus a migration to backfill it. Database queries switched to the by_tournament index. A getLastVisibleRound helper with tests was implemented and integrated in deepenTournament. Draft pairings now accept include lists, and a mutation updates pairing config. UI infrastructure moved to an actions prop, identity components were refactored, and rankings/roster were replaced with unified competitors/registrations tables. The pairings page was overhauled with a drag-and-drop grid and new dialogs.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ian/cc-2-improve-pairing-workflow

@ianpaschal ianpaschal changed the title WIP CC-2: Improve Pairing Workflow May 8, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 21

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
src/components/PageWrapper/PageWrapper.tsx (1)

57-68: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

actions can be dropped when no title/back button is shown.

The header container is currently not rendered unless back button or visible title is present, so actions alone will never render.

💡 Suggested fix
-          {(showBackButton || (title && !hideTitle)) && (
+          {(showBackButton || (title && !hideTitle) || actions != null) && (
             <div className={styles.PageWrapper_Header}>
               {showBackButton && (
                 <Button icon={<ArrowLeft />} variant="shaded" onClick={handleClickBack} />
               )}
               {title && (
                 <h1>{title}</h1>
               )}
-              <div className={styles.PageWrapper_Header_Right}>
-                {actions}
-              </div>
+              {actions != null && (
+                <div className={styles.PageWrapper_Header_Right}>
+                  {actions}
+                </div>
+              )}
             </div>
           )}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/PageWrapper/PageWrapper.tsx` around lines 57 - 68, The header
currently only renders when showBackButton or a visible title exists, so actions
passed to PageWrapper are never shown alone; update the render condition around
the div with class PageWrapper_Header to also check for actions (i.e., render
when showBackButton || (title && !hideTitle) || actions), keeping the internal
structure (Button using ArrowLeft and handleClickBack, the h1 for title, and the
PageWrapper_Header_Right wrapper for actions) unchanged so actions can display
even when no back button or title is present.
convex/_model/utils/deleteTestTournament.ts (1)

21-39: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use Promise.all instead of forEach to ensure deletions complete before mutation returns.

At lines 21, 29, and 37, forEach(async ...) does not await the async callbacks—the mutation may return before all deletions finish. Replace with Promise.all() paired with map():

Proposed fix
-  competitors.forEach(async ({ _id }) => {
-    await ctx.db.delete(_id);
-  });
+  await Promise.all(competitors.map(({ _id }) => ctx.db.delete(_id)));

-  pairings.forEach(async ({ _id }) => {
-    await ctx.db.delete(_id);
-  });
+  await Promise.all(pairings.map(({ _id }) => ctx.db.delete(_id)));

-  matchResults.forEach(async ({ _id }) => {
-    await ctx.db.delete(_id);
-  });
+  await Promise.all(matchResults.map(({ _id }) => ctx.db.delete(_id)));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@convex/_model/utils/deleteTestTournament.ts` around lines 21 - 39, The code
uses forEach with async callbacks for deleting competitors, pairings, and
matchResults so deletions may not complete before the mutation returns; change
each block to build an array of deletion promises (e.g., using competitors.map(c
=> ctx.db.delete(c._id))) and await Promise.all(...) for the competitors,
pairings, and matchResults collections so ctx.db.delete is awaited before the
function returns; update the blocks that reference competitors, pairings,
matchResults and ctx.db.delete accordingly (in the deleteTestTournament logic).
convex/_model/tournaments/_helpers/deepenTournament.ts (1)

22-29: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

JSDoc @param name is stale.

The parameter was renamed to doc, but the JSDoc still references tournament. Worth syncing so editor tooltips don't mislead.

♻️ Proposed refactor
  * `@param` ctx - Convex query context
- * `@param` tournament - Raw Tournament document
+ * `@param` doc - Raw Tournament document
  * `@returns` A deep Tournament
  */
 export const deepenTournament = async (
   ctx: QueryCtx,
   doc: Doc<'tournaments'>,
 ) => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@convex/_model/tournaments/_helpers/deepenTournament.ts` around lines 22 - 29,
Update the JSDoc for deepenTournament to match the current parameter names:
change the `@param` that currently says "tournament" to "doc" and adjust its
description to "Raw Tournament document" (or similar) so the JSDoc matches the
function signature deepenTournament(ctx: QueryCtx, doc: Doc<'tournaments'>) and
editor tooltips show correct info.
src/components/TournamentCompetitorIdentity/TournamentCompetitorIdentity.tsx (1)

31-46: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Disable the control when it has no actionable subject.

Right now this still renders as an enabled <button> when subject is missing, onClick is absent, or the row is loading. handleClick becomes a no-op, but the control still advertises itself as interactive. Derive both disabled and data-clickable from a single actionable flag instead.

Suggested fix
   const { displayName } = subject ?? placeholder ?? { displayName: 'Unknown Competitor', details: { alignments: [], factions: [] } };
+  const isActionable = !loading && !!subject && !!onClick && !disabled;
   const handleClick = (e: MouseEvent): void => {
     e.preventDefault(); // Prevent submit if in a form
-    if (subject && onClick) {
+    if (isActionable && subject && onClick) {
       onClick(subject._id);
     }
   };
@@
-      disabled={disabled}
-      data-clickable={!!onClick}
+      disabled={!isActionable}
+      data-clickable={isActionable}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/TournamentCompetitorIdentity/TournamentCompetitorIdentity.tsx`
around lines 31 - 46, Compute a single "actionable" flag (e.g. const actionable
= Boolean(subject && onClick && !loading)) and use it to drive interactivity:
set the button disabled={!actionable}, set data-clickable={actionable}, and
update handleClick to return immediately unless actionable before calling
onClick(subject._id); reference handleClick, subject, onClick, the disabled prop
and data-clickable attribute and include any existing loading/isLoading prop in
the actionable check.
src/components/TournamentPairingGenerationForm/components/TournamentPairingConfigForm/TournamentPairingConfigForm.tsx (1)

48-57: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Submit handler bypasses validation and double-fires onSubmit.

When validation succeeds, onSubmit is invoked twice — first with the validated payload, then again unconditionally with the raw form data merged with forcedValues, overwriting the validated result and re-triggering any downstream side-effects. When validation fails, validateForm populates field errors but the unconditional second call still submits the unvalidated data, defeating the schema entirely.

The unconditional block needs to be inside the success branch and should be the only onSubmit call, with forcedValues merged into the validated payload.

🐛 Proposed fix
   const handleSubmit: SubmitHandler<TournamentPairingConfig> = async (formData): Promise<void> => {
     const validFormData = validateForm(tournamentPairingConfigSchema, formData, form.setError);
-    if (validFormData) {
-      onSubmit?.(validFormData);
-    }
-    onSubmit?.({
-      ...formData,
-      ...forcedValues,
-    });
+    if (!validFormData) {
+      return;
+    }
+    onSubmit?.({
+      ...validFormData,
+      ...forcedValues,
+    });
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/components/TournamentPairingGenerationForm/components/TournamentPairingConfigForm/TournamentPairingConfigForm.tsx`
around lines 48 - 57, The submit handler handleSubmit currently calls onSubmit
twice and bypasses validation; change it so that after calling
validateForm(tournamentPairingConfigSchema, formData, form.setError) you only
call onSubmit when validFormData is truthy, and call it once with the validated
payload merged with forcedValues (e.g., onSubmit?.({ ...validFormData,
...forcedValues })); ensure there is no unconditional onSubmit outside that
success branch so invalid submissions do not fire.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@convex/_fixtures/createMockTournament.ts`:
- Around line 51-54: The tableCount calculation in pairingConfig incorrectly
applies the division only to the fallback due to operator precedence; change the
expression so the nullish coalescing picks the competitor count first and then
divide by 2 (e.g. compute Math.ceil((overrides?.maxCompetitors ?? 48) / 2)), and
remove the redundant trailing ?? 1 since Math.ceil always returns a number;
update the expression in the pairingConfig block that references
tournamentPairingConfigDefaultValues and tableCount accordingly.

In `@convex/_model/tournamentPairings/_helpers/assignTables.ts`:
- Line 23: The code uses a non-null assertion on
data.tournament.pairingConfig.tableCount which will break tournaments not yet
migrated; replace the assertion with a runtime-safe fallback (e.g., use optional
chaining and nullish coalescing: data.tournament.pairingConfig?.tableCount ??
<fallback>) and pick an appropriate fallback (a constant DEFAULT_TABLE_COUNT, a
value derived from data.tournament/player count, or a domain-specific safe
default) inside the assignTables logic so pair generation still works until
migration is complete; remove the "!" on tableCount and reference the
pairingConfig?.tableCount and the chosen DEFAULT_TABLE_COUNT or computed
fallback in the same function.

In `@convex/_model/tournaments/_helpers/getLastVisibleRound.ts`:
- Around line 25-27: In getLastVisibleRound, avoid the truthy check on
doc.lastRound; change the condition "if (doc.lastRound && doc.lastRound + 1 ===
doc.roundCount)" to explicitly check for undefined (e.g., "if (doc.lastRound !==
undefined && doc.lastRound + 1 === doc.roundCount)") so a lastRound of 0 is
handled correctly and still returns Math.max(doc.lastRound - 1, 0); leave the
fallback return doc.lastRound as-is.

In `@convex/migrations.ts`:
- Around line 145-147: The guard that skips backfilling uses a truthy check on
doc.pairingConfig.tableCount which will treat 0 as missing; change the
conditional to explicitly check for undefined or null (e.g., use !== undefined
or != null) so only truly missing values are backfilled; locate the check that
reads "if (doc.pairingConfig.tableCount) { return; }" and replace it with an
explicit undefined/null check on doc.pairingConfig.tableCount.

In `@src/components/FactionIndicator/FactionIndicator.tsx`:
- Around line 25-28: primaryAlignment can become undefined when alignment ===
[], which breaks getAlignmentColor (expects Alignment | null). Update the
primaryAlignment computation (the variable used by getAlignmentColor and
getAlignmentDisplayName) to explicitly guard empty arrays and normalize to null
instead of undefined (e.g., when Array.isArray(alignment) use alignment.length ?
alignment[0] : null, and when alignment is a scalar use alignment ?? null) so
getAlignmentColor(primaryAlignment) always receives Alignment | null and
displayName fallback logic continues to work.

In `@src/components/FactionIndicator/FactionIndicator.utils.ts`:
- Around line 6-11: The function getAlignmentColor treats undefined as a valid
alignment and falls through to return 'mixed'; update the null check to treat
both null and undefined as "unknown" by changing the guard (alignment === null)
to a check that catches both (e.g., alignment == null or alignment === null ||
alignment === undefined) so missing alignment values return 'gray' instead of
'mixed'; locate this change inside getAlignmentColor in
FactionIndicator.utils.ts.

In
`@src/components/TournamentPairingGenerationForm/TournamentPairingGenerationForm.tsx`:
- Around line 78-80: The expression that computes showLoading is overly verbose:
it uses [loading].some((l) => !!l) to check a single flag; simplify by replacing
that array/some pattern with a direct boolean cast (!!loading) so showLoading
becomes const showLoading = !tournament || !!loading, keeping the existing
variables tournament and loading (and preserving behavior).
- Around line 65-76: handleSubmit currently bypasses validation and sends raw
form data; re-enable the commented validation so the pairing config is validated
against tournamentPairingConfigSchema before calling onSubmit. Use
validateForm(tournamentPairingConfigSchema, formData, form.setError) and
validateForm(tournamentPairingConfigSchema, formData.config, form.setError) (as
previously attempted) and only invoke onSubmit?.({...formData, ...forcedValues})
when validation returns a truthy valid object; ensure you propagate
form.setError for field errors and keep the merge with forcedValues intact,
preserving the SubmitHandler signature in handleSubmit and the
GenerateDraftTournamentPairingsArgs type.
- Around line 46-58: The form's defaultValues for tournamentId is captured once
by useForm in TournamentPairingGenerationForm and can remain '' if tournament
loads after mount; fix by adding an effect that watches tournament?._id and
calls form.reset(...) to merge the same defaults (config:
tournamentPairingConfigDefaultValues, tournamentId: tournament?._id ?? '',
round: 0, include: [], plus existingValues and forcedValues) so the live form
state is updated when tournament._id becomes available (alternatively, gate the
component render on tournament truthiness so useForm only mounts after
tournament is loaded—choose one approach and implement it in
TournamentPairingGenerationForm, referencing useForm and form.reset).

In
`@src/components/TournamentRegistrationIdentity/TournamentRegistrationIdentity.tsx`:
- Line 29: The fallback object used when destructuring displayName in the
TournamentRegistrationIdentity component contains an unused details property;
update the expression that assigns const { displayName } = subject ??
placeholder ?? { displayName: 'Unknown Competitor' } by removing the redundant
details: { alignments: [], factions: [] } so the fallback only provides
displayName, keeping the rest of the component logic unchanged.
- Line 51: Update the Avatar URL access in TournamentRegistrationIdentity to use
defensive optional chaining: change occurrences of subject?.user.avatarUrl to
subject?.user?.avatarUrl so the leaf component still guards against a missing
user; reference the TournamentRegistrationIdentity component and the
deepenTournamentRegistration helper in the message for context.

In `@src/hooks/useAsyncState.ts`:
- Line 14: Add a one-line comment above the initialized ref declaration to
explain that useRef(asyncValue !== undefined) runs only on the first render and
is intentionally used to record whether an initial synchronous asyncValue was
provided (so the effect won't reapply it); reference the initialized constant
and the asyncValue parameter/name in your comment to make the intention clear to
future readers.
- Around line 14-20: The hook useAsyncState now only initializes value once
using the initialized ref and then ignores further asyncValue updates, which can
cause stale config in callers like TournamentPairingsPage (pairingConfig →
config); update the implementation or documentation: either restore
resync-on-asyncValue-change by removing the initialized gate and allowing
setValue(asyncValue) whenever asyncValue changes, or add explicit JSDoc on
useAsyncState explaining the once-only initialization semantics (mention
initialized, asyncValue, value, setValue) so callers know local edits win and
external updates are ignored.

In
`@src/pages/TournamentDetailPage/components/TournamentCompetitorsCard/TournamentCompetitorsCard.tsx`:
- Around line 25-34: The component currently calls
useGetTournamentCompetitorsByTournament and
useGetTournamentRegistrationsByTournament in TournamentCompetitorsCard while
TournamentCompetitorTable and TournamentRegistrationTable also re-run the same
queries; pick one owner: either lift data up or delegate to child. To fix,
remove the duplicate hook calls from the child components if you decide the card
should own data, and pass competitors, competitorsLoading, registrations,
registrationsLoading (or a single active view's rows/loading) into
TournamentCompetitorTable and TournamentRegistrationTable via props;
alternatively remove the hooks from TournamentCompetitorsCard and derive
showLoadingState/showEmptyState from the active child’s returned rows/loading so
only the active table issues the query. Ensure you update prop names used by
TournamentCompetitorTable and TournamentRegistrationTable to accept the passed
rows and loading values.

In
`@src/pages/TournamentDetailPage/components/TournamentCompetitorsCard/TournamentCompetitorsCard.utils.tsx`:
- Around line 26-101: Remove the large commented-out implementation of
getTableConfig in TournamentCompetitorsCard.utils.tsx: delete the entire
commented block that begins with the commented export const getTableConfig and
ends at its closing comment so the file only exposes the active exported
types/APIs; rely on git history if you need the previous implementation later.
Ensure no other code is accidentally removed and run a quick build/lint to
confirm no references to getTableConfig remain.

In
`@src/pages/TournamentDetailPage/components/TournamentRegistrationTable/TournamentRegistrationTable.tsx`:
- Around line 42-45: The identity column label in TournamentRegistrationTable is
misleading for team events because the rows render TournamentRegistration via
TournamentRegistrationIdentity (which is player-oriented); change the column
definition (the object with key: 'identity' in TournamentRegistrationTable) to
always use the player-oriented label (e.g., 'Player') instead of using
tournament.useTeams ? 'Team' : 'Player' so the header matches the rendered
content; update any related tests or snapshots that assert the header text if
present.

In `@src/pages/TournamentDetailPage/TournamentDetailPage.tsx`:
- Around line 106-108: Update the stale inline comment that references the
removed 'roster' tab: remove the literal 'roster' and instead refer to the
actual behavior (the default tab returned by getDefaultTab) or the current
default tab name; update the comment near the if (activeTab !== queryTab &&
!loading) check to accurately describe that the first render occurs before
tournament status is known so getDefaultTab determines the initial tab, and
remove any mention of the old 'roster' identifier.

In `@src/pages/TournamentPairingsPage/components/Draggable/Draggable.tsx`:
- Around line 33-39: The drag handle button in Draggable.tsx is an unlabeled
icon-only control (GripVertical) so add an accessible name to the <button> by
providing an aria-label (or aria-labelledby to a nearby visible label) so screen
readers can announce the control; update the button that uses attributes and
listeners (the element with class styles.Draggable_Handle and props
{...attributes} {...listeners}) to include a descriptive aria-label such as
"Drag handle" or context-specific text like "Move pairing".

In
`@src/pages/TournamentPairingsPage/components/PairingsGrid/PairingsGrid.utils.ts`:
- Around line 56-58: The code assumes destPairingIndex is valid but findIndex
can return -1; add a guard after computing destPairingIndex (using the existing
variables destPairingIndex, destSlotIndex, destOccupant, parts, and pairings)
that checks for destPairingIndex === -1 and bails out (e.g., return early, skip
the drop handling, or set destOccupant to undefined) so you never access
pairings[-1]. Ensure any downstream logic that uses destOccupant handles the
early-exit case consistently.

In `@src/pages/TournamentPairingsPage/TournamentPairingsPage.tsx`:
- Around line 92-94: The current isDirty only checks whether every local pairing
exists on the server and short-circuits when either pairings or
tournamentPairings is empty, missing deletions and create-from-empty cases;
replace that logic with a symmetric comparison: compute isDirty as true if any
local pairing has no match in tournamentPairings OR any server pairing has no
match in pairings (e.g., isDirty = pairings.some(p =>
!tournamentPairings?.some(sp => pairingsMatch(p, sp))) ||
tournamentPairings?.some(sp => !pairings.some(p => pairingsMatch(p, sp))));
ensure you do not short-circuit on empty arrays so adding the first pairing or
clearing all pairings is detected.
- Around line 68-79: The effect seeds local pairings from
useGetTournamentPairings too eagerly and when local pairings are cleared; fix by
tracking which round you've initialized: add a state like initializedRound:
number | null, and change the useEffect (watching tournamentPairings, nextRound,
initializedRound) to only call setPairings(sanitize(tournamentPairings)) when
tournamentPairings?.length && initializedRound === null && nextRound === the
round corresponding to tournamentPairings (and/or nextRound !== 0 if
applicable), then set initializedRound to nextRound; remove pairings from the
effect deps so clearing local pairings doesn't re-seed. Ensure you reference
useGetTournamentPairings, nextRound, pairings, setPairings, sanitize and
initializedRound in your change.

---

Outside diff comments:
In `@convex/_model/tournaments/_helpers/deepenTournament.ts`:
- Around line 22-29: Update the JSDoc for deepenTournament to match the current
parameter names: change the `@param` that currently says "tournament" to "doc" and
adjust its description to "Raw Tournament document" (or similar) so the JSDoc
matches the function signature deepenTournament(ctx: QueryCtx, doc:
Doc<'tournaments'>) and editor tooltips show correct info.

In `@convex/_model/utils/deleteTestTournament.ts`:
- Around line 21-39: The code uses forEach with async callbacks for deleting
competitors, pairings, and matchResults so deletions may not complete before the
mutation returns; change each block to build an array of deletion promises
(e.g., using competitors.map(c => ctx.db.delete(c._id))) and await
Promise.all(...) for the competitors, pairings, and matchResults collections so
ctx.db.delete is awaited before the function returns; update the blocks that
reference competitors, pairings, matchResults and ctx.db.delete accordingly (in
the deleteTestTournament logic).

In `@src/components/PageWrapper/PageWrapper.tsx`:
- Around line 57-68: The header currently only renders when showBackButton or a
visible title exists, so actions passed to PageWrapper are never shown alone;
update the render condition around the div with class PageWrapper_Header to also
check for actions (i.e., render when showBackButton || (title && !hideTitle) ||
actions), keeping the internal structure (Button using ArrowLeft and
handleClickBack, the h1 for title, and the PageWrapper_Header_Right wrapper for
actions) unchanged so actions can display even when no back button or title is
present.

In
`@src/components/TournamentCompetitorIdentity/TournamentCompetitorIdentity.tsx`:
- Around line 31-46: Compute a single "actionable" flag (e.g. const actionable =
Boolean(subject && onClick && !loading)) and use it to drive interactivity: set
the button disabled={!actionable}, set data-clickable={actionable}, and update
handleClick to return immediately unless actionable before calling
onClick(subject._id); reference handleClick, subject, onClick, the disabled prop
and data-clickable attribute and include any existing loading/isLoading prop in
the actionable check.

In
`@src/components/TournamentPairingGenerationForm/components/TournamentPairingConfigForm/TournamentPairingConfigForm.tsx`:
- Around line 48-57: The submit handler handleSubmit currently calls onSubmit
twice and bypasses validation; change it so that after calling
validateForm(tournamentPairingConfigSchema, formData, form.setError) you only
call onSubmit when validFormData is truthy, and call it once with the validated
payload merged with forcedValues (e.g., onSubmit?.({ ...validFormData,
...forcedValues })); ensure there is no unconditional onSubmit outside that
success branch so invalid submissions do not fire.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e4a7a6b4-7a61-43da-88af-26e54f8f5885

📥 Commits

Reviewing files that changed from the base of the PR and between 90e0c5b and 2b01393.

⛔ Files ignored due to path filters (1)
  • convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (97)
  • convex/_fixtures/createMockTournament.ts
  • convex/_model/common/tournamentPairingConfig.ts
  • convex/_model/tournamentCompetitors/mutations/createTournamentCompetitor.ts
  • convex/_model/tournamentCompetitors/queries/getTournamentCompetitorsByTournament.ts
  • convex/_model/tournamentCompetitors/table.ts
  • convex/_model/tournamentPairings/_helpers/assignTables.ts
  • convex/_model/tournamentPairings/actions/generateDraftTournamentPairings.ts
  • convex/_model/tournamentPairings/index.ts
  • convex/_model/tournamentPairings/mutations/createTournamentPairings.ts
  • convex/_model/tournamentRegistrations/_helpers/deepenTournamentRegistration.ts
  • convex/_model/tournamentRegistrations/_helpers/sortTournamentCompetitorsByName.ts
  • convex/_model/tournamentRegistrations/queries/getTournamentRegistrationsByTournament.ts
  • convex/_model/tournamentResults/_helpers/aggregateTournamentData.ts
  • convex/_model/tournamentResults/mutations/refreshTournamentResult.ts
  • convex/_model/tournaments/_helpers/deepenTournament.ts
  • convex/_model/tournaments/_helpers/getAvailableActions.ts
  • convex/_model/tournaments/_helpers/getLastVisibleRound.test.ts
  • convex/_model/tournaments/_helpers/getLastVisibleRound.ts
  • convex/_model/tournaments/index.ts
  • convex/_model/tournaments/mutations/deleteTournament.ts
  • convex/_model/tournaments/mutations/updateTournamentPairingConfig.ts
  • convex/_model/utils/deleteTestTournament.ts
  • convex/migrations.ts
  • convex/tournaments.ts
  • src/api.ts
  • src/components/FactionIndicator/FactionIndicator.tsx
  • src/components/FactionIndicator/FactionIndicator.utils.ts
  • src/components/FooterBar/FooterBar.tsx
  • src/components/PageWrapper/PageWrapper.module.scss
  • src/components/PageWrapper/PageWrapper.tsx
  • src/components/ScoreAdjustmentFields/ScoreAdjustmentFields.tsx
  • src/components/TournamentCompetitorIdentity/TournamentCompetitorIdentity.module.scss
  • src/components/TournamentCompetitorIdentity/TournamentCompetitorIdentity.tsx
  • src/components/TournamentCompetitorIdentity/index.ts
  • src/components/TournamentForm/TournamentForm.schema.ts
  • src/components/TournamentForm/components/PairingFields.tsx
  • src/components/TournamentPairingConfigForm/index.ts
  • src/components/TournamentPairingGenerationForm/TournamentPairingGenerationForm.module.scss
  • src/components/TournamentPairingGenerationForm/TournamentPairingGenerationForm.tsx
  • src/components/TournamentPairingGenerationForm/components/TournamentCompetitorChecklist/TournamentCompetitorChecklist.module.scss
  • src/components/TournamentPairingGenerationForm/components/TournamentCompetitorChecklist/TournamentCompetitorChecklist.tsx
  • src/components/TournamentPairingGenerationForm/components/TournamentCompetitorChecklist/index.ts
  • src/components/TournamentPairingGenerationForm/components/TournamentPairingConfigFields/TournamentPairingConfigFields.hooks.ts
  • src/components/TournamentPairingGenerationForm/components/TournamentPairingConfigFields/TournamentPairingConfigFields.module.scss
  • src/components/TournamentPairingGenerationForm/components/TournamentPairingConfigFields/TournamentPairingConfigFields.tsx
  • src/components/TournamentPairingGenerationForm/components/TournamentPairingConfigFields/index.ts
  • src/components/TournamentPairingGenerationForm/components/TournamentPairingConfigForm/TournamentPairingConfigForm.tsx
  • src/components/TournamentPairingGenerationForm/components/TournamentPairingConfigForm/index.ts
  • src/components/TournamentPairingGenerationForm/index.ts
  • src/components/TournamentProvider/TournamentContextMenu.tsx
  • src/components/TournamentProvider/TournamentProvider.hooks.tsx
  • src/components/TournamentProvider/actions/useConfigureRoundAction.tsx
  • src/components/TournamentProvider/actions/useStartAction.tsx
  • src/components/TournamentProvider/index.ts
  • src/components/TournamentRegistrationIdentity/TournamentRegistrationIdentity.module.scss
  • src/components/TournamentRegistrationIdentity/TournamentRegistrationIdentity.tsx
  • src/components/TournamentRegistrationIdentity/index.ts
  • src/hooks/useAsyncState.ts
  • src/hooks/useFormDialog.tsx
  • src/pages/MatchResultDetailPage/MatchResultDetailPage.tsx
  • src/pages/TournamentCompetitorDetailPage/TournamentCompetitorDetailPage.tsx
  • src/pages/TournamentDetailPage/TournamentDetailPage.tsx
  • src/pages/TournamentDetailPage/components/TournamentCompetitorTable/TournamentCompetitorTable.module.scss
  • src/pages/TournamentDetailPage/components/TournamentCompetitorTable/TournamentCompetitorTable.tsx
  • src/pages/TournamentDetailPage/components/TournamentCompetitorTable/index.ts
  • src/pages/TournamentDetailPage/components/TournamentCompetitorsCard/TournamentCompetitorsCard.module.scss
  • src/pages/TournamentDetailPage/components/TournamentCompetitorsCard/TournamentCompetitorsCard.tsx
  • src/pages/TournamentDetailPage/components/TournamentCompetitorsCard/TournamentCompetitorsCard.utils.tsx
  • src/pages/TournamentDetailPage/components/TournamentCompetitorsCard/index.ts
  • src/pages/TournamentDetailPage/components/TournamentRankingsCard/TournamentRankingsCard.tsx
  • src/pages/TournamentDetailPage/components/TournamentRankingsCard/TournamentRankingsCard.utils.tsx
  • src/pages/TournamentDetailPage/components/TournamentRankingsCard/index.ts
  • src/pages/TournamentDetailPage/components/TournamentRegistrationTable/TournamentRegistrationTable.module.scss
  • src/pages/TournamentDetailPage/components/TournamentRegistrationTable/TournamentRegistrationTable.tsx
  • src/pages/TournamentDetailPage/components/TournamentRegistrationTable/index.ts
  • src/pages/TournamentDetailPage/components/TournamentRosterCard/TournamentRosterCard.module.scss
  • src/pages/TournamentDetailPage/components/TournamentRosterCard/TournamentRosterCard.tsx
  • src/pages/TournamentDetailPage/components/TournamentRosterCard/TournamentRosterCard.utils.tsx
  • src/pages/TournamentDetailPage/components/TournamentRosterCard/index.ts
  • src/pages/TournamentPairingDetailPage/TournamentPairingDetailPage.tsx
  • src/pages/TournamentPairingsPage/TournamentPairingsPage.module.scss
  • src/pages/TournamentPairingsPage/TournamentPairingsPage.schema.ts
  • src/pages/TournamentPairingsPage/TournamentPairingsPage.tsx
  • src/pages/TournamentPairingsPage/TournamentPairingsPage.utils.tsx
  • src/pages/TournamentPairingsPage/components/Draggable/Draggable.module.scss
  • src/pages/TournamentPairingsPage/components/Draggable/Draggable.tsx
  • src/pages/TournamentPairingsPage/components/Draggable/index.ts
  • src/pages/TournamentPairingsPage/components/Droppable/Droppable.module.scss
  • src/pages/TournamentPairingsPage/components/Droppable/Droppable.tsx
  • src/pages/TournamentPairingsPage/components/Droppable/index.ts
  • src/pages/TournamentPairingsPage/components/PairingsGrid/PairingsGrid.hooks.ts
  • src/pages/TournamentPairingsPage/components/PairingsGrid/PairingsGrid.module.scss
  • src/pages/TournamentPairingsPage/components/PairingsGrid/PairingsGrid.tsx
  • src/pages/TournamentPairingsPage/components/PairingsGrid/PairingsGrid.utils.ts
  • src/pages/TournamentPairingsPage/components/PairingsGrid/index.ts
  • src/services/tournaments.ts
  • src/utils/common/getRoundOptions.ts
💤 Files with no reviewable changes (14)
  • src/pages/TournamentDetailPage/components/TournamentRankingsCard/index.ts
  • src/components/TournamentProvider/index.ts
  • src/components/TournamentProvider/TournamentProvider.hooks.tsx
  • src/pages/TournamentDetailPage/components/TournamentRosterCard/TournamentRosterCard.module.scss
  • src/pages/TournamentDetailPage/components/TournamentRosterCard/TournamentRosterCard.utils.tsx
  • src/pages/TournamentDetailPage/components/TournamentRosterCard/index.ts
  • src/components/TournamentPairingConfigForm/index.ts
  • src/components/TournamentProvider/actions/useStartAction.tsx
  • src/pages/TournamentDetailPage/components/TournamentRosterCard/TournamentRosterCard.tsx
  • src/pages/TournamentPairingsPage/TournamentPairingsPage.module.scss
  • src/components/TournamentProvider/TournamentContextMenu.tsx
  • convex/_model/tournamentPairings/mutations/createTournamentPairings.ts
  • src/pages/TournamentDetailPage/components/TournamentRankingsCard/TournamentRankingsCard.utils.tsx
  • src/pages/TournamentDetailPage/components/TournamentRankingsCard/TournamentRankingsCard.tsx

Comment thread convex/_fixtures/createMockTournament.ts
Comment thread convex/_model/tournamentPairings/_helpers/assignTables.ts
Comment thread convex/_model/tournaments/_helpers/getLastVisibleRound.ts Outdated
Comment thread convex/migrations.ts Outdated
Comment thread src/components/FactionIndicator/FactionIndicator.tsx Outdated
Comment thread src/pages/TournamentDetailPage/TournamentDetailPage.tsx Outdated
Comment thread src/pages/TournamentPairingsPage/components/Draggable/Draggable.tsx
Comment thread src/pages/TournamentPairingsPage/TournamentPairingsPage.tsx Outdated
Comment thread src/pages/TournamentPairingsPage/TournamentPairingsPage.tsx Outdated
@ianpaschal ianpaschal merged commit 24ea33f into main May 26, 2026
4 checks passed
@ianpaschal ianpaschal deleted the ian/cc-2-improve-pairing-workflow branch May 26, 2026 17:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant