Skip to content

Improve validation#48

Merged
ntietje1 merged 15 commits intomainfrom
dev
Apr 17, 2026
Merged

Improve validation#48
ntietje1 merged 15 commits intomainfrom
dev

Conversation

@ntietje1
Copy link
Copy Markdown
Owner

@ntietje1 ntietje1 commented Apr 17, 2026

Summary by CodeRabbit

  • New Features

    • Centralized, stricter validation and normalization for usernames, campaign slugs, and sidebar item names; canonical slugs and typed colors/icons.
    • Better UI responses for malformed invite links and clearer validation errors.
  • Bug Fixes

    • Fixes for name/slug edge cases (whitespace, casing, hyphens) and consistent color/icon normalization.
    • Improved map image load/error handling and more reliable recent/last-selected item behavior.
  • Refactor

    • Validation and helpers reorganized into focused modules and unified frontend/backend flows.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

Warning

Rate limit exceeded

@ntietje1 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 29 minutes and 25 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 29 minutes and 25 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6b715d1f-e7d3-4bd4-af79-931eb29695e8

📥 Commits

Reviewing files that changed from the base of the PR and between 69fc0ec and 430d262.

📒 Files selected for processing (11)
  • .github/workflows/ci.yml
  • convex/_test/factories.helper.ts
  • convex/sidebarItems/__tests__/sidebarItemValidation.test.ts
  • convex/sidebarItems/functions/moveSidebarItem.ts
  • convex/sidebarItems/functions/treeOperations.ts
  • convex/sidebarItems/validation/move.ts
  • convex/sidebarItems/validation/orchestration.ts
  • convex/sidebarItems/validation/parent.ts
  • src/features/campaigns/hooks/useCampaign.ts
  • src/features/settings/components/tabs/campaign-people/__tests__/people-tab.test.tsx
  • src/features/sidebar/components/forms/sidebar-item-edit-dialog.tsx
📝 Walkthrough

Walkthrough

Introduces branded/domain validators (usernames, campaign slugs, sidebar item names/slugs, colors, icons), refactors sidebar validation into focused modules (access, name, slug, color, icon, parent, move, orchestration), updates Convex backend call sites/types, and tightens frontend types and input normalization across many components and tests.

Changes

Cohort / File(s) Summary
VS Code config
\.vscode/settings.json
Disabled format-on-save and save-time auto-fix actions.
Core slug/validation & zod helpers
convex/common/slug.ts, convex/common/zod.ts, convex/common/async.ts
Added canonical slug helpers, branded-string factory, parse/validate/assert helpers, findUniqueSlug enhancements, and parse-or-throw Zod helper plus MaybePromise/isPromiseLike utility.
User & campaign validation/types
convex/users/validation.ts, convex/users/*, convex/campaigns/validation.ts, convex/campaigns/*
Introduced Username and CampaignSlug branded types/validators and require/parse/assert helpers; replaced ad-hoc slugify/trim logic and tightened schema/arg types across users and campaigns.
Sidebar validation reorganization
deleted convex/sidebarItems/validation.ts, convex/sidebarItems/sharedValidation.ts; added convex/sidebarItems/validation/{access,name,slug,color,icon,parent,move,orchestration}.ts
Removed monolithic validator, split into focused modules (access, name, slug, color, icon, parent/path, move, orchestration) with branded SidebarItem types, coercion/require helpers, and prepare/rename/create orchestration functions.
Sidebar types & schema
convex/sidebarItems/schema/*, convex/sidebarItems/types/*, convex/sidebarItems/schema/validators.ts
Switched DB table validators and exported types to branded SidebarItem types (name/slug/icon/color), added normalization/enhancement pipeline types and updated Convex value validators.
Orchestration & create/rename flows
convex/sidebarItems/validation/orchestration.ts, and create/update functions across convex/*/functions/* (notes, files, folders, gameMaps, canvases, etc.)
Centralized prepareSidebarItemCreate/prepareSidebarItemRename/findUniqueSidebarItemSlug; replaced local validate+findUniqueSlug logic with orchestration and tightened function parameter/return types to branded types.
Access helper relocation
many backend files (e.g., convex/*/functions/* such as blockShares, blocks, bookmarks, documentSnapshots, gameMaps, yjsSync, etc.)
Updated imports to convex/sidebarItems/validation/access for checkItemAccess/requireItemAccess; behavior preserved (minor return-type tweak in one function).
Frontend typing & wiring
src/features/sidebar/*, src/features/campaigns/*, src/features/editor/*, src/features/dnd/*, src/features/search/*, src/features/settings/*
Replaced string-based props/route params with branded types (SidebarItemSlug/Name/IconName/Color, Username, CampaignSlug), added parse/require normalization, guarded queries on parsed route identity, and updated many hooks/components to use typed/validated values.
Client utilities refactor
src/features/sidebar/utils/*, src/shared/utils/category-icons.ts, src/features/sidebar/utils/sidebar-item-utils.ts
Delegated color/icon/slug utilities to Convex validation modules, removed local hex helpers, adopted typed icon map and canonical defaults.
Tests & factories
convex/*/__tests__/*, src/test/factories/*, convex/_test/factories.helper.ts, many test updates
Updated tests and factories to assert/parse branded slugs/usernames, adjusted expectations for new validation rules, added/expanded validation tests (slug/name/color/icon), and updated test helpers.
Sidebar runtime helpers & tree ops
convex/sidebarItems/functions/* (enhanceSidebarItem, getSidebarItemWithContent, treeOperations, collectDescendants, etc.)
Added normalization step that asserts name/icon/color/slug on enhanced items, tightened row types (AnySidebarItemRow), adjusted overloads and type-aware getSidebarItemWithContent behavior when expectedType is provided.
Frontend UX/ux-state typing changes
src/features/sidebar/stores/*, useRecentItems, editor/navigation/hooks, forms and pickers
Updated store/hooks/types to use branded slugs/names/icon/color; adapted forms and components to coerce/require values and to pass undefined vs null where appropriate.
Misc app logic & UX fixes
assorted UI changes (map image loading, remote cursor will-change handling, editor initialization, campaign route parsing/guarding)
Several UI fixes and refactors combined with type tightening; e.g., safer image load state, editor instance lifecycles, and route-based early-returns when identity parsing fails.
CI / dev deps
.github/workflows/ci.yml, package.json
Adjusted CI workflow (fallow action usage, env var) and bumped devDependency fallow version.

Sequence Diagram(s)

(omitted — changes are broad and distributed across many modules without a single new multi-component sequential flow suitable for a compact diagram)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Edit history #35 — Overlaps edit-history/document-snapshot and related sidebar-item flows; likely touches many of the same create/update hooks and snapshot logic.
  • add canvases #33 — Related canvas and sidebar-item surface changes; shares schema/validation edits and canvas create/update flows.
  • Testing #26 — Related test infrastructure and CI changes; touches factory helpers and workflow configuration similar to this diff.

Poem

🐇 I hop through names and brand their strings,
I nudge your slugs to proper, tidy things.
Colors lowered, icons named just right—
I plant validation, snug and light.
Hooray! One tidy database spring. 🌿

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 17, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
wizard-archive 430d262 Commit Preview URL

Branch Preview URL
Apr 17 2026, 09:52 PM

@github-actions
Copy link
Copy Markdown

Preview Deployment

Service URL
Frontend https://preview-48.wizardarchive.com

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

Caution

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

⚠️ Outside diff range comments (6)
src/features/dnd/hooks/useFileDropHandler.tsx (1)

205-205: 🧹 Nitpick | 🔵 Trivial

Drop the now-redundant Id<'sidebarItems'> cast.

createItem already returns id: Id<'sidebarItems'> (as reflected by UploadSingleFileResult.id above), so the as Id<'sidebarItems'> assertion here is noise and hides any future type drift in createItem's return shape.

♻️ Proposed cleanup
-    const folderId = result.id as Id<'sidebarItems'>
+    const folderId = result.id
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/dnd/hooks/useFileDropHandler.tsx` at line 205, The explicit cast
on result.id is redundant and should be removed: stop using "as
Id<'sidebarItems'>" when assigning folderId (const folderId = result.id). Rely
on the existing return type from createItem / UploadSingleFileResult to infer
the correct Id type; if TypeScript complains, adjust the variable's declared
type (or the createItem return type) rather than adding an inline assertion.
Update references to folderId accordingly and remove the cast to avoid hiding
future type drift.
src/features/campaigns/components/campaign-dialog.tsx (2)

189-197: ⚠️ Potential issue | 🟡 Minor

Aggressive slugify on every keystroke can break typing UX.

Running slugify inside onChange normalizes the input on every character. Depending on slugify's rules, this can make certain intermediate states unreachable — e.g., if it strips leading/trailing separators or collapses repeated hyphens, users typing my-cool- will see the trailing - vanish immediately and be unable to continue with my-cool-campaign naturally. It also fights with caret position, since replacing the input value on every keystroke can jump the cursor to the end.

Consider normalizing more leniently while typing (e.g., lowercasing + replacing spaces only) and applying full slugify on blur/submit, which matches how the submission path at lines 83/93 already re-normalizes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/campaigns/components/campaign-dialog.tsx` around lines 189 -
197, The current onChange uses slugify(...) which aggressively normalizes on
every keystroke (Input onChange -> slugify) and breaks typing/caret; change the
onChange to apply a lightweight transformation (e.g., lowercasing and replacing
spaces with single hyphens) before calling field.handleChange, and move the full
slugify(...) call to the onBlur handler (Input onBlur -> call full slugify then
field.handleBlur) and ensure the submit path already calling slugify remains
as-is (so final normalization happens on blur/submit); reference the Input
component's onChange/onBlur handlers, slugify, and
field.handleChange/field.handleBlur when making this change.

62-76: ⚠️ Potential issue | 🟡 Minor

Seed initialSlug with slugify() to ensure min-length compliance.

Math.random().toString(36).substring(2, 15) produces variable-length strings that can fall below the CAMPAIGN_SLUG_MIN_LENGTH of 3 characters. Since the form validates on mount (line 169), it may open with a validation error. Apply slugify() to normalize and ensure the initial value meets validation rules:

-  const [initialSlug] = useState(() => Math.random().toString(36).substring(2, 15))
+  const [initialSlug] = useState(() => slugify(Math.random().toString(36).substring(2, 15)))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/campaigns/components/campaign-dialog.tsx` around lines 62 - 76,
initialSlug is generated with Math.random().toString(36)... which can produce
strings shorter than CAMPAIGN_SLUG_MIN_LENGTH and triggers validation on mount;
replace that generation by passing the random string through slugify and ensure
it meets CAMPAIGN_SLUG_MIN_LENGTH (e.g., generate slug = slugify(random), then
if slug.length < CAMPAIGN_SLUG_MIN_LENGTH pad or regenerate/append characters
until length >= CAMPAIGN_SLUG_MIN_LENGTH) before passing it into the useState
initializer used by form (initialSlug and the useForm default slug).
convex/_test/factories.helper.ts (1)

158-189: 🧹 Nitpick | 🔵 Trivial

Minor: duplicated slug computation/validation in insertSidebarItem.

sidebarItemBase already runs assertSidebarItemSlug(slugify(name)) at line 177; insertSidebarItem then recomputes and re-asserts the same slug at line 223 before overwriting the base slug at line 229. When no overrides.slug is passed, slugify(name) + assertSidebarItemSlug runs twice.

Consider moving slug resolution out of sidebarItemBase (or accepting an optional slug arg there) so it's validated exactly once:

♻️ Suggested refactor
 const sidebarItemBase = (
   campaignId: Id<'campaigns'>,
   creatorProfileId: Id<'userProfiles'>,
   name: string,
+  slug: SidebarItemSlug,
 ): {
   ...
 } & ReturnType<typeof commonFields> => ({
   name,
-  slug: assertSidebarItemSlug(slugify(name)),
+  slug,
   ...
 })
@@
-  const { inheritShares, imageStorageId, storageId, slug, ...sidebarOverrides } = overrides ?? {}
-  const validatedSlug = slug ? assertSidebarItemSlug(slug) : assertSidebarItemSlug(slugify(name))
+  const { inheritShares, imageStorageId, storageId, slug, ...sidebarOverrides } = overrides ?? {}
+  const validatedSlug = assertSidebarItemSlug(slug ?? slugify(name))

   const sharedData = {
-    ...sidebarItemBase(campaignId, creatorProfileId, name),
+    ...sidebarItemBase(campaignId, creatorProfileId, name, validatedSlug),
     type,
     ...sidebarOverrides,
-    slug: validatedSlug,
   }

Also applies to: 219-230

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/_test/factories.helper.ts` around lines 158 - 189, sidebarItemBase
currently runs assertSidebarItemSlug(slugify(name)) and insertSidebarItem
recomputes and re-asserts the slug, causing duplicate validation; change
sidebarItemBase to accept an optional slug parameter (or remove its slug
computation entirely) and have insertSidebarItem compute slug once using
slugify(name) + assertSidebarItemSlug, then pass that slug into sidebarItemBase
so the slug is validated and set in only one place (update references to
sidebarItemBase and insertSidebarItem accordingly).
convex/sidebarItems/functions/treeOperations.ts (1)

62-82: ⚠️ Potential issue | 🟠 Major

Risk: assertSidebarItemName on persisted descendant names can break restore for legacy items.

When restoring a folder, restoreTreeDescendants applies assertSidebarItemName directly to DB-sourced names in the descendant loop (line 73). If any descendant has a name that violates the validation rules (length bounds, forbidden characters, control characters, leading/trailing dots, or insufficient alphanumerics), the assertion will throw mid-traversal, leaving the subtree in an inconsistent state and making those items permanently unrestorable through this path.

Compare this to the parent item restore in moveSidebarItem.ts:62–81, which safely pre-processes the name via deduplicateName before assertion. Descendants lack this protection.

Options to de-risk:

  • Use parseSidebarItemName (the non-throwing variant already in the codebase) and fall back to a sanitized value before calling findUniqueSidebarItemSlug.
  • Or apply deduplicateName to descendant names before validation, matching the pattern used for the parent item.
  • Or confirm via data audit that no pre-existing rows violate the invariant before relying on the hard assertion here.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/sidebarItems/functions/treeOperations.ts` around lines 62 - 82, The
descendant restore loop in restoreTreeDescendants calls assertSidebarItemName on
DB-sourced names (via applyToTree) which can throw and leave the subtree
partially restored; update the loop so it first normalizes/sanitizes names
instead of asserting raw DB values—use parseSidebarItemName to parse without
throwing and if it returns invalid fall back to a sanitized name (or call
deduplicateName as moveSidebarItem does) before passing the result to
findUniqueSidebarItemSlug, then use that slug in the ctx.db.patch; ensure you do
not call assertSidebarItemName directly on persisted descendant names to avoid
throwing mid-traversal.
convex/folders/functions/folderHelpers.ts (1)

22-37: ⚠️ Potential issue | 🟡 Minor

Asymmetric normalization creates lookup misses: line 22 dropped .trim() but line 35 still trims for comparison.

Line 22 changed normalizedName to name.toLowerCase() without trimming, while line 35 still compares against item.name.trim().toLowerCase(). This breaks the lookup if any caller passes a SidebarItemName with leading/trailing whitespace.

The issue: resolveOrCreateFolderPath passes raw path segments from parentTarget.pathSegments (validated as v.array(v.string()), not SidebarItemName) directly into resolveOrCreateChildFolder with a SidebarItemName type annotation. TypeScript does not enforce the branded type's validation invariant at runtime, so untrimmed strings like " foo " can reach findSidebarChildByName. With the missing trim on line 22:

  • normalizedName = " foo ".toLowerCase() = " foo "
  • Comparison: "foo".trim().toLowerCase() = "foo"" foo " → lookup miss
  • Later validation in ensureSidebarItemNameAvailable might reject the name, but by then a duplicate folder could have been processed.

Either restore .trim() on line 22 or validate pathSegments before passing them as SidebarItemName.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/folders/functions/folderHelpers.ts` around lines 22 - 37, The lookup
normalizes the caller string asymmetrically (normalizedName = name.toLowerCase()
without trim) causing mismatches against item.name.trim().toLowerCase(); fix by
trimming when normalizing the incoming name (set normalizedName =
name.trim().toLowerCase()) in the function that performs the lookup (the
normalizedName variable inside findSidebarChildByName / folderHelpers.ts), or
alternatively validate/trim path segments in
resolveOrCreateFolderPath/resolveOrCreateChildFolder before passing them as
SidebarItemName; prefer restoring .trim() on normalizedName to keep
normalization symmetric with item.name.trim().toLowerCase().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.vscode/settings.json:
- Line 4: The workspace setting currently disables formatting on save via the
"editor.formatOnSave": false entry; restore automatic formatting for the
workspace by removing that setting or changing it to "editor.formatOnSave": true
(or move this preference to user settings instead) so contributors keep
consistent formatting and avoid review noise.
- Around line 7-8: The workspace settings currently disable save-time autofixes
by setting "source.fixAll.oxc": "never" and "source.fixAll.eslint": "never";
revert these two keys to their defaults (remove them or set to "prompt" or
"true" per your team's preference) so ESLint and OXC can run fixAll on save for
all contributors, or replace with a documented team-wide rationale if disabling
is required; update the .vscode/settings.json to either delete the
"source.fixAll.oxc" and "source.fixAll.eslint" entries or set them to
"true"/"prompt" and commit the change.

In `@convex/_test/factories.helper.ts`:
- Around line 72-87: The current truthiness checks (e.g., the username
conditional that does ...(username ? { username: assertUsername(username) } :
{})) silently treat empty-string overrides as absent; change these to explicit
undefined checks (username !== undefined) so any supplied value is validated,
and call the validator (assertUsername) on the provided value to fail fast on
empty strings; make the analogous change for slug/assertCampaignSlug in the
other block (lines around the slug handling) so explicit empty overrides are
validated rather than ignored.

In `@convex/campaigns/functions/updateCampaign.ts`:
- Around line 32-41: The guard comparing conflict._id to campaign._id is
redundant because the outer check slug !== campaign.slug guarantees the query
by_slug_dm can't return the same campaign; in updateCampaign replace the
conditional "if (conflict && conflict._id !== campaign._id)" with a simple
existence check "if (conflict)" (keeping the same thrown error and surrounding
logic) so that the ctx.db query result (conflict) alone triggers the CONFLICT
error.

In `@convex/campaigns/mutations.ts`:
- Line 55: Change the truthy check on args.slug to an explicit undefined check
so optional presence (not falsiness) is intended: replace the expression
currently using args.slug ? requireCampaignSlug(args.slug) : undefined with a
check for args.slug !== undefined ? requireCampaignSlug(args.slug) : undefined,
referencing the same requireCampaignSlug call and the args.slug property in the
mutation to ensure empty strings or other falsy-but-present values are still
validated.

In `@convex/campaigns/validation.ts`:
- Around line 50-52: prepareCampaignDescription currently only trims and allows
arbitrarily long strings; add a max-length guard similar to
prepareCampaignName/validateCampaignName. Define a
MAX_CAMPAIGN_DESCRIPTION_LENGTH constant (or reuse project convention), trim the
incoming description, return undefined for empty strings, and if trimmed.length
exceeds the max either throw the same validation error type used by
validateCampaignName or call a new validateCampaignDescription to enforce and
surface the violation; ensure function name prepareCampaignDescription and the
new constant/validator are referenced so callers can locate the change.
- Around line 58-60: The function requireCampaignUsername is just a direct
pass-through to requireUsername and should be removed or given campaign-specific
behavior: either delete requireCampaignUsername and update all call sites to
call requireUsername(username) directly, or implement campaign-specific
validation inside requireCampaignUsername (e.g., enforce DM reservation or other
campaign rules) before delegating to requireUsername; look for references to
requireCampaignUsername and requireUsername to update them accordingly.
- Around line 41-48: prepareCampaignName currently trims the input and then
calls validateCampaignName(trimmed), but validateCampaignName trims again; to
remove redundancy call validateCampaignName with the original name
(validateCampaignName(name)) and keep the existing trimmed = name.trim() for the
return and error flow (throwClientError(ERROR_CODE.VALIDATION_FAILED, error));
update prepareCampaignName to use validateCampaignName(name) so trimming happens
only once inside validateCampaignName while still returning the trimmed value.

In `@convex/canvases/mutations.ts`:
- Around line 33-45: The handler in createCanvas is using redundant "??
undefined" coalesces for iconName and color; remove the trailing "?? undefined"
so that requireOptionalSidebarItemIconName(args.iconName) and
requireOptionalSidebarItemColor(args.color) are assigned directly (in the
handler function that calls createCanvasFn) to match updateCanvas's pattern and
preserve the intended optional/undefined typing for iconName and color.

In `@convex/common/async.ts`:
- Around line 3-9: The type guard isPromiseLike currently narrows to Promise<T>
while only checking for a thenable; update the predicate to match the runtime
check by changing its return type to value is PromiseLike<T> (or, if you truly
need a native Promise, extend the runtime check to also verify methods like
catch and/or finally before narrowing to Promise<T>). Locate isPromiseLike and
either change its signature to return value is PromiseLike<T> or add checks for
typeof (value as { catch?: unknown }).catch === 'function' (and finally if
desired) so the runtime test matches the narrowed type.

In `@convex/common/slug.ts`:
- Around line 98-109: appendSuffix currently returns an empty string when
suffix.length >= maxLength which silently causes findUniqueSlug to fail later;
change appendSuffix to detect this case and throw a descriptive Error (include
suffixNumber, computed suffix length and maxLength) instead of returning '' so
callers (e.g., findUniqueSlug) get a clear failure; update the function body in
appendSuffix to perform this guard and ensure any callers handle or propagate
the thrown error.
- Around line 71-77: The assert function currently throws a plain Error (const
assert = (...) => { ... }) which bypasses the client-error path; update assert
so it either (A) delegates to parseOrThrowClientValidation(...) and throws a
client-classified validation error (using ERROR_CODE.VALIDATION_FAILED) when
called on user input, or (B) if assert is intended only for trusted/DB coercion,
add a clear JSDoc on assert stating it must only be used for trusted/DB values
(and callers like requireUsername/requireCampaignSlug should instead call
parseOrThrowClientValidation). Reference the symbols assert, parse, validate,
fallbackMessage, and parseOrThrowClientValidation when making the change.
- Around line 33-50: In buildCanonicalSlugSchema, avoid passing an empty string
as the refine message when minLength is undefined: either only call the
min-length refine when minLength is defined (i.e., conditionally chain the
.refine that checks minLength) or replace that block with
z.string().min(minLength, `${label} must be at least ${minLength} characters`)
when minLength is provided; update the use around the minLength predicate and
message so no refine is invoked with an empty error string and keep the other
validations (trim, whitespace, CANONICAL_SLUG_PATTERN) intact.
- Around line 122-139: The loop wastes attempts when slugify(name) returns an
empty string; detect if normalized is empty immediately after const normalized =
slugify(name) and fail fast with a clear error (or return a specific result)
instead of iterating MAX_UNIQUE_SLUG_ATTEMPTS. Update the function containing
normalized, using the symbols slugify, appendSuffix, MAX_UNIQUE_SLUG_ATTEMPTS,
isValidCandidate and checkFn to check for !normalized and throw a descriptive
Error like "Cannot generate slug: input normalized to empty" (or an appropriate
domain-specific error) so callers get a clear failure instead of 100 no-ops.

In `@convex/gameMaps/mutations.ts`:
- Line 39: The handler's explicit return type should use the branded type
SidebarItemSlug instead of string: change the Promise return annotation in the
handler at the signature (handler: async (ctx, args): Promise<{ mapId:
Id<'sidebarItems'>; slug: SidebarItemSlug }>) and do the same for the other
handler occurrence around line 66; also add an import for the SidebarItemSlug
type (e.g., import type { SidebarItemSlug } from '...') so the TS compiler
recognizes the branded slug type.
- Around line 42-43: createMap normalizes iconName/color to undefined using
requireOptionalSidebarItemIconName(...) ?? undefined and
requireOptionalSidebarItemColor(...) ?? undefined, while updateMap passes
through nullable values (null) to the helpers; make them consistent by
normalizing in updateMap as well: in the updateMap handler call the same helpers
and append "?? undefined" to both results before passing to updateMapFn (or
similarly ensure
requireOptionalSidebarItemIconName/requireOptionalSidebarItemColor always return
only undefined for absent values and update validators accordingly); reference
the functions createMap, updateMap, requireOptionalSidebarItemIconName,
requireOptionalSidebarItemColor, and updateMapFn when implementing the change.

In `@convex/notes/functions/createNote.ts`:
- Line 35: The function createNote currently declares its return type as {
noteId: Id<'sidebarItems'>; slug: string } but prepared.slug is a branded
SidebarItemSlug; change the return type to { noteId: Id<'sidebarItems'>; slug:
SidebarItemSlug } and return the branded prepared.slug directly so callers no
longer need to re-assert; apply the same change to the sibling creator handlers
createMap, createFile, and createFolder (update their return types to use
SidebarItemSlug and return the branded slug value).

In `@convex/sidebarItems/__tests__/sidebarItemValidation.test.ts`:
- Around line 321-394: Add a unit test asserting that validateLocalSidebarMove
still detects a circular-parent error even when isRestoring is true;
specifically, create a tree where an item's parent points to its child (as in
the existing circular test), call validateLocalSidebarMove with isRestoring:
true and the circular parentId, and expect the same { valid: false, error:
'Cannot move item: circular reference detected' } result to ensure the circular
check runs before the isRestoring shortcut.

In `@convex/sidebarItems/functions/treeOperations.ts`:
- Around line 12-21: Update the SidebarTreeBranchItem type to narrow its fields:
replace type: string with type: SidebarItemType and name: string with name:
SidebarItemName so compile-time checks catch typos and callers can avoid
re-validating; update any related usages of SidebarTreeBranchItem and
ItemOperation signatures as needed, and ensure applyToTree (which compares
against SIDEBAR_ITEM_TYPES.folders) and code that currently calls
assertSidebarItemName (around the current assertSidebarItemName usage) are
adjusted so validation is performed by producers rather than here.

In `@convex/sidebarItems/schema/sidebarItemsTable.ts`:
- Around line 16-20: The schema tightened validators (sidebarItemNameValidator,
sidebarItemSlugValidator, sidebarItemIconNameValidator,
sidebarItemColorValidator) may reject existing rows; either backfill/migrate
existing sidebarItems to conform to these rules or document and add a migration
that normalizes current documents (sanitize names to allowed chars/length,
canonicalize slugs to max 255, map or clear iconName values not in the 32-item
enum, and convert/fix color strings to `#RRGGBB` or `#RRGGBBAA`), and then enable
the new validators; update or add a migration script and a brief migration plan
describing the transformations and verification steps so reads won’t fail after
the validator change.

In `@convex/sidebarItems/types/types.ts`:
- Around line 40-43: Unify the two type helpers by replacing the long
conditional chain in WithContentSidebarItem with an Extract-based mapping like
the concise form used in WithContentBySidebarItemType: use
Extract<AnySidebarItemWithContent, { type: T['type'] }> (referencing
AnySidebarItemWithContent, WithContentBySidebarItemType, and
WithContentSidebarItem) so the type selects the matching item by its type
property instead of enumerating each variant.

In `@convex/sidebarItems/validation/color.ts`:
- Line 1: Run the formatter/linters locally and fix the formatting in this file
by running `vp check --fix`, then re-commit; specifically ensure the import line
for zodToConvex (import { zodToConvex } from 'convex-helpers/server/zod4')
adheres to the project's formatting rules (quotes, spacing, trailing newline)
and re-run `vp check` to confirm the CI error is resolved.

In `@convex/sidebarItems/validation/icon.ts`:
- Around line 67-70: The current validateSidebarItemIconName function returns
string|null (null meaning valid), which is confusing; change its return shape to
a clear discriminated result like { ok: true } | { ok: false; message: string }
(or a boolean + separate error accessor) so callers can't accidentally invert
the check. Update validateSidebarItemIconName to call
sidebarItemIconNameSchema.safeParse(iconName) and return { ok: true } on success
or { ok: false, message: result.error.issues[0]?.message ?? 'Invalid icon' } on
failure, and update any callers to use the new { ok } discriminator.
- Around line 99-110: The function coerceSidebarItemIconNameForInput currently
calls assertSidebarItemIconName which throws a generic Error; replace that call
with the client-validation helper requireSidebarItemIconName so invalid client
input yields a structured validation error (ERROR_CODE.VALIDATION_FAILED)
instead of an internal Error. Keep the existing overload behavior (return
null/undefined when passed null/undefined) and only call
requireSidebarItemIconName(iconName) for the non-null string branch; ensure the
function signature and return types (SidebarItemIconName | null | undefined)
remain unchanged.

In `@convex/sidebarItems/validation/move.ts`:
- Around line 32-52: The circular-parent validation (validateNoCircularParent)
currently runs even when isRestoring is true, but the summary/doc implied
restores skip validation; to make intent explicit, add a brief inline comment
next to the validateNoCircularParent(...) call (or above the conditional)
clarifying that circular-parent checks intentionally apply to restore operations
(and must block restores into cycles) so the behavior and documentation remain
aligned; reference the isRestoring flag and the validateNoCircularParent
function in the comment.

In `@convex/sidebarItems/validation/name.ts`:
- Around line 31-51: The new strict sidebarItemNameValueSchema will cause
runtime ClientValidationError for existing rows; run an audit query to find
sidebarItems whose name violates the schema, and if any exist add a DB migration
that sanitizes legacy names before enabling the validator: for each offending
row trim whitespace, strip leading/trailing dots, remove control characters and
forbidden chars (or replace them with a safe character), and ensure the result
produces a non-null parseSidebarItemSlug(slugify(name)) or otherwise set a safe
fallback name; also update deduplicateName to sanitize the base name (call the
same normalization used in the migration) before appending numeric suffixes and
keep assertSidebarItemName usage unchanged so runtime validation matches the
migration.

In `@convex/sidebarItems/validation/orchestration.ts`:
- Around line 167-184: The catch block around findUniqueSlug /
assertSidebarItemSlug loses the original error; update the handler in the
slug-generation flow to preserve and propagate known validation errors or attach
the original error as cause when wrapping: if the caught error is a
ClientValidationError (or similar expected error from
findUniqueSlug/assertSidebarItemSlug) rethrow it unchanged, otherwise call
throwClientError(ERROR_CODE.VALIDATION_FAILED, 'Failed to generate a valid slug
for this item', { cause: error }) (or equivalent API that preserves cause), and
remove the plain console.error logging in favor of using the preserved cause so
stack and structured data from findUniqueSlug/assertSidebarItemSlug (and
checkSlugConflict/validateSidebarItemSlug) remain available for debugging.
- Around line 118-135: validateSidebarMove currently calls
assertSidebarItemName(item.name) which runs runtime validation and can throw for
legacy DB rows; instead avoid re-validating here by passing the raw string as
the branded type or by changing the callee to accept a plain string. Update
validateSidebarMove to call ensureSidebarItemNameAvailable with name: item.name
as SidebarItemName (non-throwing cast) or change
ensureSidebarItemNameAvailable's name parameter from SidebarItemName to string
and leave the argument as item.name; references to checkNameConflict remain
unchanged since it accepts string.

In `@convex/sidebarItems/validation/parent.ts`:
- Around line 292-305: The function currently returns parentId: null when
resolvedParent === VIRTUAL_PARENT which can be misused; change the return shape
to a discriminated result by adding a boolean like resolved: false (or
parentIsVirtual: true) when resolvedParent === VIRTUAL_PARENT and keep parentId
absent or undefined in that branch, and resolved: true plus parentId and
siblings in the normal branch; update callers such as validateCreateItemLocally
to read the new discriminant (resolved or parentIsVirtual) before using parentId
or siblings so future callers cannot accidentally treat null as the campaign
root.

In `@convex/sidebarItems/validation/slug.ts`:
- Around line 10-33: Add concise one-line JSDoc comments above both
validateSidebarItemSlug and validateItemSlug clarifying their contracts and
return shapes: state that validateSidebarItemSlug returns the raw validator
result (string | null) while validateItemSlug returns a ValidationResult ({
valid: boolean, error?: string }). Place the comments immediately above the
exported constants/function declarations (validateSidebarItemSlug,
validateItemSlug) so readers can quickly see the difference without searching;
keep wording consistent with other patterns like
validateSidebarItemName/validateItemName and do not change existing function
logic or signatures (leave parseOrThrowClientValidation and
sidebarItemSlugSchema references untouched).

In `@convex/users/functions/getUserProfile.ts`:
- Around line 7-12: toUserProfileFromDb currently calls assertUsername which
will throw for legacy usernames and break getUserProfile,
getUserProfileByUsername, getUserProfileById and toAuthenticatedProfile; either
confirm a migration/backfill exists or replace the strict call with a soft
normalization routine (e.g., create safeNormalizeUsername used by
toUserProfileFromDb and toAuthenticatedProfile) that catches assertUsername
failures, logs the offending profile/username, coerces into a valid form (trim,
toLowerCase, remove/replace disallowed chars, truncate to 30) and returns the
coerced value instead of throwing, ensuring downstream callers no longer crash
while preserving logs for a later backfill.

In `@convex/users/mutations.ts`:
- Around line 6-12: The handler for updateUsername redundantly re-validates and
normalizes input: remove the requireUsername(args.username) call and any
subsequent unreachable normalization (slugify or normalizeUsernameInput) so the
handler uses the already-validated args.username directly; update the
updateUsername handler to operate on args.username and remove any now-unused
imports or variables (requireUsername, slugify, normalizeUsernameInput) to keep
the code clean.

In `@src/features/campaigns/components/join-campaign-page.tsx`:
- Around line 25-26: The module currently uses non-null assertions on
parseUsername('placeholder') and parseCampaignSlug('placeholder')
(PLACEHOLDER_USERNAME, PLACEHOLDER_SLUG) which can silently become null if
validation rules change; replace each "parseX('placeholder')!" with a runtime
check that throws a clear Error when the parse function returns null (e.g.,
const v = parseX('placeholder'); if (!v) throw new Error('...');) so failures
surface at module load time and keep references to the same symbols
(parseUsername, parseCampaignSlug, PLACEHOLDER_USERNAME, PLACEHOLDER_SLUG) to
locate the change.

In `@src/features/search/hooks/use-recent-items.ts`:
- Around line 21-48: The isRecentEntry type-guard duplicates parseRecentEntry
and discards the parsed slug; remove isRecentEntry and update useRecentItems to
call parseRecentEntry for each raw entry (use parsedEntries =
rawArray.flatMap(entry => { const p = parseRecentEntry(entry); return p ? [p] :
[] }) or equivalent) instead of filter(isRecentEntry) so you reuse parsing logic
and keep the validated SidebarItemSlug; also delete or stop referencing
isRecentEntry wherever else used and ensure parseEntries remains the single
source of truth for entry validation.

In `@src/features/sidebar/hooks/useCreateSidebarItem.ts`:
- Line 105: The client-side calls to assertSidebarItemSlug in
useCreateSidebarItem.ts are redundant because the server already validates slug
via sidebarItemSlugValidator; remove the runtime assertions in the return
statements (e.g., the return producing { id: noteId, slug:
assertSidebarItemSlug(slug), type: args.type } and the similar occurrences at
the other noted lines) and replace them with a simple type cast or a no-op
branding helper (e.g., toSidebarItemSlug that only narrows the type without
re-checking) so the value retains the branded SidebarItemSlug type without the
unnecessary runtime cost.
- Around line 81-88: The current order calls
assertSidebarItemName(args.name.trim()) before validateCreateItem, causing an
exception to be thrown for empty/whitespace names instead of returning the
structured { valid, error } from validateCreateItem; change the order to first
call validateCreateItem with the trimmed name (e.g., validateCreateItem({
...args, name: args.name.trim() })), check nameResult.valid and throw
nameResult.error if invalid, then call assertSidebarItemName on the trimmed
value to produce normalizedName for further use (referencing
assertSidebarItemName, validateCreateItem, nameResult, and normalizedName).

In `@src/features/sidebar/hooks/useEditSidebarItem.ts`:
- Around line 98-102: The current code calls assertSidebarItemName(name.trim())
which can throw before validation.validateName runs; change the flow to first
trim the input, then call validation.validateName(normalizedTrim) to get a
non-throwing result and only after it passes use a safe parser
(parseSidebarItemName) or convert to the branded type; specifically, replace the
direct assertSidebarItemName usage in useEditSidebarItem with a non-throwing
parse or defer branding until after validation.validateName returns valid, and
apply the same pattern for response-side checks (replace
assertSidebarItemSlug(res.slug) with a safe parse or guarded validation where
appropriate).

In `@src/features/sidebar/utils/__tests__/validate-search.test.ts`:
- Around line 46-58: Add a test asserting that overly long slugs are rejected by
validateSearch (and thus parseSidebarItemSlug) by constructing a slug longer
than the enforced maximum and expecting validateSearch({ item: longSlug })
toEqual({}) to lock in the upper bound; reference validateSearch and
parseSidebarItemSlug when adding this case to
src/features/sidebar/utils/__tests__/validate-search.test.ts so future changes
to the max length will be caught.

---

Outside diff comments:
In `@convex/_test/factories.helper.ts`:
- Around line 158-189: sidebarItemBase currently runs
assertSidebarItemSlug(slugify(name)) and insertSidebarItem recomputes and
re-asserts the slug, causing duplicate validation; change sidebarItemBase to
accept an optional slug parameter (or remove its slug computation entirely) and
have insertSidebarItem compute slug once using slugify(name) +
assertSidebarItemSlug, then pass that slug into sidebarItemBase so the slug is
validated and set in only one place (update references to sidebarItemBase and
insertSidebarItem accordingly).

In `@convex/folders/functions/folderHelpers.ts`:
- Around line 22-37: The lookup normalizes the caller string asymmetrically
(normalizedName = name.toLowerCase() without trim) causing mismatches against
item.name.trim().toLowerCase(); fix by trimming when normalizing the incoming
name (set normalizedName = name.trim().toLowerCase()) in the function that
performs the lookup (the normalizedName variable inside findSidebarChildByName /
folderHelpers.ts), or alternatively validate/trim path segments in
resolveOrCreateFolderPath/resolveOrCreateChildFolder before passing them as
SidebarItemName; prefer restoring .trim() on normalizedName to keep
normalization symmetric with item.name.trim().toLowerCase().

In `@convex/sidebarItems/functions/treeOperations.ts`:
- Around line 62-82: The descendant restore loop in restoreTreeDescendants calls
assertSidebarItemName on DB-sourced names (via applyToTree) which can throw and
leave the subtree partially restored; update the loop so it first
normalizes/sanitizes names instead of asserting raw DB values—use
parseSidebarItemName to parse without throwing and if it returns invalid fall
back to a sanitized name (or call deduplicateName as moveSidebarItem does)
before passing the result to findUniqueSidebarItemSlug, then use that slug in
the ctx.db.patch; ensure you do not call assertSidebarItemName directly on
persisted descendant names to avoid throwing mid-traversal.

In `@src/features/campaigns/components/campaign-dialog.tsx`:
- Around line 189-197: The current onChange uses slugify(...) which aggressively
normalizes on every keystroke (Input onChange -> slugify) and breaks
typing/caret; change the onChange to apply a lightweight transformation (e.g.,
lowercasing and replacing spaces with single hyphens) before calling
field.handleChange, and move the full slugify(...) call to the onBlur handler
(Input onBlur -> call full slugify then field.handleBlur) and ensure the submit
path already calling slugify remains as-is (so final normalization happens on
blur/submit); reference the Input component's onChange/onBlur handlers, slugify,
and field.handleChange/field.handleBlur when making this change.
- Around line 62-76: initialSlug is generated with Math.random().toString(36)...
which can produce strings shorter than CAMPAIGN_SLUG_MIN_LENGTH and triggers
validation on mount; replace that generation by passing the random string
through slugify and ensure it meets CAMPAIGN_SLUG_MIN_LENGTH (e.g., generate
slug = slugify(random), then if slug.length < CAMPAIGN_SLUG_MIN_LENGTH pad or
regenerate/append characters until length >= CAMPAIGN_SLUG_MIN_LENGTH) before
passing it into the useState initializer used by form (initialSlug and the
useForm default slug).

In `@src/features/dnd/hooks/useFileDropHandler.tsx`:
- Line 205: The explicit cast on result.id is redundant and should be removed:
stop using "as Id<'sidebarItems'>" when assigning folderId (const folderId =
result.id). Rely on the existing return type from createItem /
UploadSingleFileResult to infer the correct Id type; if TypeScript complains,
adjust the variable's declared type (or the createItem return type) rather than
adding an inline assertion. Update references to folderId accordingly and remove
the cast to avoid hiding future type drift.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fb486167-7060-40fc-97f5-e0eb9e64fc5e

📥 Commits

Reviewing files that changed from the base of the PR and between 721950e and 484014b.

⛔ Files ignored due to path filters (1)
  • convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (139)
  • .vscode/settings.json
  • convex/_test/factories.helper.ts
  • convex/auth/functions/onCreateUser.ts
  • convex/blockShares/functions/setBlocksShareStatus.ts
  • convex/blockShares/functions/shareBlocks.ts
  • convex/blockShares/functions/unshareBlocks.ts
  • convex/blocks/functions/getBlockWithShares.ts
  • convex/blocks/functions/getBlocksWithShares.ts
  • convex/bookmarks/functions/toggleItemBookmark.ts
  • convex/campaigns/functions/createCampaign.ts
  • convex/campaigns/functions/getCampaign.ts
  • convex/campaigns/functions/joinCampaign.ts
  • convex/campaigns/functions/updateCampaign.ts
  • convex/campaigns/mutations.ts
  • convex/campaigns/queries.ts
  • convex/campaigns/schema.ts
  • convex/campaigns/types.ts
  • convex/campaigns/validation.ts
  • convex/canvases/functions/createCanvas.ts
  • convex/canvases/functions/rollbackCanvas.ts
  • convex/canvases/functions/updateCanvas.ts
  • convex/canvases/mutations.ts
  • convex/common/__tests__/validation.test.ts
  • convex/common/async.ts
  • convex/common/slug.ts
  • convex/common/zod.ts
  • convex/documentSnapshots/functions/getSnapshot.ts
  • convex/documentSnapshots/functions/rollbackToSnapshot.ts
  • convex/editHistory/queries.ts
  • convex/files/functions/createFile.ts
  • convex/files/functions/updateFile.ts
  • convex/files/mutations.ts
  • convex/files/queries.ts
  • convex/folders/__tests__/folders.test.ts
  • convex/folders/functions/createFolder.ts
  • convex/folders/functions/folderHelpers.ts
  • convex/folders/functions/getFolderContentsForDownload.ts
  • convex/folders/functions/resolveOrCreateFolderPath.ts
  • convex/folders/functions/updateFolder.ts
  • convex/folders/mutations.ts
  • convex/functions.ts
  • convex/gameMaps/functions/createItemPin.ts
  • convex/gameMaps/functions/createMap.ts
  • convex/gameMaps/functions/requirePinAccess.ts
  • convex/gameMaps/functions/rollbackGameMap.ts
  • convex/gameMaps/functions/updateMap.ts
  • convex/gameMaps/mutations.ts
  • convex/gameMaps/queries.ts
  • convex/notes/__tests__/createNote.test.ts
  • convex/notes/__tests__/notes.test.ts
  • convex/notes/functions/createNote.ts
  • convex/notes/functions/rollbackNote.ts
  • convex/notes/functions/updateNote.ts
  • convex/notes/mutations.ts
  • convex/notes/queries.ts
  • convex/sidebarItems/__tests__/sidebarItemValidation.test.ts
  • convex/sidebarItems/createParentTarget.ts
  • convex/sidebarItems/functions/claimPreviewGeneration.ts
  • convex/sidebarItems/functions/defaultItemName.ts
  • convex/sidebarItems/functions/enhanceSidebarItem.ts
  • convex/sidebarItems/functions/getSidebarItemBySlug.ts
  • convex/sidebarItems/functions/getSidebarItemWithContent.ts
  • convex/sidebarItems/functions/hardDeleteItem.ts
  • convex/sidebarItems/functions/moveSidebarItem.ts
  • convex/sidebarItems/functions/setPreviewImage.ts
  • convex/sidebarItems/functions/treeOperations.ts
  • convex/sidebarItems/queries.ts
  • convex/sidebarItems/schema/sidebarItemsTable.ts
  • convex/sidebarItems/schema/validators.ts
  • convex/sidebarItems/sharedValidation.ts
  • convex/sidebarItems/types/baseTypes.ts
  • convex/sidebarItems/types/types.ts
  • convex/sidebarItems/validation.ts
  • convex/sidebarItems/validation/access.ts
  • convex/sidebarItems/validation/color.ts
  • convex/sidebarItems/validation/icon.ts
  • convex/sidebarItems/validation/move.ts
  • convex/sidebarItems/validation/name.ts
  • convex/sidebarItems/validation/orchestration.ts
  • convex/sidebarItems/validation/parent.ts
  • convex/sidebarItems/validation/slug.ts
  • convex/sidebarShares/functions/getSidebarItemShares.ts
  • convex/sidebarShares/functions/getSidebarItemWithShares.ts
  • convex/sidebarShares/functions/setAllPlayersPermission.ts
  • convex/sidebarShares/functions/setFolderInheritShares.ts
  • convex/sidebarShares/functions/shareSidebarItem.ts
  • convex/sidebarShares/functions/unshareSidebarItem.ts
  • convex/users/__tests__/users.test.ts
  • convex/users/functions/checkUsernameExists.ts
  • convex/users/functions/getUserProfile.ts
  • convex/users/mutations.ts
  • convex/users/queries.ts
  • convex/users/schema.ts
  • convex/users/types.ts
  • convex/users/validation.ts
  • convex/yjsSync/functions/checkYjsAccess.ts
  • src/features/campaigns/components/campaign-dialog.tsx
  • src/features/campaigns/components/join-campaign-page.tsx
  • src/features/campaigns/contexts/campaign-context.tsx
  • src/features/campaigns/hooks/__tests__/use-campaign.test.tsx
  • src/features/campaigns/hooks/useCampaign.ts
  • src/features/dnd/hooks/useFileDropHandler.tsx
  • src/features/dnd/utils/dnd-registry.ts
  • src/features/editor/components/extensions/link-click-handler.tsx
  • src/features/editor/components/forms/file-form/file-form.tsx
  • src/features/editor/components/forms/map-form/map-form.tsx
  • src/features/editor/components/viewer/map/map-viewer.tsx
  • src/features/editor/components/viewer/map/pin-utils.ts
  • src/features/editor/hooks/useLinkResolver.ts
  • src/features/search/hooks/__tests__/use-recent-items.test.ts
  • src/features/search/hooks/use-recent-items.ts
  • src/features/settings/components/tabs/account-profile/components/username-row.tsx
  • src/features/settings/components/tabs/campaign-people/people-tab.tsx
  • src/features/sidebar/components/forms/color-picker.tsx
  • src/features/sidebar/components/forms/icon-picker.tsx
  • src/features/sidebar/components/forms/sidebar-item-edit-dialog.tsx
  • src/features/sidebar/hooks/useCreateSidebarItem.ts
  • src/features/sidebar/hooks/useEditSidebarItem.ts
  • src/features/sidebar/hooks/useEditorLinkProps.ts
  • src/features/sidebar/hooks/useEditorNavigation.ts
  • src/features/sidebar/hooks/useLastEditorItem.ts
  • src/features/sidebar/hooks/useMoveSidebarItem.ts
  • src/features/sidebar/hooks/useSelectedItem.ts
  • src/features/sidebar/hooks/useSidebarValidation.ts
  • src/features/sidebar/stores/__tests__/sidebar-ui-store.test.ts
  • src/features/sidebar/stores/sidebar-ui-store.ts
  • src/features/sidebar/utils/__tests__/sidebar-create-validation.test.ts
  • src/features/sidebar/utils/__tests__/sidebar-item-utils.test.ts
  • src/features/sidebar/utils/__tests__/sidebar-validation.test.ts
  • src/features/sidebar/utils/__tests__/validate-search.test.ts
  • src/features/sidebar/utils/sidebar-create-validation.ts
  • src/features/sidebar/utils/sidebar-item-utils.ts
  • src/features/sidebar/utils/sidebar-validation.ts
  • src/features/sidebar/utils/validate-search.ts
  • src/shared/hooks/useNameValidation.ts
  • src/shared/utils/category-icons.ts
  • src/test/factories/campaign-factory.ts
  • src/test/factories/sidebar-item-factory.ts
  • src/test/factories/user-factory.ts
💤 Files with no reviewable changes (4)
  • convex/sidebarItems/createParentTarget.ts
  • convex/sidebarItems/sharedValidation.ts
  • src/features/sidebar/utils/sidebar-create-validation.ts
  • convex/sidebarItems/validation.ts

Comment thread .vscode/settings.json
Comment thread .vscode/settings.json
Comment thread convex/_test/factories.helper.ts
Comment thread convex/campaigns/functions/updateCampaign.ts
name: args.name,
description: args.description,
slug: args.slug,
slug: args.slug ? requireCampaignSlug(args.slug) : undefined,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Prefer explicit undefined check over truthy test.

campaignSlugValidator should already reject empty strings, so this works today, but args.slug ? … : undefined would silently fall through for any falsy value if the validator were ever loosened. Use args.slug !== undefined ? requireCampaignSlug(args.slug) : undefined to make the intent (optional presence, not truthiness) explicit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/campaigns/mutations.ts` at line 55, Change the truthy check on
args.slug to an explicit undefined check so optional presence (not falsiness) is
intended: replace the expression currently using args.slug ?
requireCampaignSlug(args.slug) : undefined with a check for args.slug !==
undefined ? requireCampaignSlug(args.slug) : undefined, referencing the same
requireCampaignSlug call and the args.slug property in the mutation to ensure
empty strings or other falsy-but-present values are still validated.

Comment on lines 21 to +48
function isRecentEntry(e: unknown): e is RecentEntry {
return (
typeof e === 'object' &&
e !== null &&
typeof (e as RecentEntry).slug === 'string' &&
typeof (e as RecentEntry).timestamp === 'number'
)
if (typeof e !== 'object' || e === null) return false
const rawSlug = (e as { slug?: unknown }).slug
const slug = typeof rawSlug === 'string' ? parseSidebarItemSlug(rawSlug) : null
return slug !== null && typeof (e as { timestamp?: unknown }).timestamp === 'number'
}

function parseRecentEntry(entry: unknown): RecentEntry | null {
if (typeof entry !== 'object' || entry === null) return null
const rawSlug = (entry as { slug?: unknown }).slug
const timestamp = (entry as { timestamp?: unknown }).timestamp
if (typeof rawSlug !== 'string' || typeof timestamp !== 'number') return null

const slug = parseSidebarItemSlug(rawSlug)
if (!slug) return null

return { slug, timestamp }
}

function parseEntries(raw: string | null, key: string): Array<RecentEntry> {
if (!raw) return []
try {
const parsed: unknown = JSON.parse(raw)
if (!Array.isArray(parsed)) return []
return parsed.filter(isRecentEntry)
return parsed.flatMap((entry) => {
const parsedEntry = parseRecentEntry(entry)
return parsedEntry ? [parsedEntry] : []
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

DRY: isRecentEntry duplicates parseRecentEntry and discards parsed slug.

isRecentEntry re-implements the same validation as parseRecentEntry and then throws away the parsed SidebarItemSlug, forcing the useRecentItems path to re-trust the shape via filter(isRecentEntry) at Line 90 while parseEntries uses parseRecentEntry instead. Consolidate by having useRecentItems (Line 90) also go through parseRecentEntry via flatMap, and drop isRecentEntry.

♻️ Proposed refactor
-function isRecentEntry(e: unknown): e is RecentEntry {
-  if (typeof e !== 'object' || e === null) return false
-  const rawSlug = (e as { slug?: unknown }).slug
-  const slug = typeof rawSlug === 'string' ? parseSidebarItemSlug(rawSlug) : null
-  return slug !== null && typeof (e as { timestamp?: unknown }).timestamp === 'number'
-}
-
 function parseRecentEntry(entry: unknown): RecentEntry | null {
@@
-  const validEntries = Array.isArray(entries) ? entries.filter(isRecentEntry) : []
+  const validEntries = Array.isArray(entries)
+    ? entries.flatMap((e) => {
+        const parsed = parseRecentEntry(e)
+        return parsed ? [parsed] : []
+      })
+    : []
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/search/hooks/use-recent-items.ts` around lines 21 - 48, The
isRecentEntry type-guard duplicates parseRecentEntry and discards the parsed
slug; remove isRecentEntry and update useRecentItems to call parseRecentEntry
for each raw entry (use parsedEntries = rawArray.flatMap(entry => { const p =
parseRecentEntry(entry); return p ? [p] : [] }) or equivalent) instead of
filter(isRecentEntry) so you reuse parsing logic and keep the validated
SidebarItemSlug; also delete or stop referencing isRecentEntry wherever else
used and ensure parseEntries remains the single source of truth for entry
validation.

Comment thread src/features/sidebar/hooks/useCreateSidebarItem.ts Outdated
content: args.content,
})
return { id: noteId, slug, type: args.type }
return { id: noteId, slug: assertSidebarItemSlug(slug), type: args.type }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Redundant client-side slug assertion.

The server mutation already validates slug through sidebarItemSlugValidator on the return contract; re-asserting with assertSidebarItemSlug on the client is defensive but effectively dead code for any slug that successfully round-tripped through Convex. If you want the branded type at the boundary without the runtime cost, a plain cast (or a toSidebarItemSlug that brands without re-checking) is equivalent. Not blocking — just noise you may want to drop later.

Also applies to: 115-115, 126-126, 137-137, 147-147

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/sidebar/hooks/useCreateSidebarItem.ts` at line 105, The
client-side calls to assertSidebarItemSlug in useCreateSidebarItem.ts are
redundant because the server already validates slug via
sidebarItemSlugValidator; remove the runtime assertions in the return statements
(e.g., the return producing { id: noteId, slug: assertSidebarItemSlug(slug),
type: args.type } and the similar occurrences at the other noted lines) and
replace them with a simple type cast or a no-op branding helper (e.g.,
toSidebarItemSlug that only narrows the type without re-checking) so the value
retains the branded SidebarItemSlug type without the unnecessary runtime cost.

Comment thread src/features/sidebar/hooks/useEditSidebarItem.ts Outdated
Comment on lines +46 to +58
it('ignores invalid item slugs', () => {
expect(validateSearch({ item: 'bad slug' })).toEqual({})
expect(validateSearch({ item: 'Bad-Slug' })).toEqual({})
expect(validateSearch({ item: '-leading-hyphen' })).toEqual({})
expect(validateSearch({ item: 'trailing-hyphen-' })).toEqual({})
expect(validateSearch({ item: 'double--hyphen' })).toEqual({})
expect(validateSearch({ item: 'special@char' })).toEqual({})
})
it('accepts valid slugs with numbers and single characters', () => {
expect(validateSearch({ item: 'item-123' })).toEqual({ item: 'item-123' })
expect(validateSearch({ item: 'a' })).toEqual({ item: 'a' })
expect(validateSearch({ item: 'a-b-c' })).toEqual({ item: 'a-b-c' })
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Solid coverage for the new slug parsing.

Consider also adding a test for a slug exceeding the max length (if one is enforced by parseSidebarItemSlug) to lock in the upper bound.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/sidebar/utils/__tests__/validate-search.test.ts` around lines 46
- 58, Add a test asserting that overly long slugs are rejected by validateSearch
(and thus parseSidebarItemSlug) by constructing a slug longer than the enforced
maximum and expecting validateSearch({ item: longSlug }) toEqual({}) to lock in
the upper bound; reference validateSearch and parseSidebarItemSlug when adding
this case to src/features/sidebar/utils/__tests__/validate-search.test.ts so
future changes to the max length will be caught.

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

Caution

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

⚠️ Outside diff range comments (1)
src/features/editor/components/note-content.tsx (1)

170-214: ⚠️ Potential issue | 🔴 Critical

Same StrictMode use-after-destroy here, compounded by misleading effect deps.

  1. The cleanup at Line 211 destroys editor._tiptapEditor, but editor is held in useState, so StrictMode's simulated unmount→remount will replay this effect against the already-destroyed editor — patchYUndoPluginDestroy, patchYSyncAfterTypeChanged, and all downstream NoteView/WikiLinkAutocomplete renders then operate on a torn-down view.
  2. The dep array [doc, editor] (Line 214) suggests the effect reacts to doc changes, but the editor was constructed with doc.getXmlFragment('document'), provider, and user captured by the useState initializer at first render. A doc change would destroy the editor in cleanup and re-run the effect with a stale editor still bound to the original fragment. Today this is masked only because the parent forces a full remount via key={instanceId}; the deps are still wrong.

The cleanest fix is to move BlockNoteEditor.create(...) into the same effect as its destroy, so creation/destruction are symmetric and collaboration options are always fresh:

🔧 Proposed fix
-  const [editor] = useState<CustomBlockNoteEditor>(
-    () =>
-      BlockNoteEditor.create({
-        schema: editorSchema,
-        collaboration: {
-          provider,
-          fragment: doc.getXmlFragment('document'),
-          user: { name: user.name, color: user.color },
-          showCursorLabels: 'activity',
-        },
-      }) as CustomBlockNoteEditor,
-  )
+  const [editor, setEditor] = useState<CustomBlockNoteEditor | null>(null)
   const linkResolver = useLinkResolver(noteId)
   const onEditorChangeRef = useRef(onEditorChange)
   onEditorChangeRef.current = onEditorChange

   useEffect(() => {
-    onEditorChangeRef.current?.(editor, doc)
+    const instance = BlockNoteEditor.create({
+      schema: editorSchema,
+      collaboration: {
+        provider,
+        fragment: doc.getXmlFragment('document'),
+        user: { name: user.name, color: user.color },
+        showCursorLabels: 'activity',
+      },
+    }) as CustomBlockNoteEditor
+    setEditor(instance)
+    onEditorChangeRef.current?.(instance, doc)

     let cancelled = false
     let retries = 0
     const MAX_RETRIES = 30
     const tryPatch = () => {
       if (cancelled) return
-      if (editor._tiptapEditor.view.state.plugins.length === 0) {
+      if (instance._tiptapEditor.view.state.plugins.length === 0) {
         if (++retries >= MAX_RETRIES) {
           logger.error('Failed to patch Yjs plugins', { maxRetries: MAX_RETRIES })
           return
         }
         setTimeout(tryPatch, 50)
         return
       }
-      patchYUndoPluginDestroy(editor._tiptapEditor.view)
-      patchYSyncAfterTypeChanged(editor._tiptapEditor.view)
+      patchYUndoPluginDestroy(instance._tiptapEditor.view)
+      patchYSyncAfterTypeChanged(instance._tiptapEditor.view)
     }
     setTimeout(tryPatch, 50)

     return () => {
       cancelled = true
-      editor._tiptapEditor.destroy()
+      instance._tiptapEditor.destroy()
       onEditorChangeRef.current?.(null, null)
+      setEditor(null)
     }
-  }, [doc, editor])
+  }, [doc, provider, user.name, user.color])

Consumers of editor further down will need a null guard (as they were before this refactor).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/editor/components/note-content.tsx` around lines 170 - 214, Move
the BlockNoteEditor creation out of the useState initializer and into the same
useEffect that currently destroys editor so creation and destruction are
symmetric: make the editor state nullable (e.g., CustomBlockNoteEditor | null),
create the editor via BlockNoteEditor.create(...) inside the effect using fresh
doc, provider and user values, run patchYUndoPluginDestroy and
patchYSyncAfterTypeChanged against the newly-created editor._tiptapEditor.view,
and call editor._tiptapEditor.destroy() in the effect cleanup; update the effect
dependency list to include the true inputs (doc, provider, user) instead of the
stale editor and ensure all consumers (including useLinkResolver,
onEditorChangeRef usage and NoteView/WikiLinkAutocomplete) guard for a null
editor before accessing editor._tiptapEditor or rendering.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 102-103: Remove the redundant per-step env override that
re-assigns VITE_SITE_URL in the CI step: since VITE_SITE_URL is already defined
at the workflow-level env it is inherited by steps, so delete the env:
VITE_SITE_URL: ${{ env.VITE_SITE_URL }} block from the step in
.github/workflows/ci.yml (or only add it back if you intend a different value
for this job/step).

In `@convex/common/slug.ts`:
- Around line 71-77: The assert function currently calls parse and then calls
validate again causing a double parse; change assert to call validate(value)
once, inspect the returned SafeParse result (or equivalent) and if success
return the parsed data cast to BrandedString<Kind>, otherwise throw new
Error(using the validation error message or fallbackMessage). Update assert to
reference validate (not call parse first) and return the parsed value from the
single validate result so you avoid double-parsing; keep the thrown type as a
plain Error but consider documenting that behavior for callers of assert/require
functions.

In `@src/features/campaigns/hooks/__tests__/use-campaign.test.tsx`:
- Line 107: The mock for useMatch should return undefined (the real API's
no-match value) instead of false; update the test mock call
mockUseMatch.mockReturnValue(false) to mockUseMatch.mockReturnValue(undefined)
so it mirrors TanStack Router behavior and prevents masking future
strict-equality checks in useOptionalCampaignRoute or other code that consumes
campaignMatch.

In `@src/features/campaigns/hooks/useCampaign.ts`:
- Around line 57-74: Add a short JSDoc above useOptionalCampaign explaining its
contract: unlike CampaignProvider it does not throw or surface CampaignNotFound
on query errors; it always calls useAuthQuery (skipped when context exists or
identity missing) and, when a query errors, returns the value produced by
buildCampaignContextValue with isCampaignLoaded set false and campaign.isError
true — callers (e.g., PeopleTab) must inspect campaign.isError to handle
not-found or error cases; mention interaction with CampaignProvider to clarify
subscription/skip behavior.

In `@src/features/canvas/components/canvas-remote-cursors.tsx`:
- Line 97: Replace the magic number 150 used in the will-change timeout
expression (isDragging ? PIN_LERP_DURATION : 150) with a named constant (e.g.
WILL_CHANGE_IDLE_TIMEOUT = 150) and use that constant in the ternary; add a
short comment by the constant explaining it is the timeout to clear the GPU hint
shortly after the last cursor update so the intent matches PIN_LERP_DURATION and
both timings are easy to adjust.
- Around line 143-154: The useEffect at the bottom of canvas-remote-cursors.tsx
is redundant: its cleanup duplicates the existing cleanup that already clears
willChangeTimeoutRef and manipulates element.style.willChange in the other
effect, and it captures a stale elementRef.current due to empty deps; remove
this entire useEffect (the one that references elementRef.current and
willChangeTimeoutRef and sets element.style.willChange = '') to avoid
duplicate/no-op cleanup and stale-capture confusion.

In
`@src/features/settings/components/tabs/campaign-people/__tests__/people-tab.test.tsx`:
- Around line 19-55: Add a test that mocks useOptionalCampaign to return a
campaign context with isDm: true (e.g., reuse createCampaign and mockAuthQuery)
and mocks useAuthQuery similarly, render <PeopleTab /> in TestWrapper, then
assert that InviteLinkSection is rendered and that the generated joinUrl (or the
element containing it) matches the expected value; target the PeopleTab
component and the InviteLinkSection/joinUrl outputs when adding the new test so
the DM-specific branch is covered.

In `@src/features/settings/components/tabs/campaign-people/people-tab.tsx`:
- Line 47: The guard in the PeopleTab component is too broad: dmUsername,
campaignSlug and campaign are non-optional properties on CampaignContextType, so
replace the multi-variable null-check with a single check for campaignContext
(e.g., if (!campaignContext) ...), then access campaignContext.dmUsername,
campaignContext.campaignSlug and campaignContext.campaign as before; update the
early return/placeholder logic only to trigger when campaignContext is falsy and
remove the redundant checks for dmUsername, campaignSlug and campaign.

In `@src/features/sidebar/hooks/useEditSidebarItem.ts`:
- Around line 117-120: optimisticFields' name property is declared as plain
string (Partial<Pick<AnySidebarItem, 'name' | ...>>) but you assign a branded
SidebarItemName (normalizedName), so preserve the branded type: change the
optimisticFields type to explicitly use SidebarItemName for name (e.g.,
Partial<{ name: SidebarItemName; iconName: ... }>) or narrow the field by
declaring a small local type that maps name: SidebarItemName while keeping
iconName and color as their branded types; update the declaration of
optimisticFields and keep assigning normalizedName, normalizedIconName, and
normalizedColor without casting so the branded type is preserved for downstream
uses (refer to optimisticFields, AnySidebarItem, normalizedName, and
SidebarItemName).

---

Outside diff comments:
In `@src/features/editor/components/note-content.tsx`:
- Around line 170-214: Move the BlockNoteEditor creation out of the useState
initializer and into the same useEffect that currently destroys editor so
creation and destruction are symmetric: make the editor state nullable (e.g.,
CustomBlockNoteEditor | null), create the editor via BlockNoteEditor.create(...)
inside the effect using fresh doc, provider and user values, run
patchYUndoPluginDestroy and patchYSyncAfterTypeChanged against the newly-created
editor._tiptapEditor.view, and call editor._tiptapEditor.destroy() in the effect
cleanup; update the effect dependency list to include the true inputs (doc,
provider, user) instead of the stale editor and ensure all consumers (including
useLinkResolver, onEditorChangeRef usage and NoteView/WikiLinkAutocomplete)
guard for a null editor before accessing editor._tiptapEditor or rendering.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2ff2a302-7c8a-494e-8c15-1953d46d1bdc

📥 Commits

Reviewing files that changed from the base of the PR and between 484014b and 6ee173a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (21)
  • .github/workflows/ci.yml
  • convex/campaigns/functions/updateCampaign.ts
  • convex/common/__tests__/validation.test.ts
  • convex/common/async.ts
  • convex/common/slug.ts
  • convex/gameMaps/mutations.ts
  • convex/notes/functions/createNote.ts
  • convex/notes/mutations.ts
  • convex/sidebarItems/validation/color.ts
  • package.json
  • src/features/campaigns/contexts/__tests__/campaign-context.test.tsx
  • src/features/campaigns/contexts/campaign-context.tsx
  • src/features/campaigns/hooks/__tests__/use-campaign.test.tsx
  • src/features/campaigns/hooks/useCampaign.ts
  • src/features/canvas/components/canvas-remote-cursors.tsx
  • src/features/editor/components/note-content.tsx
  • src/features/editor/components/viewer/map/map-viewer.tsx
  • src/features/settings/components/tabs/campaign-people/__tests__/people-tab.test.tsx
  • src/features/settings/components/tabs/campaign-people/people-tab.tsx
  • src/features/sidebar/hooks/useCreateSidebarItem.ts
  • src/features/sidebar/hooks/useEditSidebarItem.ts

Comment thread .github/workflows/ci.yml Outdated
Comment thread convex/common/slug.ts
Comment thread src/features/campaigns/hooks/__tests__/use-campaign.test.tsx Outdated
Comment thread src/features/campaigns/hooks/useCampaign.ts
Comment thread src/features/canvas/components/canvas-remote-cursors.tsx Outdated
Comment thread src/features/canvas/components/canvas-remote-cursors.tsx Outdated
Comment thread src/features/settings/components/tabs/campaign-people/people-tab.tsx Outdated
Comment on lines +117 to +120
const optimisticFields: Partial<Pick<AnySidebarItem, 'name' | 'iconName' | 'color'>> = {}
if (normalizedName !== undefined) optimisticFields.name = normalizedName
if (normalizedIconName !== undefined) optimisticFields.iconName = normalizedIconName
if (normalizedColor !== undefined) optimisticFields.color = normalizedColor
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f 'types.ts' convex/sidebarItems/types | xargs -I{} sh -c 'echo "=== {} ==="; cat {}'
fd -t f 'baseTypes.ts' convex/sidebarItems/types | xargs -I{} sh -c 'echo "=== {} ==="; cat {}'

Repository: ntietje1/wizard-archive

Length of output: 5993


🏁 Script executed:

# First, let's look at the complete context around lines 117-120 in useEditSidebarItem.ts
fd -t f 'useEditSidebarItem.ts' | xargs -I{} sh -c 'echo "=== {} ==="; cat -n {} | sed -n "100,130p"'

Repository: ntietje1/wizard-archive

Length of output: 1531


🏁 Script executed:

# Also need to check what types normalizedName, normalizedIconName, normalizedColor have
fd -t f 'useEditSidebarItem.ts' | xargs -I{} sh -c 'cat -n {} | head -50'

Repository: ntietje1/wizard-archive

Length of output: 2337


🏁 Script executed:

# Check one of the enhanced types to see if they override field types
fd -t f 'types.ts' convex/notes | xargs -I{} sh -c 'echo "=== {} ==="; cat {}'

Repository: ntietje1/wizard-archive

Length of output: 1054


🏁 Script executed:

# Check the validation functions to see their return types
fd -t f 'name.ts' convex/sidebarItems/validation | xargs -I{} sh -c 'echo "=== {} ==="; cat {}'

Repository: ntietje1/wizard-archive

Length of output: 4189


🏁 Script executed:

# Check if any enhanced types override the name/iconName/color field types
fd -t f 'types.ts' convex/folders | xargs -I{} sh -c 'echo "=== {} ==="; cat {}'

Repository: ntietje1/wizard-archive

Length of output: 683


🏁 Script executed:

# Also check GameMap, SidebarFile, Canvas
fd -t f 'types.ts' convex/gameMaps | xargs -I{} sh -c 'echo "=== {} ==="; cat {}'
fd -t f 'types.ts' convex/files | xargs -I{} sh -c 'echo "=== {} ==="; cat {}'
fd -t f 'types.ts' convex/canvases | xargs -I{} sh -c 'echo "=== {} ==="; cat {}'

Repository: ntietje1/wizard-archive

Length of output: 3854


🏁 Script executed:

# Check the color and icon validation functions to confirm they return branded types
fd -t f 'color.ts' convex/sidebarItems/validation | xargs -I{} sh -c 'echo "=== {} ==="; cat {} | head -50'
fd -t f 'icon.ts' convex/sidebarItems/validation | xargs -I{} sh -c 'echo "=== {} ==="; cat {} | head -50'

Repository: ntietje1/wizard-archive

Length of output: 2921


The name field has a type inconsistency with branded input values.

optimisticFields picks AnySidebarItem fields typed as name: string (plain), iconName: SidebarItemIconName | null, and color: SidebarItemColor | null. The code assigns normalizedName (type SidebarItemName, a branded string) to the plain string field, while normalizedIconName and normalizedColor already match their branded field types in the base definition. TypeScript accepts this assignment via widening, but the optimistic object's name field will carry a plain string type despite coming from a branded value, creating a shape mismatch if the optimistic data is later used in contexts expecting SidebarItemName.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/sidebar/hooks/useEditSidebarItem.ts` around lines 117 - 120,
optimisticFields' name property is declared as plain string
(Partial<Pick<AnySidebarItem, 'name' | ...>>) but you assign a branded
SidebarItemName (normalizedName), so preserve the branded type: change the
optimisticFields type to explicitly use SidebarItemName for name (e.g.,
Partial<{ name: SidebarItemName; iconName: ... }>) or narrow the field by
declaring a small local type that maps name: SidebarItemName while keeping
iconName and color as their branded types; update the declaration of
optimisticFields and keep assigning normalizedName, normalizedIconName, and
normalizedColor without casting so the branded type is preserved for downstream
uses (refer to optimisticFields, AnySidebarItem, normalizedName, and
SidebarItemName).

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

Caution

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

⚠️ Outside diff range comments (5)
src/features/editor/components/topbar/editable-breadcrumb.tsx (1)

73-90: ⚠️ Potential issue | 🟠 Major

Reset isSubmitting on every validation exit path.

Line 76 returns early when checkNameUnique fails, but Line 90 is skipped, leaving the input disabled for the rest of the component lifetime.

Suggested fix
     setIsSubmitting(true)
     try {
       const error = checkNameUnique(trimmedName)
       if (error) {
         toast.error(error)
         setName(initialName)
         onChange?.(initialName)
         setIsEditing(false)
         return
       }
       await onRename?.(trimmedName)
       setIsEditing(false)
     } catch (error) {
       handleError(error, 'Failed to rename item')
       setName(initialName)
       onChange?.(initialName)
+    } finally {
+      setIsSubmitting(false)
     }
-    setIsSubmitting(false)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/editor/components/topbar/editable-breadcrumb.tsx` around lines
73 - 90, The early return after validation in the EditableBreadcrumb handler
leaves isSubmitting true; ensure setIsSubmitting(false) is executed on every
exit path (successful rename, validation failure, and error) by moving the call
into a finally block or explicitly calling setIsSubmitting(false) before the
validation-return branch; update the function that calls checkNameUnique (and
references setIsSubmitting, setIsEditing, setName, onChange, onRename) so
isSubmitting is reset whether returning early, throwing, or completing.
src/features/sidebar/components/sidebar-item/editable-item-name.tsx (1)

75-91: ⚠️ Potential issue | 🟠 Major

Clear isSubmitting when validation rejects the rename.

Line 79’s error branch returns before Line 91, so after a duplicate/invalid name the component stays in a submitting state and future rename attempts can open with a disabled input.

Suggested fix
       // Validate the name before submitting
       setIsSubmitting(true)
       try {
         const error = checkNameUnique(trimmedName)
         if (error) {
           toast.error(error)
           setName(initialName)
           onCancelRename()
           return
         }
         await onFinishRename(trimmedName)
       } catch (error) {
         handleError(error, 'Failed to rename')
         setName(initialName)
         onCancelRename()
+      } finally {
+        setIsSubmitting(false)
       }
-      setIsSubmitting(false)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/sidebar/components/sidebar-item/editable-item-name.tsx` around
lines 75 - 91, The validation branch returns early leaving isSubmitting true;
update the rename flow in the EditableItemName component so
setIsSubmitting(false) always runs even on validation rejection or errors—either
move setIsSubmitting(false) into a finally block around the try/catch or call
setIsSubmitting(false) just before the early return after
checkNameUnique(trimmedName) fails; ensure this change touches the block that
calls checkNameUnique, toast.error, setName(initialName), onCancelRename,
onFinishRename and handleError so subsequent rename attempts re-enable the
input.
src/features/sidebar/components/forms/sidebar-item-edit-dialog.tsx (1)

73-79: ⚠️ Potential issue | 🟠 Major

Reset the form when the dialog opens or the target item changes.

With the current dependencies, canceling after local edits and reopening the same item can show stale unsaved values. Switching to another item with identical name, iconName, and color also skips reset because item._id is no longer tracked.

Suggested fix
   useEffect(() => {
     form.reset({
       name: item.name ?? '',
       iconName: item.iconName ?? null,
       color: item.color ?? null,
     })
-  }, [form, item.color, item.iconName, item.name])
+  }, [form, isOpen, item._id, item.color, item.iconName, item.name])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/sidebar/components/forms/sidebar-item-edit-dialog.tsx` around
lines 73 - 79, The form reset effect currently only depends on form and item
fields (name, iconName, color) which lets reopening the same item or switching
to a different item with identical fields skip reset; update the useEffect that
calls form.reset to also depend on the item's unique identifier (item._id) and
the dialog open state (e.g., open or isOpen if present) so the effect runs
whenever the dialog opens or the target item changes, ensuring form.reset({
name: item.name ?? '', iconName: item.iconName ?? null, color: item.color ??
null }) is invoked at those times.
convex/sidebarItems/functions/moveSidebarItem.ts (1)

63-82: ⚠️ Potential issue | 🟠 Major

Legacy DB names can hard-fail the restore flow.

assertSidebarItemName(deduplicateName(item.name, otherNames)) runs the full SidebarItemName validation pipeline (trim/whitespace/forbidden chars/length) on a DB-sourced value. Any pre-existing sidebarItems row whose name does not satisfy the current strict rules (legacy data, older character rules, etc.) will cause the entire restore-from-trash to throw VALIDATION_FAILED, with no way for the user to recover short of admin intervention. This mirrors the concern already raised in convex/sidebarItems/validation/orchestration.ts (validateSidebarMove).

Prefer a non-throwing brand cast (since downstream findUniqueSidebarItemSlug / checkNameConflict treat it structurally) or loosen findUniqueSidebarItemSlug's name type to string:

♻️ Suggested fix
-  const uniqueName = assertSidebarItemName(deduplicateName(item.name, otherNames))
+  const uniqueName = deduplicateName(item.name, otherNames) as SidebarItemName

Also minor: the return type and local patch declare slug?: string, but the assigned value is SidebarItemSlug. Consider slug?: SidebarItemSlug for symmetry with name.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/sidebarItems/functions/moveSidebarItem.ts` around lines 63 - 82,
resolveRestoreConflicts currently calls
assertSidebarItemName(deduplicateName(...)) which will throw if DB legacy names
don't meet current SidebarItemName validation; change this to a non-throwing
approach by either casting the deduplicated name to SidebarItemName using a
safe/explicit brand cast (instead of assertSidebarItemName) or update
findUniqueSidebarItemSlug to accept a plain string name so you don't validate
DB-sourced names here; then keep using findUniqueSidebarItemSlug(ctx, { itemId:
item._id, name: uniqueName }) and only set patch.name if the deduplicated value
structurally differs, and update the patch type and return signature to use
slug?: SidebarItemSlug (matching SidebarItemName/SidebarItemSlug types) for
symmetry with name.
convex/sidebarItems/functions/treeOperations.ts (1)

53-74: ⚠️ Potential issue | 🟠 Major

Restore-descendants path can hard-fail on legacy names.

assertSidebarItemName(i.name) at line 62 runs the full SidebarItemName validator against a DB value during folder restore. If any descendant has a legacy name that fails the current rules, restoreTreeDescendants throws partway through the traversal and the tree is left half-restored (no transaction rollback across iterations once a patch has been applied earlier in the loop). Prefer a non-throwing brand cast here, since the only downstream use is as input to findUniqueSidebarItemSlug, which only needs the string for slug generation and campaign-scoped uniqueness:

♻️ Suggested fix
-    const name = assertSidebarItemName(i.name)
+    const name = i.name as SidebarItemName
     const slug = await findUniqueSidebarItemSlug(ctx, {
       itemId: i._id,
       name,
     })

This is the same root-cause concern raised on enhanceSidebarItem.ts/moveSidebarItem.ts/orchestration.ts — re-validating DB data with assertion functions turns latent schema drift into loud runtime failures in flows unrelated to the user's intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/sidebarItems/functions/treeOperations.ts` around lines 53 - 74,
restoreTreeDescendants currently uses assertSidebarItemName(i.name) which can
throw on legacy DB names and leave the tree half-restored; replace the assertion
with a non-throwing brand cast so slug generation still gets a string but no
exception is thrown (e.g. const name = String(i.name) as SidebarItemName or
i.name as unknown as SidebarItemName) in restoreTreeDescendants before calling
findUniqueSidebarItemSlug, keeping the rest of the loop unchanged so patches
don’t abort mid-traversal.
♻️ Duplicate comments (1)
convex/_test/factories.helper.ts (1)

88-88: ⚠️ Potential issue | 🟡 Minor

Truthiness-based override handling still silently swallows empty strings.

Lines 88, 121, and 232 all use value ? validate(value) : fallback, so an explicit empty-string override is indistinguishable from undefined and is replaced by the generated default (or, at L232, by slugify(name)) rather than failing validation. This is inconsistent with the !== undefined pattern used in src/test/factories/sidebar-item-factory.ts in the same PR, and hides test bugs where an empty override was unintentional.

🔧 Suggested change
-    ...(username ? { username: assertUsername(username) } : {}),
+    ...(username !== undefined ? { username: assertUsername(username) } : {}),
-    ...(slug ? { slug: assertCampaignSlug(slug) } : {}),
+    ...(slug !== undefined ? { slug: assertCampaignSlug(slug) } : {}),
-  const validatedSlug = slug ? assertSidebarItemSlug(slug) : assertSidebarItemSlug(slugify(name))
+  const validatedSlug =
+    slug !== undefined ? assertSidebarItemSlug(slug) : assertSidebarItemSlug(slugify(name))

Also applies to: 121-121, 232-232

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/_test/factories.helper.ts` at line 88, The current truthiness checks
(e.g. the spread using "(username ? { username: assertUsername(username) } :
{})" and the similar patterns at the other two sites) treat empty string as
absent and silently replace it; change those ternaries to check explicitly for
undefined (e.g. "username !== undefined ? { username: assertUsername(username) }
: {}") so an empty-string override is passed to the validator instead of being
dropped, and do the same for the other occurrences (including the case where a
provided slug override should be validated rather than replaced by
slugify(name)). Ensure you call the same validators used today (e.g.
assertUsername and the slug validator) when the value !== undefined.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 50-54: Update the fallow action invocation: change the input
"format: json" to "format: sarif" (or add a short comment why json is
intentional) to restore SARIF-based annotation support, and replace the mutable
action reference "fallow-rs/fallow@v2" with a pinned commit SHA (use the
repository's full commit SHA for the desired v2 release) to follow SHA-pinning
guidance; ensure you keep existing inputs "annotations: true" and
"fail-on-issues: false" intact when making these edits.

In `@convex/sidebarItems/validation/move.ts`:
- Around line 40-52: The code must reject moves to non-existent/stale targets
before running circular/name validations: call getParent(parentId) (or otherwise
verify the target parent exists) and if it returns null/undefined return {
valid: false, error: 'Cannot move item: target parent does not exist' }
immediately (before invoking validateNoCircularParent, isRestoring checks, or
validateSidebarItemNameWithSiblings). Update the logic around
validateNoCircularParent, getParent, getSiblings,
validateSidebarItemNameWithSiblings and isRestoring so the existence check for
parentId happens first and short-circuits the flow when missing.

In `@convex/sidebarItems/validation/orchestration.ts`:
- Around line 73-91: In validateSidebarParentChange remove the redundant
"parentFromDb &&" guards after the throwClientError calls (since
throwClientError is typed never and parentFromDb is already narrowed to
non-null) — directly reference parentFromDb in the subsequent checks and calls
(e.g., the type check, location check, and requireItemAccess call). Also make
validation behavior consistent by updating validateSidebarCreateParent to use
requireItemAccess (instead of getSidebarItemPermissionLevel +
hasAtLeastPermissionLevel) so both flows centralize access gating via
requireItemAccess.

In `@convex/sidebarItems/validation/parent.ts`:
- Around line 238-241: The parent validation currently only checks existence via
buildParentStack(parentTarget.baseParentId, itemsMap) but not that the resolved
base parent is a folder; update the branch handling parentTarget to ensure the
resolved parent item (from parentStack or itemsMap using
parentTarget.baseParentId) has the folder type (e.g., type === 'folder' or
isFolder flag) and return { valid: false, error: 'Parent is not a folder' } when
it isn't; keep using buildParentStack, parentTarget.baseParentId, itemsMap and
also consider pathSegments resolution path so both direct and path branches
enforce folder-only parents.

In `@src/features/campaigns/hooks/useCampaign.ts`:
- Around line 14-17: The exported type CampaignRouteIdentity is not used outside
this module, so remove the export to keep it internal: change the declaration of
CampaignRouteIdentity (the type that contains dmUsername: Username and
campaignSlug: CampaignSlug) from an exported type to a plain internal type by
deleting the "export" keyword and ensure any local usages in functions/hooks in
this file continue to reference CampaignRouteIdentity without import changes.

In
`@src/features/settings/components/tabs/campaign-people/__tests__/people-tab.test.tsx`:
- Around line 129-131: The test currently asserts against
import.meta.env.VITE_SITE_URL but PeopleTab builds the join URL using
getOrigin(), so mock the getOrigin() utility in the test to return a
deterministic origin (e.g., 'https://example.test') and update the expectation
to use that mocked origin combined with `/join/testdm/${campaign.slug}`; ensure
you call jest.mock(...) for the module that exports getOrigin and set the mock
implementation before rendering PeopleTab so the invite-link-section assertion
is stable.

---

Outside diff comments:
In `@convex/sidebarItems/functions/moveSidebarItem.ts`:
- Around line 63-82: resolveRestoreConflicts currently calls
assertSidebarItemName(deduplicateName(...)) which will throw if DB legacy names
don't meet current SidebarItemName validation; change this to a non-throwing
approach by either casting the deduplicated name to SidebarItemName using a
safe/explicit brand cast (instead of assertSidebarItemName) or update
findUniqueSidebarItemSlug to accept a plain string name so you don't validate
DB-sourced names here; then keep using findUniqueSidebarItemSlug(ctx, { itemId:
item._id, name: uniqueName }) and only set patch.name if the deduplicated value
structurally differs, and update the patch type and return signature to use
slug?: SidebarItemSlug (matching SidebarItemName/SidebarItemSlug types) for
symmetry with name.

In `@convex/sidebarItems/functions/treeOperations.ts`:
- Around line 53-74: restoreTreeDescendants currently uses
assertSidebarItemName(i.name) which can throw on legacy DB names and leave the
tree half-restored; replace the assertion with a non-throwing brand cast so slug
generation still gets a string but no exception is thrown (e.g. const name =
String(i.name) as SidebarItemName or i.name as unknown as SidebarItemName) in
restoreTreeDescendants before calling findUniqueSidebarItemSlug, keeping the
rest of the loop unchanged so patches don’t abort mid-traversal.

In `@src/features/editor/components/topbar/editable-breadcrumb.tsx`:
- Around line 73-90: The early return after validation in the EditableBreadcrumb
handler leaves isSubmitting true; ensure setIsSubmitting(false) is executed on
every exit path (successful rename, validation failure, and error) by moving the
call into a finally block or explicitly calling setIsSubmitting(false) before
the validation-return branch; update the function that calls checkNameUnique
(and references setIsSubmitting, setIsEditing, setName, onChange, onRename) so
isSubmitting is reset whether returning early, throwing, or completing.

In `@src/features/sidebar/components/forms/sidebar-item-edit-dialog.tsx`:
- Around line 73-79: The form reset effect currently only depends on form and
item fields (name, iconName, color) which lets reopening the same item or
switching to a different item with identical fields skip reset; update the
useEffect that calls form.reset to also depend on the item's unique identifier
(item._id) and the dialog open state (e.g., open or isOpen if present) so the
effect runs whenever the dialog opens or the target item changes, ensuring
form.reset({ name: item.name ?? '', iconName: item.iconName ?? null, color:
item.color ?? null }) is invoked at those times.

In `@src/features/sidebar/components/sidebar-item/editable-item-name.tsx`:
- Around line 75-91: The validation branch returns early leaving isSubmitting
true; update the rename flow in the EditableItemName component so
setIsSubmitting(false) always runs even on validation rejection or errors—either
move setIsSubmitting(false) into a finally block around the try/catch or call
setIsSubmitting(false) just before the early return after
checkNameUnique(trimmedName) fails; ensure this change touches the block that
calls checkNameUnique, toast.error, setName(initialName), onCancelRename,
onFinishRename and handleError so subsequent rename attempts re-enable the
input.

---

Duplicate comments:
In `@convex/_test/factories.helper.ts`:
- Line 88: The current truthiness checks (e.g. the spread using "(username ? {
username: assertUsername(username) } : {})" and the similar patterns at the
other two sites) treat empty string as absent and silently replace it; change
those ternaries to check explicitly for undefined (e.g. "username !== undefined
? { username: assertUsername(username) } : {}") so an empty-string override is
passed to the validator instead of being dropped, and do the same for the other
occurrences (including the case where a provided slug override should be
validated rather than replaced by slugify(name)). Ensure you call the same
validators used today (e.g. assertUsername and the slug validator) when the
value !== undefined.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 13796649-4f5a-48f5-84ed-3a88c89f1d57

📥 Commits

Reviewing files that changed from the base of the PR and between 6ee173a and 69fc0ec.

📒 Files selected for processing (32)
  • .github/workflows/ci.yml
  • convex/_test/factories.helper.ts
  • convex/common/slug.ts
  • convex/links/__tests__/linkResolution.test.ts
  • convex/links/functions/syncNoteLinks.ts
  • convex/sidebarItems/functions/collectDescendants.ts
  • convex/sidebarItems/functions/defaultItemName.ts
  • convex/sidebarItems/functions/enhanceSidebarItem.ts
  • convex/sidebarItems/functions/hardDeleteItem.ts
  • convex/sidebarItems/functions/moveSidebarItem.ts
  • convex/sidebarItems/functions/purgeExpiredTrash.ts
  • convex/sidebarItems/functions/treeOperations.ts
  • convex/sidebarItems/types/baseTypes.ts
  • convex/sidebarItems/types/types.ts
  • convex/sidebarItems/validation/access.ts
  • convex/sidebarItems/validation/move.ts
  • convex/sidebarItems/validation/orchestration.ts
  • convex/sidebarItems/validation/parent.ts
  • convex/sidebarShares/functions/sidebarItemPermissions.ts
  • src/features/campaigns/hooks/__tests__/use-campaign.test.tsx
  • src/features/campaigns/hooks/useCampaign.ts
  • src/features/canvas/components/canvas-remote-cursors.tsx
  • src/features/editor/components/topbar/editable-breadcrumb.tsx
  • src/features/search/utils/__tests__/merge-search-results.test.ts
  • src/features/settings/components/tabs/campaign-people/__tests__/people-tab.test.tsx
  • src/features/settings/components/tabs/campaign-people/people-tab.tsx
  • src/features/sidebar/components/forms/sidebar-item-edit-dialog.tsx
  • src/features/sidebar/components/sidebar-item/editable-item-name.tsx
  • src/features/sidebar/components/sidebar-item/types.ts
  • src/features/sidebar/hooks/useSidebarValidation.ts
  • src/routes/__root.tsx
  • src/test/factories/sidebar-item-factory.ts

Comment thread .github/workflows/ci.yml Outdated
Comment on lines +50 to +54
- uses: fallow-rs/fallow@v2
with:
node-version: ${{ env.NODE_VERSION }}
cache: true
- run: vp install --frozen-lockfile
- name: Run Fallow
continue-on-error: true
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
vp run fallow audit --base origin/main --format annotations --format json > fallow-report.json
else
vp run fallow --format json > fallow-report.json
fi
annotations: true
fail-on-issues: false
format: json
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

🧩 Analysis chain

🌐 Web query:

fallow-rs/fallow GitHub Action v2 inputs annotations fail-on-issues format

💡 Result:

The fallow-rs/fallow GitHub Action v2 defines the following relevant inputs in its action.yml: - annotations: description: 'Emit findings as inline PR annotations via workflow commands (no Advanced Security required)' required: false default: 'true' - fail-on-issues: description: 'Exit with code 1 if issues are found' required: false default: 'true' - format: description: 'Output format (human, json, sarif, compact, markdown, codeclimate, badge)' required: false default: 'sarif' These inputs control inline PR annotations (enabled by default), workflow failure on detected issues (enabled by default), and output formatting (SARIF by default, which supports annotations when uploaded). The action emits annotations in a dedicated step if annotations is true, review-comments is false, issues are found, and command is not 'fix'. Example usage: - uses: fallow-rs/fallow@v2 with: annotations: true fail-on-issues: true format: sarif Source-grounded details from official action.yml and docs confirm v2 supports these for TypeScript/JS codebase analysis in CI.

Citations:


Consider using SARIF format and pin to a commit SHA for security.

The action inputs are valid, but two refinements are recommended:

  1. The fallow-rs/fallow@v2 action defaults to format: sarif. Using format: json instead may reduce the effectiveness of inline PR annotations. If json is intentional, document why; otherwise, switch to sarif for better annotation integration.

  2. Pin this third-party action to a full commit SHA instead of the floating @v2 tag (tags are mutable). GitHub's security hardening guide recommends SHA-pinning for non-first-party actions to prevent supply-chain compromises.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 50 - 54, Update the fallow action
invocation: change the input "format: json" to "format: sarif" (or add a short
comment why json is intentional) to restore SARIF-based annotation support, and
replace the mutable action reference "fallow-rs/fallow@v2" with a pinned commit
SHA (use the repository's full commit SHA for the desired v2 release) to follow
SHA-pinning guidance; ensure you keep existing inputs "annotations: true" and
"fail-on-issues: false" intact when making these edits.

Comment thread convex/sidebarItems/validation/move.ts
Comment thread convex/sidebarItems/validation/orchestration.ts
Comment thread convex/sidebarItems/validation/parent.ts
Comment thread src/features/campaigns/hooks/useCampaign.ts Outdated
@ntietje1 ntietje1 merged commit cfcfa9c into main Apr 17, 2026
15 checks passed
@ntietje1 ntietje1 deleted the dev branch April 17, 2026 21:53
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