Skip to content

CC-43: Replace Registration Details with List Details#334

Open
ianpaschal wants to merge 17 commits into
mainfrom
ian/cc-43-replace-registration-details-with-list-details
Open

CC-43: Replace Registration Details with List Details#334
ianpaschal wants to merge 17 commits into
mainfrom
ian/cc-43-replace-registration-details-with-list-details

Conversation

@ianpaschal
Copy link
Copy Markdown
Owner

@ianpaschal ianpaschal commented May 26, 2026

Resolves CC-43

Summary by CodeRabbit

Release Notes

  • New Features

    • Lists can now be created without file uploads, enabling bare list support.
    • List metadata (faction, alignment, era) is now captured and managed per list.
    • Match results now link to and display associated player lists.
  • Improvements

    • Tournament registration process streamlined with simplified data collection.
    • Game system-specific metadata fields now dynamically render based on selected system.

Review Change Stack

@vercel
Copy link
Copy Markdown

vercel Bot commented May 26, 2026

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

Project Deployment Actions Updated (UTC)
combat-command Error Error May 30, 2026 1:47pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 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: f930546b-94da-4d58-a715-56cad5603867

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

This PR refactors tournament registration workflows to be list-centric rather than details-centric. The backend makes lists structurally flexible (optional storage, optional game-specific metadata), while the frontend shifts registration from simple alignment/faction selection to managing multiple game-system-aware lists. Key changes include introducing ListMetaFields component hierarchy for game-specific metadata input, updating match result forms to select lists instead of inline faction, and adding migrations to backfill list associations on existing data. The createTournamentCompetitor mutation is removed in favor of direct registration creation.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: replacing registration details (alignment/faction) with list-based details throughout the codebase, which is the primary objective reflected across all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ian/cc-43-replace-registration-details-with-list-details

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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: 12

Caution

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

⚠️ Outside diff range comments (2)
src/components/MatchResultForm/components/MatchResultPlayerField/MatchResultPlayerField.tsx (1)

132-140: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

CSS module keys are stale after component rename.

Line 132 and Line 140 reference styles.PlayerField*, but the module now defines MatchResultPlayerField*. Those classes resolve to undefined, so this section loses intended styling.

Proposed fix
-          <div className={styles.PlayerField}>
+          <div className={styles.MatchResultPlayerField}>
...
-              <div className={sx({ border: 'top' }, styles.PlayerField_Placeholder)}>
+              <div className={sx({ border: 'top' }, styles.MatchResultPlayerField_Placeholder)}>
🤖 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/MatchResultForm/components/MatchResultPlayerField/MatchResultPlayerField.tsx`
around lines 132 - 140, The JSX references stale CSS keys styles.PlayerField and
styles.PlayerField_Placeholder after the component rename; update these
references to the actual class names exported by the CSS module (e.g.,
styles.MatchResultPlayerField and styles.MatchResultPlayerField_Placeholder)
wherever used in MatchResultPlayerField.tsx (around the UserFinder block and the
placeholder div) so the styling is applied again; alternatively, rename the CSS
module keys to match PlayerField if you prefer to keep the JSX unchanged—ensure
consistency between the CSS module exports and the identifiers used in the
MatchResultPlayerField component.
src/components/MatchResultForm/components/MatchResultPlayerFields/gameSystems/TeamYankeeV2MatchResultPlayerFields/TeamYankeeV2MatchResultPlayerFields.tsx (1)

38-51: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve legacy faction fields when resetting details on battle-plan changes.

This reset path reapplies defaults and restores only battle-plan/units-lost values. For records without player0ListId/player1ListId, it can silently overwrite details.player0Faction/details.player1Faction, which breaks the fallback-to-details faction behavior.

💡 Suggested patch
     reset((prev) => ({
       ...prev,
       details: {
         ...prev.details,

         // Reset mission and dependent fields if battle plans were changed:
         ...matchResultDetails.defaultValues,

         // Preserve per-player fields (and update the required field):
+        player0Faction: prev.details.player0Faction,
+        player1Faction: prev.details.player1Faction,
         player0BattlePlan: index === 0 ? value as BattlePlan : prev.details.player0BattlePlan,
         player0UnitsLost: prev.details.player0UnitsLost,
         player1BattlePlan: index === 1 ? value as BattlePlan : prev.details.player1BattlePlan,
         player1UnitsLost: prev.details.player1UnitsLost,
       },
     }), { keepDirty: true });
🤖 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/MatchResultForm/components/MatchResultPlayerFields/gameSystems/TeamYankeeV2MatchResultPlayerFields/TeamYankeeV2MatchResultPlayerFields.tsx`
around lines 38 - 51, The reset logic in TeamYankeeV2MatchResultPlayerFields.tsx
currently reapplies matchResultDetails.defaultValues and only preserves
battle-plan and units-lost fields, which can overwrite legacy per-player faction
fields; update the reset((prev) => ({...})) block to also preserve
player0Faction and player1Faction when the corresponding player list id is
missing (i.e., if prev.details.player0ListId is falsy keep
prev.details.player0Faction; likewise for player1), ensuring these legacy
faction fields are copied into details alongside
player0BattlePlan/player1BattlePlan and player0UnitsLost/player1UnitsLost
instead of being clobbered by matchResultDetails.defaultValues.
🤖 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/_model/lists/table.ts`:
- Around line 1-5: Replace the locally redefined union validator with the shared
listData Zod validator from convex/_model/common/listData.ts: import listData
and pass zodToConvex(listData) into defineTable (and any other places where the
union was redefined on lines noted), so the table schema uses the single
source-of-truth listData validator instead of duplicating the union logic.

In
`@convex/_model/tournamentRegistrations/_helpers/deepenTournamentRegistration.ts`:
- Around line 43-52: The current derivation uses lists.find((l) => !l.storageId)
which only looks at one "bare" list and ignores any other lists (and loses
metadata if that single list later gets a storageId); instead, scan all lists
for metadata and derive alignment/faction from the first non-null meta value or
collect all meta values across lists. Replace registrationList and
derivedAlignment/derivedFaction with logic that: 1) finds the first
lists[].data?.meta?.alignment (and faction) across all lists (not filtered by
storageId) falling back to details.*, and 2) when building alignments/factions
arrays (alignments, factions) include unique values collected from all lists'
meta fields (and the derived fallback) when alignmentsVisible/factionsVisible is
true so visibleDetails.alignment and visibleDetails.faction reflect that
aggregated result.

In
`@convex/_model/tournamentRegistrations/mutations/createTournamentRegistration.ts`:
- Line 27: The code currently inserts lists using the schema v.array(listData)
without checking that each list's schema matches the tournament.gameSystem;
update the createTournamentRegistration mutation to validate each item in
listData against the tournament.gameSystem before building the insert row
(identify and use the variables/function createTournamentRegistration, listData,
and tournament.gameSystem), rejecting or transforming mismatched entries and
returning a validation error; apply the same validation logic to the other
insertion block referenced around lines 107-114 so all inserted rows are
guaranteed to be compatible with tournament.gameSystem.
- Around line 99-105: The insert into tournamentRegistrations in
createTournamentRegistration.ts is missing the scoreAdjustments field so
args.scoreAdjustments is dropped; include scoreAdjustments:
args.scoreAdjustments in the payload passed to ctx.db.insert (alongside active,
confirmed, tournamentCompetitorId, tournamentId, userId) so the caller-provided
adjustments are persisted when creating the tournamentRegistrationId record.

In `@package.json`:
- Line 35: The package.json currently pins
`@ianpaschal/combat-command-game-systems` to a brittle local tarball via
"file:../combat-command-game-systems/ianpaschal-combat-command-game-systems-1.6.0.tgz"
which breaks CI (npm ci) because the sibling tarball/dir is not provided; change
the dependency strategy by either (A) switching the dependency to a published
registry version (e.g., "`@ianpaschal/combat-command-game-systems`": "1.6.0") and
update package-lock.json accordingly, or (B) convert the repo to a
workspace/local package reference (workspaces config) or (C) add a CI step in
.github/workflows/ci.yml to fetch or create the ../combat-command-game-systems
tarball before running npm ci and then regenerate lockfile so installs are
reproducible; update package.json and package-lock.json to reflect the chosen
approach and ensure npm ci succeeds.

In `@src/components/ListForm/ListForm.tsx`:
- Around line 70-71: The schema is being chosen from the pre-merge form state
(formData.gameSystem) while forcedValues may override gameSystem; first build
the merged payload (merge({}, formData, forcedValues, { userId: user?._id })),
then call createSchema using payload.gameSystem and pass that same payload into
validateForm (i.e., use the merged payload for both schema selection and
validation) to ensure the correct game-system schema is used; update references
around createSchema, validateForm, merge, formData, forcedValues, user?._id, and
validFormData accordingly.

In
`@src/components/ListMetaFields/gameSystems/TeamYankeeV2ListMetaFields/TeamYankeeV2ListMetaFields.tsx`:
- Around line 43-45: The disabled condition on the Faction FormField is using &&
and should be changed to || so the field is disabled when either forceDiagram is
falsy or there are fewer than 2 factionOptions; update the disabled prop in
TeamYankeeV2ListMetaFields (the FormField with name `${namePrefix}.faction`,
using forceDiagram and factionOptions) to use `||` instead of `&&`, and make the
identical change in FlamesOfWarV4ListMetaFields to keep behavior consistent.

In `@src/components/ListMetaFields/ListMetaFields.tsx`:
- Line 24: ListMetaFields currently watches the wrong form field and throws
during render when gameSystem is undefined; change it to read the gameSystem
from the provided namePrefix scope (watch(`${namePrefix}.gameSystem`) or
equivalent using the namePrefix prop) and remove the render-time throw — instead
render a safe fallback (null or a placeholder) when the scoped gameSystem is
missing or unknown, and only render the matched sub-component when a valid
gameSystem is found; update references inside ListMetaFields to use namePrefix
and the scoped field name rather than the root 'gameSystem'.

In
`@src/components/MatchResultForm/components/MatchResultListField/MatchResultListField.tsx`:
- Around line 72-76: The ListForm is creating lists without attaching the
current registration id, so newly created lists may not appear in
registration.lists; update the ListForm usage to include the registration id in
both forcedValues and the onSubmit payload by adding registration.id to
forcedValues (e.g. forcedValues={{ gameSystem, registrationId: registration.id
}}) and passing that registrationId into createList (onSubmit={(data) =>
createList({ ...data, registrationId: registration.id })}), updating references
to ListForm, forcedValues, createList and registration accordingly so created
lists are scoped to the selected registration.
- Around line 38-45: When the watched player userId (userId) or registration
changes, ensure the previously selected list (player${index}ListId /
selectedListId) is cleared so a list from a prior user cannot be submitted: add
an effect that depends on userId, registration (and optionally
tournamentPairingId/gameSystem) which checks whether selectedListId exists in
the current registration's allLists (registration or allLists) and if not calls
setValue(`player${index}ListId`, undefined) or
resetField(`player${index}ListId`) to clear it; reference the existing watch,
selectedListId, registration, allLists and use setValue/resetField inside that
effect.

In `@src/components/TournamentRegistrationForm/TournamentRegistrationForm.tsx`:
- Line 225: The div inside TournamentRegistrationForm that renders each list
item erroneously uses the root prop className (className={className}) and lacks
a React key; update the map render so the per-item div does not reuse the root
className (use a specific item class like itemClassName or a static className
for list items, or remove className from the item and keep className on the root
container) and add a unique key prop (e.g., key={participant.id} or key={index})
to that div in the participants.map (or equivalent) callback to fix
reconciliation issues.
- Around line 224-235: The Lists rendering crashes because ListMetaFields relies
on watch('gameSystem') but the TournamentRegistrationForm defaultValues/schema
don't provide gameSystem, so either add a gameSystem form field (initialize it
from tournament.gameSystem in defaultValues and include it in the form schema)
or stop relying on RHF context by passing tournament.gameSystem as a prop into
ListMetaFields and update ListMetaFields to use that prop instead of
watch('gameSystem'); additionally fix the list map in TournamentRegistrationForm
(the listFields.map block) by adding key={field.id} to the root <div> and stop
reusing the outer form className for each row (use a dedicated list-row
className instead) so styling and React keys are correct.

---

Outside diff comments:
In
`@src/components/MatchResultForm/components/MatchResultPlayerField/MatchResultPlayerField.tsx`:
- Around line 132-140: The JSX references stale CSS keys styles.PlayerField and
styles.PlayerField_Placeholder after the component rename; update these
references to the actual class names exported by the CSS module (e.g.,
styles.MatchResultPlayerField and styles.MatchResultPlayerField_Placeholder)
wherever used in MatchResultPlayerField.tsx (around the UserFinder block and the
placeholder div) so the styling is applied again; alternatively, rename the CSS
module keys to match PlayerField if you prefer to keep the JSX unchanged—ensure
consistency between the CSS module exports and the identifiers used in the
MatchResultPlayerField component.

In
`@src/components/MatchResultForm/components/MatchResultPlayerFields/gameSystems/TeamYankeeV2MatchResultPlayerFields/TeamYankeeV2MatchResultPlayerFields.tsx`:
- Around line 38-51: The reset logic in TeamYankeeV2MatchResultPlayerFields.tsx
currently reapplies matchResultDetails.defaultValues and only preserves
battle-plan and units-lost fields, which can overwrite legacy per-player faction
fields; update the reset((prev) => ({...})) block to also preserve
player0Faction and player1Faction when the corresponding player list id is
missing (i.e., if prev.details.player0ListId is falsy keep
prev.details.player0Faction; likewise for player1), ensuring these legacy
faction fields are copied into details alongside
player0BattlePlan/player1BattlePlan and player0UnitsLost/player1UnitsLost
instead of being clobbered by matchResultDetails.defaultValues.
🪄 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: f5fe83fe-cd20-4951-a3af-ab3b48db4729

📥 Commits

Reviewing files that changed from the base of the PR and between 7f4b1ad and 594ba13.

⛔ Files ignored due to path filters (2)
  • convex/_generated/api.d.ts is excluded by !**/_generated/**
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (53)
  • convex/_model/_test/mutations/cleanUp.ts
  • convex/_model/common/listData.ts
  • convex/_model/lists/_helpers/checkListApproved.ts
  • convex/_model/lists/_helpers/checkListSubmittedOnTime.ts
  • convex/_model/lists/_helpers/deepenList.ts
  • convex/_model/lists/index.ts
  • convex/_model/lists/mutations/createList.ts
  • convex/_model/lists/mutations/updateList.ts
  • convex/_model/lists/table.ts
  • convex/_model/matchResults/_helpers/deepenMatchResult.ts
  • convex/_model/tournamentCompetitors/index.ts
  • convex/_model/tournamentCompetitors/mutations/createTournamentCompetitor.ts
  • convex/_model/tournamentRegistrations/_helpers/deepenTournamentRegistration.ts
  • convex/_model/tournamentRegistrations/mutations/createTournamentRegistration.ts
  • convex/_model/tournamentRegistrations/table.ts
  • convex/_model/tournaments/table.ts
  • convex/migrations.ts
  • convex/tournamentCompetitors.ts
  • package.json
  • src/components/ListDetails/ListDetails.tsx
  • src/components/ListForm/ListForm.schema.ts
  • src/components/ListForm/ListForm.tsx
  • src/components/ListMetaFields/ListMetaFields.schema.ts
  • src/components/ListMetaFields/ListMetaFields.tsx
  • src/components/ListMetaFields/gameSystems/FlamesOfWarV4ListMetaFields/FlamesOfWarV4ListMetaFields.tsx
  • src/components/ListMetaFields/gameSystems/FlamesOfWarV4ListMetaFields/FlamesOfWarV4ListMetaFields.types.ts
  • src/components/ListMetaFields/gameSystems/FlamesOfWarV4ListMetaFields/index.ts
  • src/components/ListMetaFields/gameSystems/TeamYankeeV2ListMetaFields/TeamYankeeV2ListMetaFields.tsx
  • src/components/ListMetaFields/gameSystems/TeamYankeeV2ListMetaFields/TeamYankeeV2ListMetaFields.types.ts
  • src/components/ListMetaFields/gameSystems/TeamYankeeV2ListMetaFields/index.ts
  • src/components/ListMetaFields/index.ts
  • src/components/MatchResultForm/MatchResultForm.schema.ts
  • src/components/MatchResultForm/components/MatchResultListField/MatchResultListField.module.scss
  • src/components/MatchResultForm/components/MatchResultListField/MatchResultListField.tsx
  • src/components/MatchResultForm/components/MatchResultListField/index.ts
  • src/components/MatchResultForm/components/MatchResultPlayerField/MatchResultPlayerField.module.scss
  • src/components/MatchResultForm/components/MatchResultPlayerField/MatchResultPlayerField.tsx
  • src/components/MatchResultForm/components/MatchResultPlayerField/index.ts
  • src/components/MatchResultForm/components/MatchResultPlayerFields/gameSystems/FlamesOfWarV4MatchResultPlayerFields/FlamesOfWarV4MatchResultPlayerFields.tsx
  • src/components/MatchResultForm/components/MatchResultPlayerFields/gameSystems/FlamesOfWarV4MatchResultPlayerFields/FlamesOfWarV4MatchResultPlayerFields.types.ts
  • src/components/MatchResultForm/components/MatchResultPlayerFields/gameSystems/TeamYankeeV2MatchResultPlayerFields/TeamYankeeV2MatchResultPlayerFields.tsx
  • src/components/MatchResultForm/components/MatchResultPlayerFields/gameSystems/TeamYankeeV2MatchResultPlayerFields/TeamYankeeV2MatchResultPlayerFields.types.ts
  • src/components/MatchResultForm/components/PlayerField/index.ts
  • src/components/TournamentForm/TournamentForm.schema.ts
  • src/components/TournamentForm/components/CompetitorFields.tsx
  • src/components/TournamentForm/components/PairingFields.tsx
  • src/components/TournamentRegistrationForm/TournamentRegistrationForm.schema.ts
  • src/components/TournamentRegistrationForm/TournamentRegistrationForm.tsx
  • src/components/TournamentRegistrationForm/components/DetailsFields/DetailsFields.module.scss
  • src/components/TournamentRegistrationForm/components/DetailsFields/DetailsFields.tsx
  • src/components/TournamentRegistrationForm/components/DetailsFields/index.ts
  • src/components/TournamentRegistrationProvider/actions/useCreateListAction.tsx
  • src/services/tournamentCompetitors.ts
💤 Files with no reviewable changes (9)
  • src/components/MatchResultForm/components/PlayerField/index.ts
  • src/components/MatchResultForm/components/MatchResultPlayerFields/gameSystems/FlamesOfWarV4MatchResultPlayerFields/FlamesOfWarV4MatchResultPlayerFields.types.ts
  • convex/tournamentCompetitors.ts
  • src/components/TournamentRegistrationForm/components/DetailsFields/index.ts
  • convex/_model/tournamentCompetitors/mutations/createTournamentCompetitor.ts
  • src/components/TournamentRegistrationForm/components/DetailsFields/DetailsFields.tsx
  • src/components/TournamentRegistrationForm/components/DetailsFields/DetailsFields.module.scss
  • src/components/MatchResultForm/components/MatchResultPlayerFields/gameSystems/TeamYankeeV2MatchResultPlayerFields/TeamYankeeV2MatchResultPlayerFields.types.ts
  • convex/_model/tournamentCompetitors/index.ts

Comment on lines +1 to +5
import { FlamesOfWarV4, TeamYankeeV2 } from '@ianpaschal/combat-command-game-systems';
import { GameSystem } from '@ianpaschal/combat-command-game-systems/common';
import { defineTable } from 'convex/server';
import { v } from 'convex/values';
import { zodToConvex } from 'convex-helpers/server/zod';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use the shared listData validator instead of redefining the union here.

This duplicates the schema contract and can drift from convex/_model/common/listData.ts. Importing the shared validator keeps one source of truth.

♻️ Proposed refactor
-import { FlamesOfWarV4, TeamYankeeV2 } from '`@ianpaschal/combat-command-game-systems`';
 import { GameSystem } from '`@ianpaschal/combat-command-game-systems/common`';
 import { defineTable } from 'convex/server';
 import { v } from 'convex/values';
-import { zodToConvex } from 'convex-helpers/server/zod';
+import { listData } from '../common/listData';
@@
-const listDataValidator = v.union(
-  zodToConvex(FlamesOfWarV4.listData.schema),
-  zodToConvex(TeamYankeeV2.listData.schema),
-);
-
 export const editableFields = {
   gameSystem,
   storageId: v.optional(v.id('_storage')),
   userId: v.id('users'),
   tournamentRegistrationId: v.optional(v.id('tournamentRegistrations')),
-  data: v.optional(listDataValidator),
+  data: v.optional(listData),
 };

Also applies to: 11-14, 21-21

🤖 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/lists/table.ts` around lines 1 - 5, Replace the locally
redefined union validator with the shared listData Zod validator from
convex/_model/common/listData.ts: import listData and pass zodToConvex(listData)
into defineTable (and any other places where the union was redefined on lines
noted), so the table schema uses the single source-of-truth listData validator
instead of duplicating the union logic.

Comment on lines +43 to +52
const registrationList = lists.find((l) => !l.storageId);
const derivedAlignment = registrationList?.data?.meta?.alignment ?? details?.alignment ?? null;
const derivedFaction = registrationList?.data?.meta?.faction ?? details?.faction ?? null;

const alignments = Array.from(new Set(alignmentsVisible && derivedAlignment ? [derivedAlignment] : []));
const factions = Array.from(new Set(factionsVisible && derivedFaction ? [derivedFaction] : []));

const visibleDetails = {
alignment: alignmentsVisible ? details?.alignment ?? null : null,
faction: factionsVisible ? details?.faction ?? null : null,
alignment: alignmentsVisible ? derivedAlignment : null,
faction: factionsVisible ? derivedFaction : null,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Derivation logic ignores uploaded lists and additional lists.

Using lists.find((l) => !l.storageId) restricts derivation to one bare list. If that list is later updated with a storageId, alignment/faction can become null even when metadata exists, and any additional lists are ignored.

🔧 Suggested fix
-  const registrationList = lists.find((l) => !l.storageId);
-  const derivedAlignment = registrationList?.data?.meta?.alignment ?? details?.alignment ?? null;
-  const derivedFaction = registrationList?.data?.meta?.faction ?? details?.faction ?? null;
-
-  const alignments = Array.from(new Set(alignmentsVisible && derivedAlignment ? [derivedAlignment] : []));
-  const factions = Array.from(new Set(factionsVisible && derivedFaction ? [derivedFaction] : []));
+  const listAlignments = lists
+    .map((l) => l.data?.meta?.alignment)
+    .filter((a): a is NonNullable<typeof a> => Boolean(a));
+  const listFactions = lists
+    .map((l) => l.data?.meta?.faction)
+    .filter((f): f is NonNullable<typeof f> => Boolean(f));
+
+  const alignments = Array.from(
+    new Set(alignmentsVisible ? (listAlignments.length ? listAlignments : details?.alignment ? [details.alignment] : []) : []),
+  );
+  const factions = Array.from(
+    new Set(factionsVisible ? (listFactions.length ? listFactions : details?.faction ? [details.faction] : []) : []),
+  );
+
+  const derivedAlignment = alignments[0] ?? null;
+  const derivedFaction = factions[0] ?? null;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const registrationList = lists.find((l) => !l.storageId);
const derivedAlignment = registrationList?.data?.meta?.alignment ?? details?.alignment ?? null;
const derivedFaction = registrationList?.data?.meta?.faction ?? details?.faction ?? null;
const alignments = Array.from(new Set(alignmentsVisible && derivedAlignment ? [derivedAlignment] : []));
const factions = Array.from(new Set(factionsVisible && derivedFaction ? [derivedFaction] : []));
const visibleDetails = {
alignment: alignmentsVisible ? details?.alignment ?? null : null,
faction: factionsVisible ? details?.faction ?? null : null,
alignment: alignmentsVisible ? derivedAlignment : null,
faction: factionsVisible ? derivedFaction : null,
const listAlignments = lists
.map((l) => l.data?.meta?.alignment)
.filter((a): a is NonNullable<typeof a> => Boolean(a));
const listFactions = lists
.map((l) => l.data?.meta?.faction)
.filter((f): f is NonNullable<typeof f> => Boolean(f));
const alignments = Array.from(
new Set(alignmentsVisible ? (listAlignments.length ? listAlignments : details?.alignment ? [details.alignment] : []) : []),
);
const factions = Array.from(
new Set(factionsVisible ? (listFactions.length ? listFactions : details?.faction ? [details.faction] : []) : []),
);
const derivedAlignment = alignments[0] ?? null;
const derivedFaction = factions[0] ?? null;
const visibleDetails = {
alignment: alignmentsVisible ? derivedAlignment : null,
faction: factionsVisible ? derivedFaction : null,
🤖 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/tournamentRegistrations/_helpers/deepenTournamentRegistration.ts`
around lines 43 - 52, The current derivation uses lists.find((l) =>
!l.storageId) which only looks at one "bare" list and ignores any other lists
(and loses metadata if that single list later gets a storageId); instead, scan
all lists for metadata and derive alignment/faction from the first non-null meta
value or collect all meta values across lists. Replace registrationList and
derivedAlignment/derivedFaction with logic that: 1) finds the first
lists[].data?.meta?.alignment (and faction) across all lists (not filtered by
storageId) falling back to details.*, and 2) when building alignments/factions
arrays (alignments, factions) include unique values collected from all lists'
meta fields (and the derived fallback) when alignmentsVisible/factionsVisible is
true so visibleDetails.alignment and visibleDetails.faction reflect that
aggregated result.

})),
nameVisibilityConsent: v.optional(v.boolean()),
scoreAdjustments: v.optional(v.array(scoreAdjustment)),
lists: v.array(listData),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate each list payload against tournament.gameSystem before insert.

listData accepts multiple game-system schemas, but inserted rows are always stamped with tournament.gameSystem. Without a compatibility check, this can persist mismatched list metadata under the wrong game system.

🔧 Suggested fix
+  // Validate list payloads are compatible with this tournament's game system.
+  const helpers = getGameSystem(tournament.gameSystem);
+
   for (const data of args.lists) {
+    if (!helpers.listData.schema.safeParse(data).success) {
+      throw new ConvexError(getErrorMessage('LIST_DATA_GAME_SYSTEM_MISMATCH'));
+    }
+
     await ctx.db.insert('lists', {
       gameSystem: tournament.gameSystem,
       userId: args.userId,
       tournamentRegistrationId,
       locked: false,
       data,
     });
   }

Also applies to: 107-114

🤖 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/tournamentRegistrations/mutations/createTournamentRegistration.ts`
at line 27, The code currently inserts lists using the schema v.array(listData)
without checking that each list's schema matches the tournament.gameSystem;
update the createTournamentRegistration mutation to validate each item in
listData against the tournament.gameSystem before building the insert row
(identify and use the variables/function createTournamentRegistration, listData,
and tournament.gameSystem), rejecting or transforming mismatched entries and
returning a validation error; apply the same validation logic to the other
insertion block referenced around lines 107-114 so all inserted rows are
guaranteed to be compatible with tournament.gameSystem.

Comment on lines 99 to 105
const tournamentRegistrationId = await ctx.db.insert('tournamentRegistrations', {
active: activePlayerCount < tournament.competitorSize,
confirmed: args.userId === currentUserId,
listApproved: false,
tournamentCompetitorId,
tournamentId: args.tournamentId,
userId: args.userId,
details: args.details,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Persist scoreAdjustments on registration creation.

scoreAdjustments is accepted in the args schema but omitted from the tournamentRegistrations insert payload, so caller input is silently dropped.

🔧 Suggested fix
   const tournamentRegistrationId = await ctx.db.insert('tournamentRegistrations', {
     active: activePlayerCount < tournament.competitorSize,
     confirmed: args.userId === currentUserId,
     tournamentCompetitorId,
     tournamentId: args.tournamentId,
     userId: args.userId,
+    scoreAdjustments: args.scoreAdjustments,
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const tournamentRegistrationId = await ctx.db.insert('tournamentRegistrations', {
active: activePlayerCount < tournament.competitorSize,
confirmed: args.userId === currentUserId,
listApproved: false,
tournamentCompetitorId,
tournamentId: args.tournamentId,
userId: args.userId,
details: args.details,
});
const tournamentRegistrationId = await ctx.db.insert('tournamentRegistrations', {
active: activePlayerCount < tournament.competitorSize,
confirmed: args.userId === currentUserId,
tournamentCompetitorId,
tournamentId: args.tournamentId,
userId: args.userId,
scoreAdjustments: args.scoreAdjustments,
});
🤖 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/tournamentRegistrations/mutations/createTournamentRegistration.ts`
around lines 99 - 105, The insert into tournamentRegistrations in
createTournamentRegistration.ts is missing the scoreAdjustments field so
args.scoreAdjustments is dropped; include scoreAdjustments:
args.scoreAdjustments in the payload passed to ctx.db.insert (alongside active,
confirmed, tournamentCompetitorId, tournamentId, userId) so the caller-provided
adjustments are persisted when creating the tournamentRegistrationId record.

Comment thread package.json
"@hookform/resolvers": "^3.9.0",
"@ianpaschal/combat-command-components": "^2.1.1",
"@ianpaschal/combat-command-game-systems": "^1.6.0",
"@ianpaschal/combat-command-game-systems": "file:../combat-command-game-systems/ianpaschal-combat-command-game-systems-1.6.0.tgz",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Verify whether the referenced sibling tarball exists in this environment:"
ls -l ../combat-command-game-systems/ianpaschal-combat-command-game-systems-1.6.0.tgz

echo
echo "2) Check CI/workflows for install steps that would fail without that sibling path:"
if [ -d .github/workflows ]; then
  rg -n -C2 'npm (ci|install)|pnpm install|yarn install' .github/workflows
else
  echo ".github/workflows not found"
fi

Repository: ianpaschal/combat-command

Length of output: 291


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package.json context =="
nl -ba package.json | sed -n '25,50p'

echo
echo "== Check for referenced tarball/dir mentions in repo =="
rg -n --hidden -S "combat-command-game-systems-1\.6\.0\.tgz|../combat-command-game-systems|`@ianpaschal/combat-command-game-systems`" .

echo
echo "== Check CI/workflows for install steps/provisioning =="
if [ -d .github/workflows ]; then
  rg -n --hidden -S 'combat-command-game-systems|1\.6\.0\.tgz|file:../|npm (ci|install)|pnpm install|yarn install' .github/workflows || true
else
  echo ".github/workflows not found"
fi

echo
echo "== Does the sibling dir exist in this environment? =="
ls -ld ../combat-command-game-systems 2>/dev/null || echo "sibling dir missing"

Repository: ianpaschal/combat-command

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package.json context =="
cat -n package.json | sed -n '25,50p'

echo
echo "== Check for referenced tarball/dir mentions in repo =="
rg -n --hidden -S "combat-command-game-systems-1\.6\.0\.tgz|../combat-command-game-systems|`@ianpaschal/combat-command-game-systems`" .

echo
echo "== Check CI/workflows for install steps/provisioning =="
if [ -d .github/workflows ]; then
  rg -n --hidden -S 'combat-command-game-systems|1\.6\.0\.tgz|file:../|../combat-command-game-systems|npm (ci|install)|pnpm install|yarn install' .github/workflows || true
else
  echo ".github/workflows not found"
fi

echo
echo "== Does the sibling dir exist in this environment? =="
ls -ld ../combat-command-game-systems 2>/dev/null || echo "sibling dir missing"

Repository: ianpaschal/combat-command

Length of output: 21260


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== .github/workflows/ci.yml =="
cat .github/workflows/ci.yml

echo
echo "== Search for referenced tarball filename =="
rg -n --hidden -S "ianpaschal-combat-command-game-systems-1\.6\.0\.tgz|../combat-command-game-systems" .

echo
echo "== package-lock resolved entry =="
rg -n --hidden -S "\"`@ianpaschal/combat-command-game-systems`\": \\{" -n package-lock.json
rg -n --hidden -S "resolved\": \"file:../combat-command-game-systems/ianpaschal-combat-command-game-systems-1\.6\.0\.tgz" package-lock.json

Repository: ianpaschal/combat-command

Length of output: 22223


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package-lock.json top-level reference =="
rg -n "\"`@ianpaschal/combat-command-game-systems`\": \"file:../combat-command-game-systems/ianpaschal-combat-command-game-systems-1\\.6\\.0\\.tgz\"" package-lock.json || true
echo
echo "== package-lock.json packages resolved entry (near node_modules/@ianpaschal/combat-command-game-systems) =="
rg -n "node_modules/@ianpaschal/combat-command-game-systems" package-lock.json || true

# Show a small window around the first match for the resolved entry
line=$(rg -n "resolved\": \"file:../combat-command-game-systems/ianpaschal-combat-command-game-systems-1\\.6\\.0\\.tgz" package-lock.json | head -n1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
  start=$((line-5)); end=$((line+10))
  sed -n "${start},${end}p" package-lock.json | cat
else
  echo "No exact resolved entry match found via rg; showing any nearby file:../combat-command-game-systems matches:"
  rg -n "resolved\": \"file:../combat-command-game-systems" package-lock.json || true
fi

echo
echo "== Any CI/provisioning mention of ../combat-command-game-systems =="
rg -n --hidden -S "combat-command-game-systems|file:../combat-command-game-systems" .github/workflows || true

Repository: ianpaschal/combat-command

Length of output: 1273


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package-lock.json top-level reference =="
rg -n "\"`@ianpaschal/combat-command-game-systems`\": \"file:../combat-command-game-systems/ianpaschal-combat-command-game-systems-1\\.6\\.0\\.tgz\"" package-lock.json || true
echo
echo "== package-lock.json packages resolved entry (near node_modules/@ianpaschal/combat-command-game-systems) =="
rg -n "node_modules/@ianpaschal/combat-command-game-systems" package-lock.json || true

# Show a small window around the first match for the resolved entry
line=$(rg -n "resolved\": \"file:../combat-command-game-systems/ianpaschal-combat-command-game-systems-1\\.6\\.0\\.tgz" package-lock.json | head -n1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
  start=$((line-5)); end=$((line+10))
  sed -n "${start},${end}p" package-lock.json | cat
else
  echo "No exact resolved entry match found via rg; showing any nearby file:../combat-command-game-systems matches:"
  rg -n "resolved\": \"file:../combat-command-game-systems" package-lock.json || true
fi

echo
echo "== Any CI/provisioning mention of ../combat-command-game-systems =="
rg -n --hidden -S "combat-command-game-systems|file:../combat-command-game-systems" .github/workflows || true

Repository: ianpaschal/combat-command

Length of output: 1273


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package-lock.json top-level reference =="
rg -n "\"`@ianpaschal/combat-command-game-systems`\": \"file:../combat-command-game-systems/ianpaschal-combat-command-game-systems-1\\.6\\.0\\.tgz\"" package-lock.json || true
echo
echo "== package-lock.json packages resolved entry (near node_modules/@ianpaschal/combat-command-game-systems) =="
rg -n "node_modules/@ianpaschal/combat-command-game-systems" package-lock.json || true

line=$(rg -n "resolved\": \"file:../combat-command-game-systems/ianpaschal-combat-command-game-systems-1\\.6\\.0\\.tgz" package-lock.json | head -n1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
  start=$((line-5)); end=$((line+10))
  echo
  echo "== package-lock.json window around resolved entry =="
  sed -n "${start},${end}p" package-lock.json | cat
else
  echo
  echo "No exact resolved entry match found via rg; showing any nearby file:../combat-command-game-systems matches:"
  rg -n "resolved\": \"file:../combat-command-game-systems" package-lock.json || true
fi

echo
echo "== Any CI/provisioning mention of ../combat-command-game-systems =="
rg -n --hidden -S "combat-command-game-systems|file:../combat-command-game-systems" .github/workflows || true

Repository: ianpaschal/combat-command

Length of output: 1327


🌐 Web query:

@ianpaschal/combat-command-game-systems 1.6.0 npm

💡 Result:

I couldn’t find any authoritative source confirming that the npm package @ianpaschal/combat-command-game-systems exists or that version 1.6.0 is published. What I did find: the GitHub account Ian Paschal has a repository named combat-command-game-systems, but the search results I retrieved did not include the corresponding npm registry page/version details needed to answer your exact query (npm + 1.6.0) with evidence. If you want, paste the npm link (or the package’s npm registry URL) and I’ll verify 1.6.0 contents, release notes, and installation instructions against the official registry and/or repo tags.

Citations:


Fix brittle out-of-repo file: dependency for @ianpaschal/combat-command-game-systems (CI will break).

  • package.json line 35 and package-lock.json resolve @ianpaschal/combat-command-game-systems from file:../combat-command-game-systems/ianpaschal-combat-command-game-systems-1.6.0.tgz.
  • .github/workflows/ci.yml runs npm ci with no step that creates/provides that ../combat-command-game-systems sibling tarball/dir.
  • The referenced tarball/sibling dir is missing in this environment (so installs won’t be reproducible).

Update the dependency strategy so installs don’t depend on a pre-provisioned sibling directory (e.g., use a registry-published version/workspace, or add a CI step to fetch/vendor the tarball before npm ci, and regenerate the lockfile).

🤖 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 `@package.json` at line 35, The package.json currently pins
`@ianpaschal/combat-command-game-systems` to a brittle local tarball via
"file:../combat-command-game-systems/ianpaschal-combat-command-game-systems-1.6.0.tgz"
which breaks CI (npm ci) because the sibling tarball/dir is not provided; change
the dependency strategy by either (A) switching the dependency to a published
registry version (e.g., "`@ianpaschal/combat-command-game-systems`": "1.6.0") and
update package-lock.json accordingly, or (B) convert the repo to a
workspace/local package reference (workspaces config) or (C) add a CI step in
.github/workflows/ci.yml to fetch or create the ../combat-command-game-systems
tarball before running npm ci and then regenerate lockfile so installs are
reproducible; update package.json and package-lock.json to reflect the chosen
approach and ensure npm ci succeeds.

return <TeamYankeeV2ListMetaFields {...props} />;
}

throw new Error(`Could not find <ListMetaFields/> for game system ${gameSystem}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify where ListMetaFields is used and whether gameSystem is always present in those form shapes.
rg -n -C3 '\b<ListMetaFields\b' --type=tsx
rg -n -C3 '\bgameSystem\b' src/components/ListForm/ListForm.schema.ts src/components/TournamentRegistrationForm/TournamentRegistrationForm.schema.ts src/components/MatchResultForm/MatchResultForm.schema.ts

Repository: ianpaschal/combat-command

Length of output: 5296


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print ListMetaFields around the throw
ls -la src/components/ListMetaFields/ListMetaFields.tsx
nl -ba src/components/ListMetaFields/ListMetaFields.tsx | sed -n '1,120p'

# Search for ListMetaFields usage and capture surrounding lines
rg -n -C3 "ListMetaFields" src -g"*.tsx" -g"*.ts"

# Find where gameSystem prop is passed into ListMetaFields
rg -n -C3 "ListMetaFields[^\\n]*gameSystem" src -g"*.tsx" -g"*.ts"
rg -n -C3 "<ListMetaFields[^>]*gameSystem" src -g"*.tsx" -g"*.ts"

# Also search for prop definitions in ListMetaFields
rg -n "export (type|interface) Props|interface Props|type Props|gameSystem" src/components/ListMetaFields/ListMetaFields.tsx

Repository: ianpaschal/combat-command

Length of output: 227


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print ListMetaFields around the throw
ls -la src/components/ListMetaFields/ListMetaFields.tsx
nl -ba src/components/ListMetaFields/ListMetaFields.tsx | sed -n '1,160p'

# Search for ListMetaFields usage and capture surrounding lines
rg -n -C3 "ListMetaFields" src -g"*.tsx" -g"*.ts"

# Find where gameSystem prop is passed into ListMetaFields
rg -n -C3 "ListMetaFields[^\n]*gameSystem" src -g"*.tsx" -g"*.ts" || true
rg -n -C3 "<ListMetaFields[^>]*gameSystem" src -g"*.tsx" -g"*.ts" || true

# Also search for prop definitions in ListMetaFields
rg -n "export (type|interface) Props|interface Props|type Props|gameSystem" src/components/ListMetaFields/ListMetaFields.tsx

Repository: ianpaschal/combat-command

Length of output: 227


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect ListMetaFields.tsx (line-numbered via cat -n)
sed -n '1,200p' src/components/ListMetaFields/ListMetaFields.tsx
echo "----"
cat -n src/components/ListMetaFields/ListMetaFields.tsx | sed -n '1,200p'

# Find all ListMetaFields usage
rg -n "ListMetaFields" src -g"*.tsx" -g"*.ts"

# Find call sites passing gameSystem prop into ListMetaFields
rg -n "<ListMetaFields[^>]*gameSystem=" src -g"*.tsx"
rg -n "gameSystem={[^}]*}[^>]*>\\s*</?ListMetaFields" src -g"*.tsx" || true
rg -n "<ListMetaFields[^>]*\\bgameSystem\\b" src -g"*.tsx"

# Show ListMetaFields props/type exports (if any)
rg -n "gameSystem" src/components/ListMetaFields/ListMetaFields.tsx

Repository: ianpaschal/combat-command

Length of output: 5398


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect call sites to see how RHF is initialized and what field path holds gameSystem.
# Print relevant sections around ListMetaFields usage in ListForm.tsx and TournamentRegistrationForm.tsx.
sed -n '1,220p' src/components/ListForm/ListForm.tsx
echo "----"
sed -n '220,420p' src/components/ListForm/ListForm.tsx

echo "==== TournamentRegistrationForm.tsx ===="
sed -n '1,280p' src/components/TournamentRegistrationForm/TournamentRegistrationForm.tsx
echo "----"
sed -n '280,520p' src/components/TournamentRegistrationForm/TournamentRegistrationForm.tsx

# Inspect ListForm.schema and TournamentRegistrationForm.schema around gameSystem/defaults.
echo "==== ListForm.schema.ts ===="
sed -n '1,220p' src/components/ListForm/ListForm.schema.ts
echo "==== TournamentRegistrationForm.schema.ts ===="
sed -n '1,260p' src/components/TournamentRegistrationForm/TournamentRegistrationForm.schema.ts

# Inspect ListMetaFields.schema (if it defines/validates gameSystem naming).
echo "==== ListMetaFields.schema.ts ===="
sed -n '1,260p' src/components/ListMetaFields/ListMetaFields.schema.ts

# Inspect game system meta field components to see how they use namePrefix and where gameSystem lives.
echo "==== FlamesOfWarV4ListMetaFields.tsx ===="
sed -n '1,260p' src/components/ListMetaFields/gameSystems/FlamesOfWarV4ListMetaFields/FlamesOfWarV4ListMetaFields.tsx

echo "==== TeamYankeeV2ListMetaFields.tsx ===="
sed -n '1,260p' src/components/ListMetaFields/gameSystems/TeamYankeeV2ListMetaFields/TeamYankeeV2ListMetaFields.tsx

# Search for how namePrefix is used to build field names, and whether those components reference gameSystem.
rg -n "\\bnamePrefix\\b|watch\\(|useWatch\\(|\\bgameSystem\\b" src/components/ListMetaFields/gameSystems -g"*.ts*" -C2

Repository: ianpaschal/combat-command

Length of output: 33240


Avoid render-time throw for missing/unknown gameSystem in ListMetaFields; render a safe fallback and select the gameSystem field using the namePrefix scope.

In src/components/TournamentRegistrationForm/TournamentRegistrationForm.tsx, ListMetaFields is rendered per list with namePrefix={lists.${index}.meta}, but ListMetaFields decides which sub-component to render using watch('gameSystem') (form root). That field isn’t defined at the tournament form root, so the current code can hit the render-time throw and crash the form tree.

throw new Error(`Could not find <ListMetaFields/> for game system ${gameSystem}`);
🤖 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/ListMetaFields/ListMetaFields.tsx` at line 24, ListMetaFields
currently watches the wrong form field and throws during render when gameSystem
is undefined; change it to read the gameSystem from the provided namePrefix
scope (watch(`${namePrefix}.gameSystem`) or equivalent using the namePrefix
prop) and remove the render-time throw — instead render a safe fallback (null or
a placeholder) when the scoped gameSystem is missing or unknown, and only render
the matched sub-component when a valid gameSystem is found; update references
inside ListMetaFields to use namePrefix and the scoped field name rather than
the root 'gameSystem'.

Comment on lines +38 to +45
const userId = watch(`player${index}UserId`);
const tournamentPairingId = watch('tournamentPairingId');
const gameSystem = watch('gameSystem');
const selectedListId = watch(`player${index}ListId`);

const competitor = tournamentPairing?.[`tournamentCompetitor${index}`];
const registration = (competitor?.registrations ?? []).find((r) => r.userId === userId);
const allLists = registration?.lists ?? [];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear stale list selection when player/registration changes.

player{index}ListId is never reset when player{index}UserId changes. A list from a previous user can remain selected and be submitted.

Proposed fix
+import { MouseEvent, useEffect } from 'react';
...
   const selectedList = allLists.find((l) => l._id === selectedListId) ?? null;
+
+  useEffect(() => {
+    if (selectedListId && !selectedList) {
+      setValue(`player${index}ListId`, null, { shouldDirty: true });
+    }
+  }, [selectedListId, selectedList, setValue, index]);

Also applies to: 52-53

🤖 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/MatchResultForm/components/MatchResultListField/MatchResultListField.tsx`
around lines 38 - 45, When the watched player userId (userId) or registration
changes, ensure the previously selected list (player${index}ListId /
selectedListId) is cleared so a list from a prior user cannot be submitted: add
an effect that depends on userId, registration (and optionally
tournamentPairingId/gameSystem) which checks whether selectedListId exists in
the current registration's allLists (registration or allLists) and if not calls
setValue(`player${index}ListId`, undefined) or
resetField(`player${index}ListId`) to clear it; reference the existing watch,
selectedListId, registration, allLists and use setValue/resetField inside that
effect.

Comment on lines +72 to +76
<ListForm
hideFileUpload
forcedValues={{ gameSystem }}
onSubmit={(data) => createList(data)}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scope created lists to the selected registration.

Line 72–76 creates a list without passing the current registration id, but Line 44–47/52 only read lists from registration.lists. This can leave newly created lists outside the selectable/editable set for this field.

Proposed fix
       <ListForm
         hideFileUpload
-        forcedValues={{ gameSystem }}
+        forcedValues={{
+          gameSystem,
+          tournamentRegistrationId: registration?._id,
+        }}
         onSubmit={(data) => createList(data)}
       />

Also applies to: 44-47, 52-52

🤖 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/MatchResultForm/components/MatchResultListField/MatchResultListField.tsx`
around lines 72 - 76, The ListForm is creating lists without attaching the
current registration id, so newly created lists may not appear in
registration.lists; update the ListForm usage to include the registration id in
both forcedValues and the onSubmit payload by adding registration.id to
forcedValues (e.g. forcedValues={{ gameSystem, registrationId: registration.id
}}) and passing that registrationId into createList (onSubmit={(data) =>
createList({ ...data, registrationId: registration.id })}), updating references
to ListForm, forcedValues, createList and registration accordingly so created
lists are scoped to the selected registration.

Comment on lines +224 to +235
{listFields.map((field, index) => (
<div className={className}>
<ListMetaFields namePrefix={`lists.${index}.meta`} />
<Button
disabled={disabled}
icon={<Trash />}
intent="danger"
variant="ghost"
onClick={(e) => handleRemoveList(index, e)}
/>
</div>
))}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify ListMetaFields watches gameSystem from form context
ast-grep --pattern $'const { watch } = useFormContext();
$$$
const gameSystem = watch($$$);'

Repository: ianpaschal/combat-command

Length of output: 51


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Locate ListMetaFields and inspect watch('gameSystem') usage
LIST_META_FILE=$(fd -a "ListMetaFields.tsx" .)
echo "ListMetaFields file(s):"
echo "$LIST_META_FILE"

echo "---- ListMetaFields: lines around watch/useFormContext ----"
rg -n "useFormContext|watch\(" "$LIST_META_FILE" || true
# print a small window around the gameSystem watcher if present
if rg -n "gameSystem" "$LIST_META_FILE" >/dev/null; then
  GAME_LINE=$(rg -n "gameSystem" "$LIST_META_FILE" | head -n1 | cut -d: -f1)
  START=$((GAME_LINE-10))
  END=$((GAME_LINE+15))
  sed -n "${START},${END}p" "$LIST_META_FILE" | nl -ba -w3 -s':'
fi

# 2) Inspect TournamentRegistrationForm: look for gameSystem in schema/defaults/fields
FORM_FILE="src/components/TournamentRegistrationForm/TournamentRegistrationForm.tsx"
if [ ! -f "$FORM_FILE" ]; then
  echo "Missing expected file: $FORM_FILE" >&2
  exit 1
fi

echo "---- TournamentRegistrationForm: occurrences of gameSystem ----"
rg -n "gameSystem" "$FORM_FILE" || true

# Print likely schema/defaults area (first 260 lines to cover new field array work)
echo "---- TournamentRegistrationForm: first 280 lines (context for schema/defaults) ----"
sed -n '1,280p' "$FORM_FILE" | nl -ba -w3 -s':'

# 3) Inspect the specific lists rendering to confirm key/className
echo "---- TournamentRegistrationForm: lines 200-260 (lists section) ----"
sed -n '200,260p' "$FORM_FILE" | nl -ba -w3 -s':'

Repository: ianpaschal/combat-command

Length of output: 1453


🏁 Script executed:

#!/bin/bash
set -euo pipefail

LIST_META="src/components/ListMetaFields/ListMetaFields.tsx"
FORM_FILE="src/components/TournamentRegistrationForm/TournamentRegistrationForm.tsx"

# 1) Does any file watch('gameSystem')?
echo "---- rg watch('gameSystem') ----"
rg -n "watch\\(['\"]gameSystem['\"]\\)" src/components || true

# 2) Inspect ListMetaFields.tsx fully if reasonably small; otherwise targeted sections
echo "---- wc -l $LIST_META ----"
wc -l "$LIST_META"

echo "---- ListMetaFields.tsx: lines 1-120 ----"
sed -n '1,120p' "$LIST_META" | cat -n

echo "---- ListMetaFields.tsx: lines 120-260 ----"
sed -n '120,260p' "$LIST_META" | cat -n

# 3) Inspect TournamentRegistrationForm relevant parts around where gameSystem appears
echo "---- TournamentRegistrationForm.tsx: rg gameSystem ----"
rg -n "gameSystem" "$FORM_FILE" || true

echo "---- TournamentRegistrationForm.tsx: lines 120-220 ----"
sed -n '120,220p' "$FORM_FILE" | cat -n

echo "---- TournamentRegistrationForm.tsx: lines 200-250 (lists rendering) ----"
sed -n '200,250p' "$FORM_FILE" | cat -n

Repository: ianpaschal/combat-command

Length of output: 8206


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FORM_FILE="src/components/TournamentRegistrationForm/TournamentRegistrationForm.tsx"

echo "---- wc -l $FORM_FILE ----"
wc -l "$FORM_FILE"

echo "---- TournamentRegistrationForm.tsx: top 220 lines (form/schema init) ----"
sed -n '1,220p' "$FORM_FILE" | cat -n

echo "---- TournamentRegistrationForm.tsx: search for useForm / defaultValues / setValue(gameSystem) ----"
rg -n "useForm\\(|defaultValues|setValue\\(|register\\(|FormData" "$FORM_FILE" || true
rg -n "setValue\\(['\"]gameSystem['\"]|defaultValues.*gameSystem|register\\(['\"]gameSystem['\"]" "$FORM_FILE" || true

echo "---- TournamentRegistrationForm.tsx: search for 'lists' field array registration ----"
rg -n "useFieldArray\\(|name:\\s*['\"]lists['\"]|appendList|removeList|listFields" "$FORM_FILE" || true

echo "---- TournamentRegistrationForm.tsx: lines around where form is created (look for useForm call) ----"
FORM_LINE=$(rg -n "useForm\\(" "$FORM_FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${FORM_LINE:-}" ]; then
  START=$((FORM_LINE-60))
  END=$((FORM_LINE+40))
  sed -n "${START},${END}p" "$FORM_FILE" | cat -n
fi

Repository: ianpaschal/combat-command

Length of output: 10144


🏁 Script executed:

#!/bin/bash
set -euo pipefail

SCHEMA_FILE="src/components/TournamentRegistrationForm/TournamentRegistrationForm.schema.ts"

echo "---- ls for schema file (sanity) ----"
ls -la "$(dirname "$SCHEMA_FILE")" | head -n 50

echo "---- wc -l schema file ----"
wc -l "$SCHEMA_FILE"

echo "---- rg gameSystem in schema file ----"
rg -n "gameSystem" "$SCHEMA_FILE" || true

echo "---- rg defaultValues in schema file ----"
rg -n "defaultValues" "$SCHEMA_FILE" || true

echo "---- show likely form data type / schema structure (entire file if small enough) ----"
if [ "$(wc -l < "$SCHEMA_FILE")" -le 220 ]; then
  cat -n "$SCHEMA_FILE"
else
  sed -n '1,220p' "$SCHEMA_FILE" | cat -n
  echo "---- tail ----"
  sed -n '221,440p' "$SCHEMA_FILE" | cat -n
fi

Repository: ianpaschal/combat-command

Length of output: 5107


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect the generic Form wrapper implementation
# Try locating by filename
FORM_COMP=$(fd -a "Form.tsx" src/components | head -n 20 | tr '\n' '\n')
echo "Candidate Form.tsx files:"
echo "$FORM_COMP"

# Look specifically for generic/Form
GENERIC_FORM=$(fd -a "generic/Form.tsx" src/components || true)
if [ -z "${GENERIC_FORM}" ]; then
  GENERIC_FORM=$(fd -a "Form.tsx" src/components/generic | head -n 20 || true)
fi
echo "Using generic form file(s):"
echo "$GENERIC_FORM"

for f in $GENERIC_FORM; do
  echo "---- $f (first 200 lines) ----"
  sed -n '1,220p' "$f" | cat -n
done

# 2) Inspect TournamentRegistrationForm.module.scss for Lists styling
SCSS="src/components/TournamentRegistrationForm/TournamentRegistrationForm.module.scss"
echo "---- rg Lists in scss ----"
rg -n "Lists" "$SCSS" || true
echo "---- scss around lists definitions ----"
rg -n "Lists" "$SCSS" -n | head -n 30 || true
# print the whole file if it's small
wc -l "$SCSS"
if [ "$(wc -l < "$SCSS")" -le 220 ]; then
  sed -n '1,260p' "$SCSS" | cat -n
fi

Repository: ianpaschal/combat-command

Length of output: 4887


Fix runtime crash in ListMetaFields + add missing key for list items

  • src/components/ListMetaFields/ListMetaFields.tsx watches gameSystem from RHF context and throws if it’s not GameSystem.FlamesOfWarV4 or GameSystem.TeamYankeeV2. src/components/TournamentRegistrationForm/TournamentRegistrationForm.schema.ts / defaultValues and TournamentRegistrationForm.tsx don’t define or set a gameSystem field, so watch('gameSystem') will be undefined and crash when rendering the Lists section.

    • Fix: add a gameSystem form field (defaulted from tournament.gameSystem) and include it in the schema, or pass tournament.gameSystem into ListMetaFields and stop watching it from context.
  • src/components/TournamentRegistrationForm/TournamentRegistrationForm.tsx (listFields.map, ~224-235): missing key on the mapped <div> and it applies the outer form className to each list row.

    • Fix: use key={field.id} on the root element and switch to a dedicated list-row class if styling is needed.
🤖 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/TournamentRegistrationForm/TournamentRegistrationForm.tsx`
around lines 224 - 235, The Lists rendering crashes because ListMetaFields
relies on watch('gameSystem') but the TournamentRegistrationForm
defaultValues/schema don't provide gameSystem, so either add a gameSystem form
field (initialize it from tournament.gameSystem in defaultValues and include it
in the form schema) or stop relying on RHF context by passing
tournament.gameSystem as a prop into ListMetaFields and update ListMetaFields to
use that prop instead of watch('gameSystem'); additionally fix the list map in
TournamentRegistrationForm (the listFields.map block) by adding key={field.id}
to the root <div> and stop reusing the outer form className for each row (use a
dedicated list-row className instead) so styling and React keys are correct.

/>
</div>
{listFields.map((field, index) => (
<div className={className}>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Incorrect className and missing key prop.

Two issues on this line:

  1. className={className} applies the root form's className prop to each list item div—likely unintended
  2. Missing key prop will cause React reconciliation issues
Proposed fix
-          {listFields.map((field, index) => (
-            <div className={className}>
+          {listFields.map((field, index) => (
+            <div key={field.id} className={styles.TournamentRegistrationForm_ListItem}>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className={className}>
<div key={field.id} className={styles.TournamentRegistrationForm_ListItem}>
🤖 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/TournamentRegistrationForm/TournamentRegistrationForm.tsx` at
line 225, The div inside TournamentRegistrationForm that renders each list item
erroneously uses the root prop className (className={className}) and lacks a
React key; update the map render so the per-item div does not reuse the root
className (use a specific item class like itemClassName or a static className
for list items, or remove className from the item and keep className on the root
container) and add a unique key prop (e.g., key={participant.id} or key={index})
to that div in the participants.map (or equivalent) callback to fix
reconciliation issues.

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