Skip to content

Tagging, styling, and per-page marcher customization#754

Merged
AlexDumo merged 40 commits intomainfrom
tagging
Dec 9, 2025
Merged

Tagging, styling, and per-page marcher customization#754
AlexDumo merged 40 commits intomainfrom
tagging

Conversation

@AlexDumo
Copy link
Collaborator

@AlexDumo AlexDumo commented Dec 6, 2025

  • inital database structure
  • Start implementation
  • Select by section
  • Select by section, family, tag, and drill prefix
  • Start tag buttons
  • apply migrations
  • Create, add, and remove tag
  • Shared appearance columns
  • Use generic appearance type in app
  • accept appearances in order of priority
  • Tag appearance updates on canvas
  • Remember tag selection
  • Add styles to individual marcher pages
  • update mock SQL files
  • fix failing tests
  • rename to appearance component
  • feedback on copy error
  • Change colorPicker to onChange
  • Toggle marcher visibility
  • Hide text labels
  • Virtualize marcher list
  • Appearance modal
  • Move appearance editor
  • Edit tag appearance by page
  • Fix marchers de-selecting
  • highlight tag that is being edited
  • Prefetch tag appearances
  • Add null shape
  • fix build errors
  • e2e tests
  • increment package.json

Summary by CodeRabbit

  • New Features

    • Tags: full tag management (create/rename/delete), assign/remove tags to marchers, and edit tag appearances.
    • Selection tools: new “Select” toolbar tab to pick marchers by section, tag, family, or drill-prefix.
    • Appearance modal: centralized UI for section- and tag-based appearance editing; tag appearance lists and editors added.
  • Improvements

    • Virtualized marcher list for smoother performance.
    • New compact color picker and refined color controls.
    • Error UI: copy-to-clipboard feedback when copying error details.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 6, 2025

Walkthrough

Adds tags and appearance-first features across database migrations, schema, DB functions, React Query hooks/mutations, UI components/modals/editors, canvas appearance-driven rendering, virtualized marcher lists, E2E tests, and supporting refactors and snapshots.

Changes

Cohort / File(s) Summary
Database migrations & snapshots
apps/desktop/electron/database/migrations/0013_gigantic_maestro.sql, .../0014_colossal_chat.sql, .../0015_typical_gorilla_man.sql, .../0016_unique_ultimo.sql, apps/desktop/electron/database/migrations/meta/*, .../_journal.json
Add tags, tag_appearances, marcher_tags tables and indexes; add/alter appearance/equipment columns; add migration snapshots and journal entries.
Schema utils & exports
apps/desktop/electron/database/migrations/schema.ts
Introduce shared appearance_columns and export new table definitions (tags, tag_appearances, marcher_tags); consolidate appearance fields across tables.
DB functions: tags & appearances
apps/desktop/src/db-functions/tag.ts, apps/desktop/src/db-functions/index.ts, apps/desktop/src/db-functions/sectionAppearance.ts, apps/desktop/src/db-functions/page.ts, apps/desktop/src/db-functions/marcherPage.ts, apps/desktop/src/db-functions/history.ts
New tag CRUD, tag-appearance, and marcher-tag DB APIs with transactional in-transaction helpers; export tag module; refactor section appearance functions to use appearance converters; add getPagesInOrder/DatabasePageWithBeat; change updateMarcherPages to return id[]; include new tables in history tracking.
Appearance model & converters
apps/desktop/src/entity-components/appearance.ts
New types and converters mapping raw DB appearance rows ↔ parsed AppearanceComponent (color parsing, boolean mapping) and optional variants.
Global classes: visuals & marcher pages
apps/desktop/src/global/classes/MarcherVisualGroup.ts, apps/desktop/src/global/classes/MarcherPage.ts, apps/desktop/src/global/classes/SectionAppearance.ts (deleted)
MarcherVisualGroup and MarcherPage refactored to use appearance model; legacy SectionAppearance class removed.
Canvas marcher & rendering
apps/desktop/src/global/classes/canvasObjects/CanvasMarcher.ts, apps/desktop/src/components/canvas/Canvas.tsx, apps/desktop/src/global/classes/canvasObjects/OpenMarchCanvas.ts
CanvasMarcher accepts and applies appearances via setAppearance; Canvas now fetches marcher visuals via React Query and applies appearance-driven rendering.
React Query: visuals, tags & invalidation
apps/desktop/src/hooks/queries/useMarchersWithVisuals.ts, apps/desktop/src/hooks/queries/tags/queries.ts, apps/desktop/src/hooks/queries/tags/mutations.ts, apps/desktop/src/hooks/queries/*, apps/desktop/src/hooks/queries/index.ts
New visuals hook and query options (marcherWithVisuals), tag queries/mutations, query keys and invalidation flows; shared invalidators updated to include visuals keys.
Hook re-exports & consumers
apps/desktop/src/hooks/index.ts, apps/desktop/src/hooks/queries/index.ts, apps/desktop/src/components/.../StateInitializer.tsx, apps/desktop/src/components/canvas/hooks/*
Rework hook exports (move visuals into queries index), add prefetches for marcher visuals, update listeners to use query-based marcher visuals.
UI: Tag/appearance UIs & modals
apps/desktop/src/components/inspector/marcher/TagEditor.tsx, .../appearance/{AppearanceEditor,AppearanceModal,TagAppearanceList,SectionAppearanceList}.tsx, apps/desktop/src/components/tags/TagsModel.tsx, apps/desktop/src/components/marcher/MarchersModal.tsx
Add TagButtons/TagEditor, AppearanceEditor, TagAppearanceList, new SectionAppearanceList (re-implemented), AppearanceModal and Tags modal launcher; replace old SectionAppearance UI.
UI: Marcher list & selection tab
apps/desktop/src/components/marcher/MarcherList.tsx, apps/desktop/src/components/toolbar/tabs/SelectTab.tsx, apps/desktop/src/components/toolbar/Toolbar.tsx
Virtualized marcher list using @tanstack/react-virtual; new SelectTab for selection by section/tag/family/drill-prefix; toolbar integrates select tab.
UI: Color pickers
apps/desktop/src/components/ui/ColorPicker.tsx, apps/desktop/src/components/ui/ColorPickerMini.tsx
ColorPicker adds onBlur and optional StaticFormField path; new compact ColorPickerMini component.
Inspector integrations
apps/desktop/src/components/inspector/MarcherEditor.tsx
Import and render TagButtons for single and multi-selection contexts.
Sidebar & modal plumbing
apps/desktop/src/components/sidebar/Sidebar.tsx, apps/desktop/src/components/sidebar/SidebarModal.tsx, apps/desktop/src/stores/SidebarModalStore.tsx
Add AppearanceModal launcher to Sidebar; Escape handling change; store gains highlightSelection and setter API.
Selection & visibility logic
apps/desktop/src/context/SelectedMarchersContext.tsx, apps/desktop/src/components/canvas/hooks/canvasListeners.movement.ts
SelectedMarchersContext prunes selections when marchers become hidden; movement listeners fetch visuals and guard updates on visuals presence.
Exporting & SVG generator changes
apps/desktop/src/components/exporting/utils/svg-generator.ts, apps/desktop/src/components/exporting/MarcherCoordinateSheet.tsx, tests
CanvasMarcher constructor option renamed to appearances; MarcherCoordinateSheet narrows marcherPages type; tests adjusted for casts/types.
E2E tests & helpers
apps/desktop/e2e/tests/appearance-editor.spec.mts, apps/desktop/e2e/utils/marchers.mts
New Playwright E2E test for appearance editor workflows and helper to create marchers programmatically.
Tests, mocks & typing tweaks
apps/desktop/src/__mocks__/*, apps/desktop/src/db-functions/__test__/*, apps/desktop/src/global/classes/__test__/*
Mocks adjusted to Partial/DatabaseMarcherPage; added tests for tag mapping and getPagesInOrder; removed SectionAppearance tests; many type casts added.
Package / i18n / editor config
apps/desktop/package.json, apps/desktop/i18n/{en,es,ja,pt-BR}.json, .vscode/settings.json, cspell.config.js
Bumped desktop package to 0.0.15-alpha; added dependencies @dnd-kit/core, @tanstack/react-virtual; new i18n keys for selection/tag/family/appearance; cSpell/VSCode setting tweaks.
Constants & deletions
apps/desktop/src/global/Constants.ts, apps/desktop/src/global/classes/SectionAppearance.ts (deleted)
Removed several deprecated table name constants; deleted legacy SectionAppearance class.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as Tag Editor / TagButtons
    participant Mutations as Tag Mutations
    participant DB as Database (drizzle)
    participant Query as React Query Cache
    participant Visuals as marcherWithVisuals
    participant Canvas

    User->>UI: create tag / add marchers to tag
    UI->>Mutations: call createNewTagFromMarcherIds / createTagAppearances
    Mutations->>DB: perform transaction (insert tag, marcher_tags, tag_appearances, history)
    DB-->>Mutations: return created records
    Mutations->>Query: invalidate tag queries and marcher-with-visuals keys
    Query->>Visuals: refetch and recompute marcher visuals map
    Visuals-->>Canvas: emit updated MarcherVisualMap
    Canvas-->>User: re-render marchers with new appearances
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing extra attention:
    • useMarchersWithVisuals: merge/priority logic, query keys and invalidation correctness.
    • appearance model converters: color parsing/formatting and boolean <-> integer mapping.
    • DB migrations ordering, schema compatibility and snapshots.
    • updateMarcherPages return-type change (ids) — audit callers and tests.
    • CanvasMarcher.setAppearance and fabric.js lifecycle (shape recreation and reattachment).
    • Virtualized MarcherList: sizing, keyboard/selection/focus behavior.
    • E2E tests: selector stability and timing (potential flakiness).

Possibly related PRs

Suggested reviewers

  • dukcc

"🐰 I hopped through code and left a trail,
Tags and colors hop on each marcher’s tail,
Pages and visuals now dance in sync,
Tiny carrots of features—give them a blink! 🥕"

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.85% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Tagging, styling, and per-page marcher customization' clearly and concisely summarizes the main changes in the pull request, focusing on the three primary feature additions.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch tagging

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

❤️ Share

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

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Dec 6, 2025

Deploying with  Cloudflare Workers  Cloudflare Workers

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

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
openmarch-website 9318bac Dec 06 2025, 06:04 AM

Copy link
Contributor

@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: 16

Caution

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

⚠️ Outside diff range comments (3)
packages/ui/src/components/base/Button.tsx (1)

19-19: Remove trailing whitespace in red variant className.

There's a trailing space in the red variant definition that should be trimmed for consistency.

-                red: ["bg-red text-text-invert "],
+                red: ["bg-red text-text-invert"],
apps/desktop/src/hooks/queries/useSectionAppearances.ts (1)

111-157: Potential race condition in query invalidations.

The conditional invalidations for byId and bySection queries (lines 128-141) execute in parallel with the main invalidation chain (lines 143-151), which could cause inconsistent cache states if marcherWithVisualsKeys is invalidated before the specific queries complete.

Consider chaining all invalidations sequentially:

         onSuccess: (_, variables) => {
             // Invalidate specific queries
             const itemIds = new Set<number>();
             const sections = new Set<string>();

             for (const modifiedArgs of variables) {
                 itemIds.add(modifiedArgs.id);
                 if (modifiedArgs.section) {
                     sections.add(modifiedArgs.section);
                 }
             }

-            // Invalidate by ID queries
-            if (itemIds.size > 0)
-                void qc.invalidateQueries({
-                    queryKey: Array.from(itemIds).map((id) =>
-                        sectionAppearanceKeys.byId(id),
-                    ),
-                });
-
-            // Invalidate by section queries
-            if (sections.size > 0)
-                void qc.invalidateQueries({
-                    queryKey: Array.from(sections).map((section) =>
-                        sectionAppearanceKeys.bySection(section),
-                    ),
-                });
-
-            void qc
+            // Build promises for all invalidations
+            const invalidations: Promise<void>[] = [];
+            
+            if (itemIds.size > 0) {
+                invalidations.push(
+                    qc.invalidateQueries({
+                        queryKey: Array.from(itemIds).map((id) =>
+                            sectionAppearanceKeys.byId(id),
+                        ),
+                    })
+                );
+            }
+            
+            if (sections.size > 0) {
+                invalidations.push(
+                    qc.invalidateQueries({
+                        queryKey: Array.from(sections).map((section) =>
+                            sectionAppearanceKeys.bySection(section),
+                        ),
+                    })
+                );
+            }
+            
+            invalidations.push(
+                qc.invalidateQueries({
+                    queryKey: [KEY_BASE],
+                })
+            );
+            
+            // Wait for all invalidations before invalidating dependent queries
+            void Promise.all(invalidations).then(() => {
+                void qc.invalidateQueries({
+                    queryKey: marcherWithVisualsKeys.all(),
+                });
+            });
-                .invalidateQueries({
-                    queryKey: [KEY_BASE],
-                })
-                .then(() => {
-                    void qc.invalidateQueries({
-                        queryKey: marcherWithVisualsKeys.all(),
-                    });
-                });
         },
apps/desktop/src/global/classes/canvasObjects/CanvasMarcher.ts (1)

66-132: Excellent refactoring: centralized shape creation.

The static createMarkerShape method centralizes all shape creation logic (circle, square, triangle, X) in one place. This:

  • Eliminates code duplication between constructor and setAppearance
  • Makes shape creation consistent
  • Simplifies future shape additions
  • Properly handles the X shape's special rendering needs (using stroke color instead of fill)
🧹 Nitpick comments (30)
apps/desktop/src/components/sidebar/SidebarModal.tsx (1)

18-30: Consider removing unused isFocused tracking.

The isFocused state is tracked (lines 8-16) and included in the effect dependencies (line 30), but it's no longer used after removing the focus check from the Escape key handler (line 20).

Apply this diff to remove the unused state:

 export default function SidebarModal() {
     const { isOpen, content, setOpen } = useSidebarModalStore();
-    const [isFocused, setIsFocused] = useState(false);
-
-    const handleFocus = () => {
-        setIsFocused(true);
-    };
-
-    const handleBlur = () => {
-        setIsFocused(false);
-    };

     useEffect(() => {
         const handleKeyDown = (event: KeyboardEvent) => {
             if (event.key === "Escape") {
                 setOpen(false);
             }
         };

         window.addEventListener("keydown", handleKeyDown);

         return () => {
             window.removeEventListener("keydown", handleKeyDown);
         };
-    }, [setOpen, isFocused]);
+    }, [setOpen]);

     return (
         <div
             tabIndex={0}
-            onFocus={handleFocus}
-            onBlur={handleBlur}
             className={`rounded-6 border-stroke bg-modal shadow-fg-1 backdrop-blur-32 absolute top-0 left-[50px] z-40 h-full min-h-0 max-w-[36rem] overflow-hidden border p-12 outline-hidden ${
                 isOpen ? "animate-scale-in flex" : "hidden"
             }`}
         >
             {content}
         </div>
     );
 }
apps/desktop/src/__mocks__/generators.ts (1)

362-401: Redundant type assertion.

The explicit as DatabaseMarcherPage assertion on line 400 is unnecessary since the function's return type is already declared as DatabaseMarcherPage (line 372). TypeScript will automatically enforce this type constraint.

Apply this diff to remove the redundant assertion:

     return {
         id,
         marcher_id: marcher.id,
         page_id: page.id,
         x,
         y,
         path_data_id: pathDataId,
         path_start_position: pathStartPosition,
         path_end_position: pathEndPosition,
         notes,
         rotation_degrees: rotationDegrees,
         created_at: faker.date.recent().toISOString(),
         updated_at: faker.date.recent().toISOString(),
-    } as DatabaseMarcherPage;
+    };
 };
apps/desktop/src/components/tags/TagsModel.tsx (1)

1-5: Filename typo: "TagsModel.tsx" should likely be "TagsModal.tsx".

The filename contains "Model" but the component is TagsModalLauncher, which suggests this should be TagsModal.tsx for consistency with the component naming.

apps/desktop/src/components/ui/ColorPicker.tsx (2)

36-47: Docstring mentions non-existent size parameter.

Line 44 documents a size parameter ("Compact or default") that doesn't exist in the component's props interface.

Apply this diff:

 * @param defaultColor - The default color of the color picker
 * @param onChange - The function to call when the color changes
+* @param onBlur - The function to call when the color picker loses focus
 * @param className - The class name of the color picker
-* @param size - Compact or default
 * @param doNotUseForm - Whether to use the form field or not (if true, the color picker will not be inside a form field)

129-196: Significant code duplication between form and non-form paths.

The popover content (lines 129-196) is nearly identical to DefaultColorPicker (lines 222-290). Since DefaultColorPicker already exists, reuse it in the else branch to eliminate duplication.

Apply this diff to the else branch:

     else
         return (
             <FormField
                 label={label}
                 tooltip={tooltip}
                 ref={pickerRef}
                 className={className}
             >
-                <div className="flex items-center gap-8">
-                    <Popover.Root>
-                        ... (duplicate content)
-                    </Popover.Root>
-
-                    <Button
-                        ... (duplicate content)
-                    </Button>
-                </div>
+                {internalComponent}
             </FormField>
         );

Also applies to: 222-290

apps/desktop/src/global/classes/__test__/MarcherPage.test.ts (1)

65-65: Consider removing Partial<> type wrapper for stronger type safety.

The test data is declared as Partial<DatabaseMarcherPage>[] but then cast to DatabaseMarcherPage[] when calling the function. Since the test data appears to include all required fields, using Partial<> with an explicit cast undermines TypeScript's compile-time type checking and could mask missing fields.

Apply this diff to use the full type:

-        const testMarcherPages: Partial<DatabaseMarcherPage>[] = [
+        const testMarcherPages: DatabaseMarcherPage[] = [
             {
                 id: 1,
                 marcher_id: 1,
                 ...
             },
         ];

         const result = databaseMarcherPagesToMarcherPages(
-            testMarcherPages as DatabaseMarcherPage[],
+            testMarcherPages,
         );

Also applies to: 111-111

apps/desktop/src/hooks/queries/usePages.ts (1)

243-249: Parameter naming inconsistency: tx used for DbConnection.

The getPagesInOrder function is called with { tx: db }, but db is a DbConnection, not a DbTransaction. While the function signature accepts DbConnection | DbTransaction, the parameter name tx (short for transaction) is misleading when passing a connection.

Consider renaming the parameter in getPagesInOrder to something like conn or keeping it generic, or update the call site comment to clarify. This is a minor naming nit.

-        queryFn: async () => await getPagesInOrder({ tx: db }),
+        queryFn: async () => await getPagesInOrder({ tx: db }), // db is DbConnection, tx accepts both
apps/desktop/src/components/inspector/MarcherEditor.tsx (1)

485-493: Redundant Set creation for selectedMarcherIds.

selectedMarcherIds is already computed as a Set via useMemo at line 186-189. Creating a new Set here is redundant.

                            <TagButtons
-                                selectedMarcherIds={
-                                    new Set(
-                                        selectedMarchers.map(
-                                            (marcher) => marcher.id,
-                                        ),
-                                    )
-                                }
+                                selectedMarcherIds={selectedMarcherIds}
                            />
apps/desktop/e2e/utils/marchers.mts (1)

1-24: New e2e helper for creating marchers.

The helper encapsulates the marcher creation flow well. However, some selectors may be fragile:

  1. Line 10: { name: "Other" } - this appears to be a fallback/placeholder label. Consider using a more stable selector or adding a data-testid.

  2. Lines 19-23: The selector .filter({ hasText: "MarchersMarcher" }).nth(4) is fragile as it depends on specific DOM structure and index positioning. Consider adding a data-testid to the target element for more reliable selection.

-    await page
-        .locator("div")
-        .filter({ hasText: "MarchersMarcher" })
-        .nth(4)
-        .press("Escape");
+    // Consider using a more stable selector like:
+    // await page.getByTestId("marcher-list-container").press("Escape");
+    await page
+        .locator("div")
+        .filter({ hasText: "MarchersMarcher" })
+        .nth(4)
+        .press("Escape");
apps/desktop/src/components/marcher/MarcherList.tsx (1)

318-327: Type inconsistency: RefObject vs MutableRefObject for changesRef.

VirtualizedMarcherRowsProps uses RefObject<{ [key: number | string]: unknown }> (line 326), but MarcherRowProps uses MutableRefObject<{ [key: number | string]: unknown }> (line 403). Since changesRef is being mutated (values are assigned in handleChange), MutableRefObject is the correct type.

 interface VirtualizedMarcherRowsProps {
     localMarchers: Marcher[];
     isEditing: boolean;
     handleChange: (value: string, attribute: string, marcherId: number) => void;
     handleDeleteMarcher: (marcherId: number) => void;
     sectionOptions: SectionOption[];
     sectionLabelMap: Record<string, string>;
     selectPlaceholder: string;
-    changesRef: RefObject<{ [key: number | string]: unknown }>;
+    changesRef: MutableRefObject<{ [key: number | string]: unknown }>;
 }
apps/desktop/src/global/classes/canvasObjects/OpenMarchCanvas.ts (1)

23-23: Type workaround in getRoundedCoordinate is safe but could be tightened

Switching fakeMarcherPage to Partial<MarcherPage> and casting to MarcherPage only at the getRoundCoordinates call avoids compile-time breakage from newly required fields and doesn’t change runtime behavior, since only x and y are used.

You might consider defining a narrower helper type (e.g. type CoordMarcherPage = Pick<MarcherPage, "id" | "marcher_id" | "page_id" | "x" | "y">;) and updating getRoundCoordinates to accept that (or just { x, y }) long-term, to drop the as MarcherPage cast.

Also applies to: 1365-1382

apps/desktop/src/components/exporting/MarcherCoordinateSheet.tsx (1)

58-60: Narrowed marcher-page type and row rendering look correct

Restricting marcherPages to a Pick of only the fields needed for coordinate output is a nice decoupling from the full MarcherPage/appearance surface, and state initialization follows that type correctly. The new <tbody> implementation cleanly joins each marcher page to its Page, uses measureRangeString(page) when includeMeasuresState is true, and renders X/Y via ReadableCoords with the configured rounding and terse/verbose modes. No functional issues spotted here.

If you end up reusing this subset elsewhere, consider extracting a shared alias (e.g. type MarcherPageCoords = Pick<MarcherPage, "id" | "marcher_id" | "page_id" | "x" | "y">;) to avoid repeating the Pick.

Also applies to: 244-247, 311-312, 526-596

apps/desktop/src/components/marcher/appearance/SectionAppearanceList.tsx (1)

62-71: Align availableSections logic and button disabled state with intent

Right now availableSections is all section names (lines 64–67) while the comment says “sections without appearances”, and the “Add section style” trigger is only disabled when availableSections.length === 0 (Line 97), which will never happen as long as SECTIONS is non‑empty. The real constraint is whether there is any section not present in takenSections.

Consider:

  • Deriving availableSections from SECTIONS but filtering out takenSections, and
  • Using that filtered list both for the “All” tab and for the disabled condition on the trigger.

For example:

-    const defaultShapeType = "circle";
-
-    // Get available sections (sections without appearances)
-    const availableSections = Object.values(SECTIONS).map(
-        (section) => section.name,
-    );
+    const defaultShapeType = "circle";
@@
-    /** A set of all sections that have an appearance */
-    const takenSections = useMemo(() => {
-        return new Set(
-            sectionAppearancesSorted.map((appearance) => appearance.section),
-        );
-    }, [sectionAppearancesSorted]);
+    /** A set of all sections that have an appearance */
+    const takenSections = useMemo(
+        () =>
+            new Set(
+                sectionAppearancesSorted.map(
+                    (appearance) => appearance.section,
+                ),
+            ),
+        [sectionAppearancesSorted],
+    );
+
+    // Sections without existing appearances
+    const availableSections = useMemo(
+        () =>
+            Object.values(SECTIONS)
+                .map((section) => section.name)
+                .filter((name) => !takenSections.has(name)),
+        [takenSections],
+    );

The rest of the code (trigger disabled={availableSections.length === 0} and availableSections.map(...) in the “All” tab) can remain as is and will now reflect the actual availability.

Also applies to: 95-103, 156-179

apps/desktop/src/db-functions/__test__/tag.test.ts (1)

287-425: Consider avoiding fc.sample inside properties for clearer shrinking and reproducibility

The property-based tests under "multiple appearances of same tag" (around Lines 289–425) create additional randomness via fc.sample(...) inside the fc.property body. This works, but:

  • It makes failing cases slightly harder to reproduce from a seed.
  • It bypasses fast-check’s normal shrinking on those inner generated values.

If you ever need to debug a failing case here, it will be simpler to express these as proper arbitraries instead, for example by:

  • Defining an arbitrary for the per-tag appearance start pages (e.g., fc.uniqueArray(fc.integer(...))), and
  • Passing that arbitrary as a normal argument to fc.property instead of sampling inside.

This is a style/perf tweak only; the current tests are logically correct.

apps/desktop/src/components/marcher/appearance/AppearanceModal.tsx (1)

9-36: Clarify naming and hard localStorage dependency in AppearanceModal

Two minor points:

  1. The default export is still called MetronomeModal (Lines 9–24) even though this file is AppearanceModal.tsx and the contents are all appearance-related. Renaming the component (and call sites) to something like AppearanceModal or AppearanceModalLauncher would make this easier to discover and reason about.

  2. AppearanceModalContents reads from localStorage at render time (Lines 33–36). That’s fine in the Electron renderer, but it will throw if this component is ever imported in a non-browser test environment. A small guard keeps it robust:

-    const localStorageMode = localStorage.getItem("marcher-appearance-mode");
+    const localStorageMode =
+        typeof window !== "undefined"
+            ? window.localStorage.getItem("marcher-appearance-mode")
+            : null;

Both are non-breaking cleanups that improve maintainability and testability.

apps/desktop/src/components/canvas/Canvas.tsx (1)

10-11: Avoid duplicating marcher fetching now that marcherWithVisualsQueryOptions exists

The new marcherWithVisualsQueryOptions integration (Lines 10–11, 56–58) looks good, and the additional guards on marcherVisuals == null before rendering are sensible.

One small inefficiency: you still have a separate useQuery(allMarchersQueryOptions()) (Line 53), but marcherWithVisualsQueryOptions itself calls queryClient.fetchQuery(allMarchersQueryOptions()) internally when building the visual groups. That means:

  • Marcher data is fetched twice at the query layer (once via useQuery, once via fetchQuery), and
  • In several effects (Lines ~220–289, 305–374, 438–447) you only really need marcher IDs, which can likely be derived from marcherVisuals instead.

If you want to tighten this up, you could:

  • Drop the top-level const { data: marchers } = useQuery(allMarchersQueryOptions());, and
  • Derive IDs from marcherVisuals (e.g., Object.keys(marcherVisuals).map(Number) or via a helper on the visual group structure) where you currently rely on marchers.

This would centralize marcher data loading in the visuals query and avoid redundant fetching, while keeping the current rendering guards intact.

Also applies to: 56-58, 220-289, 305-374, 438-447

apps/desktop/src/components/marcher/appearance/TagAppearanceList.tsx (1)

34-36: Map stores tag.name instead of the derived display name.

tagNamesByTagId stores tag.name which can be null, but consumers call getTagName with this value anyway. This works but is slightly misleading since the map doesn't contain "tag names" but raw name values.

Consider storing the derived name directly for clarity:

     const tagNamesByTagId = useMemo(() => {
-        return new Map(allTags?.map((tag) => [tag.id, tag.name]) ?? []);
+        return new Map(
+            allTags?.map((tag) => [
+                tag.id,
+                getTagName({ tag_id: tag.id, name: tag.name }),
+            ]) ?? [],
+        );
     }, [allTags]);
apps/desktop/src/components/inspector/marcher/TagEditor.tsx (3)

98-98: Non-null assertion on context could cause runtime error.

useSelectedPage()! assumes the context is always available. If this component is rendered outside the provider, it will throw.

Consider adding a guard or using optional chaining:

-    const { selectedPage } = useSelectedPage()!;
+    const selectedPageContext = useSelectedPage();
+    const selectedPage = selectedPageContext?.selectedPage;

193-196: Hardcoded UI strings should use i18n (Tolgee).

Several user-facing strings are hardcoded instead of using the <T> component or t() function: "Rename tag", "Edit appearance", "Delete tag".

Replace hardcoded strings with translation keys:

-                            <label className="text-text-subtitle text-sm">
-                                Rename tag
-                            </label>
+                            <label className="text-text-subtitle text-sm">
+                                <T keyName="tags.rename" />
+                            </label>

Also applies to: 215-216, 221-222, 228-229


308-313: Another hardcoded string that should be internationalized.

"Define a name for the new tag (optional)" should use a translation key.

apps/desktop/src/components/marcher/appearance/AppearanceEditor.tsx (1)

56-68: Unnecessary async/await on non-async callback.

handleUpdateAppearance returns void per the type signature, so the async keyword and await are unnecessary and may cause confusion.

-    async function handleChange(changes: Partial<AppearanceComponentOptional>) {
-        await handleUpdateAppearance({
+    function handleChange(changes: Partial<AppearanceComponentOptional>) {
+        handleUpdateAppearance({
             visible:
                 changes.visible !== undefined
                     ? changes.visible
                     : appearance.visible,
             label_visible:
                 changes.label_visible !== undefined
                     ? changes.label_visible
                     : appearance.label_visible,
             ...changes,
         });
     }
apps/desktop/src/components/toolbar/tabs/SelectTab.tsx (2)

19-19: Import from index file instead of specific module.

Based on learnings, db-functions should be imported from the index file.

-import { getTagName } from "@/db-functions/tag";
+import { getTagName } from "@/db-functions";

45-45: Non-null assertion on context hook.

Similar to TagEditor.tsx, useSelectedMarchers()! assumes the context is always available.

Consider adding a guard:

-    const { selectedMarchers, setSelectedMarchers } = useSelectedMarchers()!;
+    const context = useSelectedMarchers();
+    if (!context) {
+        throw new Error("useHandleSelect must be used within SelectedMarchersProvider");
+    }
+    const { selectedMarchers, setSelectedMarchers } = context;
.cursor/rules/basic-database-interaction.mdc (1)

139-162: Consider returning early without warning for expected empty input.

The console.warn on line 148 may be noisy if empty arrays are a valid use case. Consider whether this should be a silent early return or if the warning is intentional for debugging.

 export async function create{tableName}s({
     newItems,
     db,
 }: {
     newItems: New{tableName}Args[];
     db: DbConnection;
 }): Promise<Database{tableName}[]> {
     if (newItems.length === 0) {
-        console.warn("No new items to create");
         return [];
     }
apps/desktop/src/global/classes/MarcherVisualGroup.ts (1)

125-146: Complex visibility logic may benefit from clarification.

The isHidden method's logic is subtle: when there are multiple appearances and the first (marcherPage) is visible, it checks the second appearance's visibility. The inline comment explains this, but the logic could be clearer.

Consider extracting the visibility resolution to a helper or adding a more explicit comment about why marcherPage visibility is skipped when true:

     static isHidden(
         appearancesInput:
             | AppearanceComponentOptional[]
             | AppearanceComponentOptional,
     ) {
         const appearances = Array.isArray(appearancesInput)
             ? appearancesInput
             : [appearancesInput];

-        let visible: boolean;
-        if (appearances.length > 0)
-            if (appearances.length > 1 && appearances[0].visible) {
-                // If there is more than one appearance, and the marcherPage appearance is true, use the next one
-                // This is because the marcherPage will almost always be visible
-                visible = appearances[1].visible;
-            } else {
-                visible = appearances[0].visible;
-            }
-        else visible = true;
+        if (appearances.length === 0) {
+            return false; // Default to visible when no appearances
+        }
+
+        // Priority: If marcherPage (first) is visible and there's a tag/section appearance,
+        // defer to the tag/section appearance since marcherPage.visible is usually true.
+        const visible =
+            appearances.length > 1 && appearances[0].visible
+                ? appearances[1].visible
+                : appearances[0].visible;

         return !visible;
     }
apps/desktop/src/hooks/queries/useMarchersWithVisuals.ts (1)

147-147: Document rationale for eslint-disable.

The @tanstack/query/exhaustive-deps disable suppresses warnings about queryClient not being in the key. Consider adding a brief comment explaining why this is intentional (e.g., queryClient is stable singleton).

-        // eslint-disable-next-line @tanstack/query/exhaustive-deps
+        // eslint-disable-next-line @tanstack/query/exhaustive-deps -- queryClient is a stable singleton from App.tsx
         queryKey: marcherWithVisualsKeys.byPageId(pageId!),
apps/desktop/electron/database/migrations/schema.ts (1)

27-39: Good pattern: reusable appearance columns.

Exporting appearance_columns as a shared object and spreading it into multiple tables (marcher_pages, section_appearances, tag_appearances) is an excellent pattern that:

  • Ensures consistency across appearance-related fields
  • Reduces duplication
  • Makes schema evolution easier

Consider adding JSDoc comments to document each field in appearance_columns, especially shape_type, equipment_name, and equipment_state, to clarify their purpose and expected values.

apps/desktop/src/entity-components/appearance.ts (1)

17-32: Consider edge case handling in parseRgbaColor.

The regex pattern handles both rgb() and rgba() formats correctly, with a sensible default fallback { r: 0, g: 0, b: 0, a: 1 }.

However, the function silently returns a default black color for invalid input. Consider whether this should:

  1. Log a warning for invalid color strings
  2. Return null instead of a default color
  3. Throw an error for invalid input

Example enhancement:

 function parseRgbaColor(colorStr: string): RgbaColor {
     const match = colorStr.match(
         /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d*\.?\d+))?\)/,
     );
     if (match) {
         return {
             r: parseInt(match[1], 10),
             g: parseInt(match[2], 10),
             b: parseInt(match[3], 10),
             a: match[4] ? parseFloat(match[4]) : 1,
         };
     }
+    console.warn(`Invalid color string: ${colorStr}, using default black`);
     return { r: 0, g: 0, b: 0, a: 1 };
 }
apps/desktop/src/global/classes/canvasObjects/CanvasMarcher.ts (1)

36-37: Verify if dotObject needs to be public.

The dotObject field has been changed from private to public (no explicit modifier). This makes it accessible outside the class, which could lead to unintended mutations.

While this change appears necessary for the setAppearance method to dynamically replace the marker shape, consider:

  1. Whether external code needs to access this field
  2. If a getter method would be more appropriate
  3. Adding documentation about mutability expectations
apps/desktop/src/db-functions/tag.ts (1)

372-385: Inconsistent naming for inTransaction functions.

The tag appearance inTransaction functions (createTagAppearancesInTransaction, updateTagAppearancesInTransaction, deleteTagAppearancesInTransaction) don't follow the underscore-prefix convention used by marcher tag functions (_createMarcherTagsInTransaction, _updateMarcherTagsInTransaction).

Per coding guidelines, private database operation functions should be prefixed with underscore.

Also applies to: 410-436, 463-476

@AlexDumo AlexDumo requested a review from dukcc December 6, 2025 06:08
@AlexDumo
Copy link
Collaborator Author

AlexDumo commented Dec 6, 2025

@dukcc check out the new tagging UI in the marcher editor, the marcher appearance sidebar, and the select topbar

Copy link
Contributor

@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: 2

♻️ Duplicate comments (2)
apps/desktop/src/components/ui/ColorPicker.tsx (1)

61-64: Both onChange and onBlur fire on close - potential double callback.

This was previously flagged but remains unaddressed. When handleClose is called, both callbacks fire sequentially, which could cause duplicate state updates if a consumer provides the same handler to both props.

apps/desktop/src/db-functions/tag.ts (1)

101-137: Missing public wrapper functions for updateTags and deleteTags.

Based on coding guidelines, all database mutations should use transactionWithHistory for automatic undo/redo support. While createTags (lines 69-84) correctly wraps _createTagsInTransaction with transactionWithHistory, the updateTagsInTransaction and deleteTagsInTransaction functions are exported directly without public wrappers.

This forces callers to either use the transaction functions directly (bypassing history tracking) or manually wrap them. For consistency and to ensure proper undo/redo support, add public wrapper functions similar to createTags.

Based on learnings, database mutations must use transactionWithHistory for automatic undo/redo support.

🧹 Nitpick comments (4)
apps/desktop/src/components/ui/ColorPicker.tsx (1)

27-27: Consider renaming doNotUseForm to avoid double-negative.

The flag name doNotUseForm requires mental negation to understand behavior. A positive alternative like useStaticField, standalone, or disableFormWrapper would improve readability.

apps/desktop/src/components/inspector/marcher/TagEditor.tsx (1)

34-38: Consider showing a loading state.

The component returns null while tags are loading. For better UX, consider showing a loading indicator or skeleton, especially if the load takes noticeable time.

apps/desktop/src/components/marcher/appearance/AppearanceEditor.tsx (1)

32-37: Minor cleanup: simplify handleChange and tighten React type imports

If handleUpdateAppearance is intended to be synchronous (as currently typed), you can simplify a bit:

  • Drop async/await from handleChange and call it directly from handlers, removing the need for void wrappers.
  • Import ReactNode from "react" instead of using the React namespace, which avoids relying on a global React type and is more idiomatic.

Example diff:

+import type { ReactNode } from "react";
 import {
     AppearanceComponent,
     AppearanceComponentOptional,
 } from "@/entity-components/appearance";
@@
-const shapeIcons: Record<ShapeType, React.ReactNode> = {
+const shapeIcons: Record<ShapeType, ReactNode> = {
@@
-export function AppearanceEditor({
+export function AppearanceEditor({
@@
-    async function handleChange(changes: Partial<AppearanceComponentOptional>) {
-        await handleUpdateAppearance({
+    function handleChange(changes: Partial<AppearanceComponentOptional>) {
+        handleUpdateAppearance({
             visible:
                 changes.visible !== undefined
                     ? changes.visible
                     : appearance.visible,
@@
     function onColorChange(
         attribute: "fill_color" | "outline_color",
         color: RgbaColor | null,
     ) {
-        void handleChange({ [attribute]: color });
+        handleChange({ [attribute]: color });
     }
@@
     function onShapeChange(shape: string) {
-        // Convert the special "__none__" value to null
-        void handleChange({ shape_type: shape === "__none__" ? null : shape });
+        // Convert the special "__none__" value to null
+        handleChange({ shape_type: shape === "__none__" ? null : shape });
     }
     const handleVisibilityChange = () => {
-        void handleChange({ visible: !appearance.visible });
+        handleChange({ visible: !appearance.visible });
     };
     const handleLabelVisibilityChange = () => {
-        void handleChange({
-            label_visible: !appearance.label_visible,
-        });
+        handleChange({ label_visible: !appearance.label_visible });
     };

If you do expect handleUpdateAppearance to become async, instead update its type to () => Promise<void> so the current async/await + void pattern stays consistent.

Also applies to: 56-88

apps/desktop/src/db-functions/__test__/page.test.ts (1)

2878-3161: Tighten getPagesInOrder tests for robustness and clarity

The tests broadly look good and exercise ordering, mapping, and edge cases. A few small refinements would make them more future‑proof and self‑documenting:

  1. Avoid relying on beat.id === beat.position where not necessary
    Several expectations assume specific positions from hard‑coded start_beat IDs (e.g., expecting positions [0, 2, 5, 8] or [0, 1, 3, 7, 10, 12, 15]). If createBeats ever changes how position is assigned, these tests could fail even when getPagesInOrder is still correct.
    Consider:

    • Asserting only that beatObject.position values are strictly ascending (you already do this in the “many pages” test), and
    • Verifying the first page specifically (FIRST_PAGE_ID / FIRST_BEAT_ID), while deriving expectations for other positions by querying schema.beats or by comparing to sorted result.map(r => r.beatObject.position) instead of hard‑coding specific numeric positions.
      This keeps the tests focused on ordering semantics rather than internal assumptions about ID/position coupling.
  2. Prefer typed results over any
    In a few places you use let result: any; (e.g., “ordered by beat position ascending”, “join pages with their start beats”, and the last edge‑case test). Since DatabasePageWithBeat is imported and some tests already use it, you could strengthen typing without much friction, for example:

  •            let result: any;
    
  •            let result: DatabasePageWithBeat[] = [];
               await transaction(db, async (tx) => {
                   result = await getPagesInOrder({ tx });
               });
    
    This keeps the pattern compatible with the existing `transaction` helper while giving you compile‑time checks on the result shape.
    
    
    
  1. Align test description with setup for “identical beat positions”
    The test "should handle pages with identical beat positions (should not happen but test robustness)" currently uses createBeats with monotonically increasing positions and creates only a single non‑first page, so it does not actually exercise identical beat positions.
    Either:
    • Adjust the description to something like “should handle additional pages with valid beat positions”, or
    • If you truly want to cover identical beat.position values, insert beats manually via tx.insert(schema.beats) with the same position and create pages pointing at them, then assert that getPagesInOrder still returns without error and preserves a deterministic order.

Overall behavior is correct; these are maintainability and clarity tweaks you can adopt if you think they’re worthwhile.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f2dbf83 and d3b9fba.

📒 Files selected for processing (7)
  • apps/desktop/src/components/inspector/marcher/TagEditor.tsx (1 hunks)
  • apps/desktop/src/components/marcher/appearance/AppearanceEditor.tsx (1 hunks)
  • apps/desktop/src/components/ui/ColorPicker.tsx (3 hunks)
  • apps/desktop/src/db-functions/__test__/page.test.ts (2 hunks)
  • apps/desktop/src/db-functions/sectionAppearance.ts (2 hunks)
  • apps/desktop/src/db-functions/tag.ts (1 hunks)
  • apps/desktop/src/hooks/queries/useMarcherPages.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/hooks/queries/useMarcherPages.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Co-locate UI utility helper functions with their components
Always use absolute paths (with @/ alias for src/ directory) for file operations, never relative paths
Batch UI refresh operations where possible for performance optimization

When you need to use icons, use @phosphor-icons/react

Files:

  • apps/desktop/src/components/inspector/marcher/TagEditor.tsx
  • apps/desktop/src/db-functions/sectionAppearance.ts
  • apps/desktop/src/components/ui/ColorPicker.tsx
  • apps/desktop/src/db-functions/__test__/page.test.ts
  • apps/desktop/src/components/marcher/appearance/AppearanceEditor.tsx
  • apps/desktop/src/db-functions/tag.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.ts: Use useNextUndoGroup parameter to control transaction grouping in database operations
Use GroupFunction for multi-operation transactions with rollback capability
Locate database functions in electron/database/tables/ (legacy IPC-based) OR consolidate in src/global/classes/ (new shared SQL bridge pattern)
Place business logic in classes located in src/global/classes/
GroupFunction returns {responses: [], success: boolean} format, direct DB calls return DatabaseResponse<T> format
Prefer single transactions over multiple database calls for performance
Database entity types should use Drizzle inferred types: typeof table.$inferSelect and typeof table.$inferInsert, never manually defined interfaces that duplicate schema information
Internal database functions should use *Db() suffix and return raw data types, public wrapper functions should have the same name and apply DatabaseResponse wrapper
Omit auto-generated fields (id, timestamps) from NewEntityArgs type definitions using Omit utility type

**/*.ts: Always await when using transactionWithHistory() to ensure the database action finishes in time for query invalidation
Create React Query mutation hooks that wrap database operation functions and handle error/success callbacks with Tolgee internationalization
Use conToastError() with Tolgee keys for user-friendly error messages in mutation error handlers
Invalidate all relevant query keys after successful mutations, always invalidating beats first and waiting for completion before invalidating dependent queries like measures
Use query key factories from @/hooks/queries when invalidating queries to ensure consistency and maintainability

Files:

  • apps/desktop/src/db-functions/sectionAppearance.ts
  • apps/desktop/src/db-functions/__test__/page.test.ts
  • apps/desktop/src/db-functions/tag.ts
apps/desktop/src/db-functions/*.ts

📄 CodeRabbit inference engine (.cursor/rules/basic-database-interaction.mdc)

apps/desktop/src/db-functions/*.ts: Create a database function file in apps/desktop/src/db-functions/[tableName].ts when adding new database table interactions
Database function files must include proper TypeScript types with Database[TableName], New[TableName]Args, and Modified[TableName]Args interfaces
Database function files must include data transformation functions (e.g., realDatabase[TableName]ToDatabase[TableName]) to convert between database types and application types
All database mutations (create, update, delete) must use transactionWithHistory for automatic undo/redo support
Database mutations should implement separate 'inTransaction' functions (e.g., create[TableName]sInTransaction) that accept a DbTransaction parameter
Database function files must handle database-specific type transformations (e.g., boolean as 0|1 in SQLite) with explicit conversion functions
Database function files must implement comprehensive query functions (getAll, getById, etc.) before implementing mutations

Files:

  • apps/desktop/src/db-functions/sectionAppearance.ts
  • apps/desktop/src/db-functions/tag.ts
apps/desktop/src/{db-functions,hooks/queries}/*.ts

📄 CodeRabbit inference engine (.cursor/rules/basic-database-interaction.mdc)

Follow naming conventions: Database files as [tableName].ts (camelCase), hook files as use[TableName].ts (PascalCase), functions as get[TableNames], create[TableNames], update[TableNames], delete[TableNames]

Files:

  • apps/desktop/src/db-functions/sectionAppearance.ts
  • apps/desktop/src/db-functions/tag.ts
**/*[A-Z]*.ts

📄 CodeRabbit inference engine (.cursor/rules/component-database-interaction.mdc)

**/*[A-Z]*.ts: Always use transactionWithHistory() for operations that modify multiple database entities, need undo/redo support, or should be atomic
Keep database operation functions private and prefixed with underscore (_) in FeatureName.ts files, though they can still be exported for testing

Files:

  • apps/desktop/src/db-functions/sectionAppearance.ts
**/*.test.ts

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.test.ts: Use .test.ts suffix for test files and place mocks in __mocks__/ directories
Test internal *Db functions expecting raw data arrays, test public wrapper functions expecting DatabaseResponse format
Create foreign key references before testing dependent entities in database tests
Use setupTestSqlProxy() in beforeEach for database test setup

Test business logic functions independently of React components using real database operations (no mocks), and verify transaction rollback scenarios and query invalidation behavior

Files:

  • apps/desktop/src/db-functions/__test__/page.test.ts
**/*.{test,spec}.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/command-syntax.mdc)

When running vitest, use 'npx vitest run {test_here}'

Files:

  • apps/desktop/src/db-functions/__test__/page.test.ts
🧠 Learnings (28)
📚 Learning: 2025-11-27T02:03:46.836Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/basic-database-interaction.mdc:0-0
Timestamp: 2025-11-27T02:03:46.836Z
Learning: Applies to apps/desktop/src/db-functions/*.ts : Database function files must include data transformation functions (e.g., `realDatabase[TableName]ToDatabase[TableName]`) to convert between database types and application types

Applied to files:

  • apps/desktop/src/db-functions/sectionAppearance.ts
  • apps/desktop/src/db-functions/__test__/page.test.ts
  • apps/desktop/src/db-functions/tag.ts
📚 Learning: 2025-11-27T02:03:46.836Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/basic-database-interaction.mdc:0-0
Timestamp: 2025-11-27T02:03:46.836Z
Learning: Applies to apps/desktop/src/db-functions/index.ts : Update `apps/desktop/src/db-functions/index.ts` to export all new database function files

Applied to files:

  • apps/desktop/src/db-functions/sectionAppearance.ts
  • apps/desktop/src/db-functions/__test__/page.test.ts
  • apps/desktop/src/db-functions/tag.ts
📚 Learning: 2025-11-27T02:03:46.836Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/basic-database-interaction.mdc:0-0
Timestamp: 2025-11-27T02:03:46.836Z
Learning: Applies to apps/desktop/src/db-functions/*.ts : Database function files must include proper TypeScript types with `Database[TableName]`, `New[TableName]Args`, and `Modified[TableName]Args` interfaces

Applied to files:

  • apps/desktop/src/db-functions/sectionAppearance.ts
  • apps/desktop/src/db-functions/__test__/page.test.ts
  • apps/desktop/src/db-functions/tag.ts
📚 Learning: 2025-11-27T02:03:46.836Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/basic-database-interaction.mdc:0-0
Timestamp: 2025-11-27T02:03:46.836Z
Learning: Applies to apps/desktop/src/db-functions/*.ts : Database function files must handle database-specific type transformations (e.g., boolean as 0|1 in SQLite) with explicit conversion functions

Applied to files:

  • apps/desktop/src/db-functions/sectionAppearance.ts
  • apps/desktop/src/db-functions/__test__/page.test.ts
📚 Learning: 2025-11-27T02:03:46.836Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/basic-database-interaction.mdc:0-0
Timestamp: 2025-11-27T02:03:46.836Z
Learning: Applies to apps/desktop/src/db-functions/*.ts : All database mutations (create, update, delete) must use `transactionWithHistory` for automatic undo/redo support

Applied to files:

  • apps/desktop/src/db-functions/sectionAppearance.ts
  • apps/desktop/src/db-functions/tag.ts
📚 Learning: 2025-11-27T02:03:30.741Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-27T02:03:30.741Z
Learning: Applies to **/*.test.ts : Test internal `*Db` functions expecting raw data arrays, test public wrapper functions expecting `DatabaseResponse` format

Applied to files:

  • apps/desktop/src/db-functions/__test__/page.test.ts
📚 Learning: 2025-11-27T02:03:46.836Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/basic-database-interaction.mdc:0-0
Timestamp: 2025-11-27T02:03:46.836Z
Learning: Applies to apps/desktop/src/db-functions/*.ts : Database function files must implement comprehensive query functions (getAll, getById, etc.) before implementing mutations

Applied to files:

  • apps/desktop/src/db-functions/__test__/page.test.ts
  • apps/desktop/src/db-functions/tag.ts
📚 Learning: 2025-11-27T02:04:04.374Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/component-database-interaction.mdc:0-0
Timestamp: 2025-11-27T02:04:04.374Z
Learning: Applies to **/*.test.ts : Test business logic functions independently of React components using real database operations (no mocks), and verify transaction rollback scenarios and query invalidation behavior

Applied to files:

  • apps/desktop/src/db-functions/__test__/page.test.ts
📚 Learning: 2025-11-27T02:03:46.836Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/basic-database-interaction.mdc:0-0
Timestamp: 2025-11-27T02:03:46.836Z
Learning: Applies to apps/desktop/src/db-functions/*.ts : Database mutations should implement separate 'inTransaction' functions (e.g., `create[TableName]sInTransaction`) that accept a `DbTransaction` parameter

Applied to files:

  • apps/desktop/src/db-functions/__test__/page.test.ts
  • apps/desktop/src/db-functions/tag.ts
📚 Learning: 2025-11-27T02:03:30.741Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-27T02:03:30.741Z
Learning: Applies to **/*.test.ts : Create foreign key references before testing dependent entities in database tests

Applied to files:

  • apps/desktop/src/db-functions/__test__/page.test.ts
📚 Learning: 2025-11-27T02:03:46.836Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/basic-database-interaction.mdc:0-0
Timestamp: 2025-11-27T02:03:46.836Z
Learning: Applies to apps/desktop/src/{db-functions,hooks/queries}/*.ts : Follow naming conventions: Database files as `[tableName].ts` (camelCase), hook files as `use[TableName].ts` (PascalCase), functions as `get[TableNames]`, `create[TableNames]`, `update[TableNames]`, `delete[TableNames]`

Applied to files:

  • apps/desktop/src/db-functions/__test__/page.test.ts
  • apps/desktop/src/db-functions/tag.ts
📚 Learning: 2025-11-27T02:04:17.663Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/multi-query-hooks.mdc:0-0
Timestamp: 2025-11-27T02:04:17.663Z
Learning: Applies to src/hooks/**/*.test.ts?(x) : Test early return conditions (incomplete data) in multi-query hook combine functions

Applied to files:

  • apps/desktop/src/db-functions/__test__/page.test.ts
📚 Learning: 2025-11-27T02:04:17.663Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/multi-query-hooks.mdc:0-0
Timestamp: 2025-11-27T02:04:17.663Z
Learning: Applies to src/hooks/**/*.test.ts?(x) : Test data transformation logic in multi-query hooks with various data combinations

Applied to files:

  • apps/desktop/src/db-functions/__test__/page.test.ts
📚 Learning: 2025-11-27T02:04:17.663Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/multi-query-hooks.mdc:0-0
Timestamp: 2025-11-27T02:04:17.663Z
Learning: Applies to src/hooks/**/*.test.ts?(x) : Use real query options, not mocks, for integration testing of multi-query hooks

Applied to files:

  • apps/desktop/src/db-functions/__test__/page.test.ts
📚 Learning: 2025-11-27T02:04:29.206Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/unit-testing.mdc:0-0
Timestamp: 2025-11-27T02:04:29.206Z
Learning: Applies to **/{test,__tests__,tests}/**/*.{test,spec}.{ts,tsx,js,jsx} : Never use mocks for packages or database operations; all functionality should be testable with real implementations, except for mocking React context providers when rendering components that rely on hooks

Applied to files:

  • apps/desktop/src/db-functions/__test__/page.test.ts
📚 Learning: 2025-11-27T02:04:04.374Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/component-database-interaction.mdc:0-0
Timestamp: 2025-11-27T02:04:04.374Z
Learning: Applies to **/*.ts : Always await when using `transactionWithHistory()` to ensure the database action finishes in time for query invalidation

Applied to files:

  • apps/desktop/src/db-functions/__test__/page.test.ts
📚 Learning: 2025-11-27T02:04:17.663Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/multi-query-hooks.mdc:0-0
Timestamp: 2025-11-27T02:04:17.663Z
Learning: Applies to src/hooks/**/*.test.ts?(x) : Test multi-query hooks by mocking query options to return loading and error states

Applied to files:

  • apps/desktop/src/db-functions/__test__/page.test.ts
📚 Learning: 2025-11-27T02:04:23.301Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/repo.mdc:0-0
Timestamp: 2025-11-27T02:04:23.301Z
Learning: Applies to packages/**/*.{ts,tsx} : Prefer to use components in './packages/ui/src/components/base' folder for common items like buttons and inputs

Applied to files:

  • apps/desktop/src/components/marcher/appearance/AppearanceEditor.tsx
📚 Learning: 2025-11-27T02:03:30.741Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-27T02:03:30.741Z
Learning: Applies to **/*.{ts,tsx} : Always use absolute paths (with `@/` alias for `src/` directory) for file operations, never relative paths

Applied to files:

  • apps/desktop/src/components/marcher/appearance/AppearanceEditor.tsx
📚 Learning: 2025-11-27T02:04:23.301Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/repo.mdc:0-0
Timestamp: 2025-11-27T02:04:23.301Z
Learning: Applies to packages/ui/src/components/**/*.{ts,tsx} : All styles are stored in tailwind.css; match existing style patterns and color usage when making frontend components

Applied to files:

  • apps/desktop/src/components/marcher/appearance/AppearanceEditor.tsx
📚 Learning: 2025-11-27T02:03:30.741Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-27T02:03:30.741Z
Learning: Applies to **/*.{ts,tsx} : Co-locate UI utility helper functions with their components

Applied to files:

  • apps/desktop/src/components/marcher/appearance/AppearanceEditor.tsx
📚 Learning: 2025-11-27T02:03:46.836Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/basic-database-interaction.mdc:0-0
Timestamp: 2025-11-27T02:03:46.836Z
Learning: Applies to apps/desktop/src/db-functions/*.ts : Create a database function file in `apps/desktop/src/db-functions/[tableName].ts` when adding new database table interactions

Applied to files:

  • apps/desktop/src/db-functions/tag.ts
📚 Learning: 2025-11-27T02:03:30.741Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-27T02:03:30.741Z
Learning: Applies to **/*.ts : Locate database functions in `electron/database/tables/` (legacy IPC-based) OR consolidate in `src/global/classes/` (new shared SQL bridge pattern)

Applied to files:

  • apps/desktop/src/db-functions/tag.ts
📚 Learning: 2025-11-27T02:03:30.741Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-27T02:03:30.741Z
Learning: Applies to **/*.ts : Database entity types should use Drizzle inferred types: `typeof table.$inferSelect` and `typeof table.$inferInsert`, never manually defined interfaces that duplicate schema information

Applied to files:

  • apps/desktop/src/db-functions/tag.ts
📚 Learning: 2025-11-27T02:04:04.374Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/component-database-interaction.mdc:0-0
Timestamp: 2025-11-27T02:04:04.374Z
Learning: Applies to **/*[A-Z]*.ts : Keep database operation functions private and prefixed with underscore (_) in FeatureName.ts files, though they can still be exported for testing

Applied to files:

  • apps/desktop/src/db-functions/tag.ts
📚 Learning: 2025-11-27T02:04:04.374Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/component-database-interaction.mdc:0-0
Timestamp: 2025-11-27T02:04:04.374Z
Learning: Applies to **/*[A-Z]*.ts : Always use `transactionWithHistory()` for operations that modify multiple database entities, need undo/redo support, or should be atomic

Applied to files:

  • apps/desktop/src/db-functions/tag.ts
📚 Learning: 2025-11-27T02:03:30.741Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-27T02:03:30.741Z
Learning: Applies to **/*.ts : Internal database functions should use `*Db()` suffix and return raw data types, public wrapper functions should have the same name and apply `DatabaseResponse` wrapper

Applied to files:

  • apps/desktop/src/db-functions/tag.ts
📚 Learning: 2025-11-27T02:04:04.374Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/component-database-interaction.mdc:0-0
Timestamp: 2025-11-27T02:04:04.374Z
Learning: Separate business logic into `.ts` files, React integration into custom hooks, and database operations using transaction functions from `@/db-functions`

Applied to files:

  • apps/desktop/src/db-functions/tag.ts
🧬 Code graph analysis (4)
apps/desktop/src/components/inspector/marcher/TagEditor.tsx (6)
apps/desktop/src/hooks/queries/tags/queries.ts (2)
  • allTagsQueryOptions (203-211)
  • allMarcherTagsQueryOptions (250-258)
apps/desktop/src/db-functions/tag.ts (6)
  • DatabaseTag (23-23)
  • getTagName (25-28)
  • createNewTagFromMarcherIds (559-595)
  • createMarcherTags (600-623)
  • NewMarcherTagArgs (483-486)
  • deleteMarcherTags (689-709)
apps/desktop/src/stores/SidebarModalStore.tsx (1)
  • useSidebarModalStore (17-43)
apps/desktop/src/context/SelectedPageContext.tsx (1)
  • useSelectedPage (81-83)
apps/desktop/src/hooks/queries/tags/mutations.ts (5)
  • updateTagsMutationOptions (47-66)
  • deleteTagsMutationOptions (71-84)
  • createNewTagFromMarcherIdsMutationOptions (142-159)
  • createMarcherTagsMutationOptions (164-175)
  • deleteMarcherTagsMutationOptions (196-207)
apps/desktop/src/components/marcher/appearance/AppearanceModal.tsx (1)
  • AppearanceModalContents (26-80)
apps/desktop/src/db-functions/sectionAppearance.ts (1)
apps/desktop/src/entity-components/appearance.ts (5)
  • AppearanceComponent (35-43)
  • appearanceModelRawToParsed (65-79)
  • AppearanceComponentOptional (46-50)
  • AppearanceComponentRawOptional (53-57)
  • appearanceModelParsedToRawOptional (103-120)
apps/desktop/src/components/marcher/appearance/AppearanceEditor.tsx (3)
apps/desktop/src/entity-components/appearance.ts (2)
  • AppearanceComponent (35-43)
  • AppearanceComponentOptional (46-50)
packages/core/src/field/FieldTheme.ts (1)
  • RgbaColor (3-3)
apps/desktop/src/components/ui/ColorPickerMini.tsx (1)
  • ColorPickerMini (46-174)
apps/desktop/src/db-functions/tag.ts (3)
apps/desktop/src/db-functions/types.ts (1)
  • DbTransaction (8-13)
apps/desktop/src/db-functions/history.ts (1)
  • transactionWithHistory (42-138)
apps/desktop/src/entity-components/appearance.ts (6)
  • AppearanceComponent (35-43)
  • appearanceModelRawToParsed (65-79)
  • AppearanceComponentRaw (12-14)
  • appearanceModelParsedToRawOptional (103-120)
  • AppearanceComponentOptional (46-50)
  • AppearanceComponentRawOptional (53-57)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: pr-checks (build)
  • GitHub Check: build-and-release (windows-latest)
  • GitHub Check: build-and-release (macos-15-intel)
  • GitHub Check: build-and-release (ubuntu-24.04-arm)
  • GitHub Check: build-and-release (ubuntu-latest)
  • GitHub Check: build-and-release (macos-latest)
  • GitHub Check: build
  • GitHub Check: desktop-e2e
  • GitHub Check: test
🔇 Additional comments (14)
apps/desktop/src/components/ui/ColorPicker.tsx (1)

99-110: LGTM! DefaultColorPicker correctly invoked as JSX.

The previous concern about calling React components as functions has been properly addressed. The component is now rendered as a JSX element with props correctly passed as attributes.

apps/desktop/src/db-functions/tag.ts (3)

204-215: Good fix: User-provided visibility values are now preserved.

The past issue where modifiedTagAppearanceArgsToRealModifiedTagAppearanceArgs forced visible: true and label_visible: true has been correctly addressed. The converter now forwards the user-supplied values through appearanceModelParsedToRawOptional without overriding them.


559-595: LGTM: Proper transaction grouping for tag creation with marchers.

The createNewTagFromMarcherIds function correctly uses transactionWithHistory to atomically create both a tag and its associated marcher-tag relationships in a single transaction. This ensures data consistency and proper undo/redo support.


176-188: No action needed: Type design prevents visibility overrides on creation.

The NewTagAppearanceArgs type explicitly omits visible and label_visible fields (via Partial<AppearanceComponent>, which itself omits these fields from the raw database types). New tag appearances always default to visible; callers cannot pass visible: false at creation time due to TypeScript type constraints. To hide an appearance after creation, use the separate ModifiedTagAppearanceArgs type and updateTagAppearances function, which properly accepts visibility changes.

Likely an incorrect or invalid review comment.

apps/desktop/src/components/inspector/marcher/TagEditor.tsx (3)

239-346: LGTM: New tag creation flow is well-implemented.

The component properly handles tag creation with keyboard shortcuts, focus management, and mutation loading states. The use of createNewTagFromMarcherIds ensures atomic creation of both the tag and its marcher associations.


361-373: LGTM: Correctly creates marcher-tag associations.

The callback properly maps each selected marcher ID to a NewMarcherTagArgs object with the target tag ID, ensuring all selected marchers are associated with the chosen tag in a single operation.


449-467: LGTM: Critical bug fixed - now correctly filters by selected marchers.

The past issue where the filter only checked tag_id (deleting the tag from ALL marchers) has been correctly addressed. Lines 455-461 now filter by both tag_id AND whether the marcher_id is in selectedMarcherIds, ensuring only associations for selected marchers are removed.

Minor refinement: Line 462 could check marcherTagIds.length > 0 for clarity, though the current check works.

apps/desktop/src/db-functions/sectionAppearance.ts (5)

8-23: LGTM: Clean refactoring to shared appearance types.

The type definitions now properly extend AppearanceComponent and leverage shared appearance model types from @/entity-components/appearance.ts. This eliminates duplication of color/shape field definitions and ensures consistency across all appearance entities.


30-37: LGTM: Transformation properly delegates to shared converter.

The refactoring to use appearanceModelRawToParsed removes manual color/shape/boolean parsing and ensures consistency with other appearance entities.


70-80: LGTM: Past issue resolved - section field now included.

The converter now properly includes section: args.section (line 75), fixing the previously reported bug where section updates were silently ignored. The refactoring to use appearanceModelParsedToRawOptional maintains clean separation of concerns.


139-139: LGTM: Appropriate log level.

Using console.debug for this informational message reduces log noise compared to console.log.


47-58: This review comment does not identify an issue.

The forced visibility defaults (visible: true, label_visible: true) in the converter are safe and intentional. The UI component (SectionAppearanceList.tsx) already explicitly sets both fields to true before calling createSectionAppearances, and no callers in the codebase attempt to pass visible: false or omit these fields. The converter enforces these defaults as a defensive guarantee, which is a standard pattern. Additionally, the database schema itself defaults both fields to 1 (true), providing a third layer of safety. No workflows would be broken by this behavior.

apps/desktop/src/components/marcher/appearance/AppearanceEditor.tsx (1)

39-53: AppearanceEditor structure and UI wiring look solid

Props, appearance merging, visibility/label toggles, color pickers, and shape select are all wired coherently, and the component follows the absolute import and phosphor-icon guidelines. ColorPickerMini usage (update on blur with nullable RgbaColor) matches the shared appearance types, and the Select values (shapeOptions + "__none__") line up cleanly with shape_type. As per coding guidelines and retrieved learnings, this is consistent with the existing UI patterns.

Also applies to: 90-172

apps/desktop/src/db-functions/__test__/page.test.ts (1)

16-17: New imports are consistent and used correctly

getPagesInOrder and DatabasePageWithBeat are imported and exercised below; the additions fit the existing test file structure without issues.

@AlexDumo AlexDumo enabled auto-merge (squash) December 8, 2025 22:50
Copy link
Contributor

@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: 2

♻️ Duplicate comments (3)
apps/desktop/src/components/toolbar/tabs/SelectTab.tsx (3)

223-270: DrillPrefixSelector looks consistent and accessible

The selector correctly derives unique drill prefixes, wires them through to selection, and now uses a specific aria-label="Select drill prefixes", which addresses the earlier accessibility concern for this control. No further changes needed.


272-312: FamilySelector grouping and selection logic are clear

Grouping by SectionFamily, filtering sections by family, and then selecting matching marchers is straightforward and easy to follow. The updated aria-label="Select families" also aligns with the control’s purpose.


171-221: Fix remaining aria-label and consider distinguishing “no tags” from “not yet loaded”

TagSelector still uses aria-label="Select sections", which is misleading for screen readers and was flagged previously; DrillPrefixSelector and FamilySelector have been corrected, so this one just needs to be brought in line.

Also, as written, when tags haven’t finished loading yet, the UI will show “noTagsToSelect”, which conflates “no data” with “still loading”. If you care about that distinction, you could gate the empty-state message behind a separate “loaded & empty” condition.

Minimal aria-label fix:

-    return (
-        <ToolbarSection aria-label="Select sections">
+    return (
+        <ToolbarSection aria-label="Select tags">
🧹 Nitpick comments (3)
apps/desktop/src/components/toolbar/tabs/SelectTab.tsx (2)

44-71: Use functional state updates in shift-select to avoid stale selection and simplify deps

The shift-select branch closes over selectedMarchers and includes it in the dependency array, which is fine but a bit brittle. Using a functional update removes that dependency and avoids any future stale state surprises if the context becomes more complex.

You could refactor like this:

-    const selectMarcherIds = useCallback(
-        (e: React.MouseEvent<HTMLButtonElement>, ids: number[]) => {
+    const selectMarcherIds = useCallback(
+        (e: React.MouseEvent<HTMLButtonElement>, ids: number[]) => {
             e.preventDefault();
             const newSelectedMarchers = allMarchers.filter((marcher) =>
                 ids.includes(marcher.id),
             );

             // if holding shift, add to selection
             if (e.shiftKey) {
-                const currentlySelectedIds = new Set(
-                    selectedMarchers.map((marcher) => marcher.id),
-                );
-                const newSelectedMarchersToAdd = newSelectedMarchers.filter(
-                    (marcher) => !currentlySelectedIds.has(marcher.id),
-                );
-                setSelectedMarchers([
-                    ...selectedMarchers,
-                    ...newSelectedMarchersToAdd,
-                ]);
+                setSelectedMarchers((prev) => {
+                    const currentlySelectedIds = new Set(
+                        prev.map((marcher) => marcher.id),
+                    );
+                    const newSelectedMarchersToAdd =
+                        newSelectedMarchers.filter(
+                            (marcher) => !currentlySelectedIds.has(marcher.id),
+                        );
+                    return [...prev, ...newSelectedMarchersToAdd];
+                });
             } else {
                 setSelectedMarchers(newSelectedMarchers);
             }
         },
-        [allMarchers, selectedMarchers, setSelectedMarchers],
+        [allMarchers, setSelectedMarchers],
     );

314-361: Consider guarding localStorage access and localizing the loading message

Two small polish points in SelectTabContents:

  • The initial localStorage.getItem call in the useState initializer assumes a browser-like environment. If this component is ever reused outside the desktop context (e.g., SSR/tests without localStorage), a try/catch or typeof window !== "undefined" guard would make it more robust.
  • The "Loading..." string is hard-coded, whereas the rest of the file uses T for i18n. If you already have a shared loading key, wiring it through here would keep localization consistent.

Both are optional, but worth considering for future reuse.

apps/desktop/src/components/marcher/appearance/TagAppearanceList.tsx (1)

95-95: Consider error handling for async operation.

The fire-and-forget pattern (void) could silently swallow errors, leaving the UI in an inconsistent state if the tag appearance creation or scrolling fails.

Consider adding error handling:

-        void checkAndCreateTagAppearance();
+        checkAndCreateTagAppearance().catch((error) => {
+            console.error('Failed to create/scroll to tag appearance:', error);
+            // Optionally show a toast notification
+        });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d3b9fba and f3c0896.

📒 Files selected for processing (2)
  • apps/desktop/src/components/marcher/appearance/TagAppearanceList.tsx (1 hunks)
  • apps/desktop/src/components/toolbar/tabs/SelectTab.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Co-locate UI utility helper functions with their components
Always use absolute paths (with @/ alias for src/ directory) for file operations, never relative paths
Batch UI refresh operations where possible for performance optimization

When you need to use icons, use @phosphor-icons/react

Files:

  • apps/desktop/src/components/marcher/appearance/TagAppearanceList.tsx
  • apps/desktop/src/components/toolbar/tabs/SelectTab.tsx
🧠 Learnings (8)
📚 Learning: 2025-11-27T02:03:46.836Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/basic-database-interaction.mdc:0-0
Timestamp: 2025-11-27T02:03:46.836Z
Learning: Applies to apps/desktop/src/hooks/queries/index.ts : Update `apps/desktop/src/hooks/queries/index.ts` to export all new React Query hook files

Applied to files:

  • apps/desktop/src/components/marcher/appearance/TagAppearanceList.tsx
  • apps/desktop/src/components/toolbar/tabs/SelectTab.tsx
📚 Learning: 2025-11-27T02:03:46.836Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/basic-database-interaction.mdc:0-0
Timestamp: 2025-11-27T02:03:46.836Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : React Query mutation options must use selective cache invalidation by invalidating specific query keys rather than blanket invalidations where possible

Applied to files:

  • apps/desktop/src/components/marcher/appearance/TagAppearanceList.tsx
📚 Learning: 2025-11-27T02:04:17.663Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/multi-query-hooks.mdc:0-0
Timestamp: 2025-11-27T02:04:17.663Z
Learning: Applies to src/hooks/use*.ts?(x) : Consider memoization for expensive calculations within data transformations in multi-query hooks

Applied to files:

  • apps/desktop/src/components/marcher/appearance/TagAppearanceList.tsx
📚 Learning: 2025-11-27T02:03:46.836Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/basic-database-interaction.mdc:0-0
Timestamp: 2025-11-27T02:03:46.836Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : React Query mutation options must include `onSuccess` handlers for smart cache invalidation and `onError` handlers with `conToastError` for error notification

Applied to files:

  • apps/desktop/src/components/marcher/appearance/TagAppearanceList.tsx
📚 Learning: 2025-11-27T02:03:46.836Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/basic-database-interaction.mdc:0-0
Timestamp: 2025-11-27T02:03:46.836Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Create a React Query hook file in `apps/desktop/src/hooks/queries/use[TableName].ts` when adding new database table interactions

Applied to files:

  • apps/desktop/src/components/marcher/appearance/TagAppearanceList.tsx
📚 Learning: 2025-11-27T02:04:17.663Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/multi-query-hooks.mdc:0-0
Timestamp: 2025-11-27T02:04:17.663Z
Learning: Applies to src/hooks/use*.ts?(x) : Avoid inline combine functions in `useQueries` as they cause unnecessary re-renders

Applied to files:

  • apps/desktop/src/components/marcher/appearance/TagAppearanceList.tsx
📚 Learning: 2025-11-27T02:03:46.836Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/basic-database-interaction.mdc:0-0
Timestamp: 2025-11-27T02:03:46.836Z
Learning: Applies to apps/desktop/src/db-functions/index.ts : Update `apps/desktop/src/db-functions/index.ts` to export all new database function files

Applied to files:

  • apps/desktop/src/components/toolbar/tabs/SelectTab.tsx
📚 Learning: 2025-11-27T02:04:23.301Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: .cursor/rules/repo.mdc:0-0
Timestamp: 2025-11-27T02:04:23.301Z
Learning: Applies to packages/**/*.{ts,tsx} : Prefer to use components in './packages/ui/src/components/base' folder for common items like buttons and inputs

Applied to files:

  • apps/desktop/src/components/toolbar/tabs/SelectTab.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: build-and-release (ubuntu-24.04-arm)
  • GitHub Check: desktop-e2e
  • GitHub Check: build-and-release (macos-latest)
  • GitHub Check: build-and-release (macos-15-intel)
  • GitHub Check: build-and-release (windows-latest)
  • GitHub Check: build-and-release (ubuntu-latest)
  • GitHub Check: test
  • GitHub Check: build
🔇 Additional comments (7)
apps/desktop/src/components/toolbar/tabs/SelectTab.tsx (3)

21-42: ViewTab wrapper and helper components look clean and straightforward

ViewTab, Separator, and sectionsFromMarchers are simple, readable, and do what they advertise; deriving sections via a Set + sort gives a stable, deduped list. No changes needed here.


76-122: SelectByPopover implementation is solid

Popover wiring, state handling, and translation keys are all wired cleanly, and mapping SelectByOptions directly into the menu keeps the mode list single-sourced. Looks good as-is.


124-169: SectionSelector logic and empty state behave as expected

Deriving sections from marchers, selecting by section, and the “noSectionsToSelect” fallback all look correct and consistent with the rest of the toolbar UX. No changes needed here.

apps/desktop/src/components/marcher/appearance/TagAppearanceList.tsx (4)

1-47: LGTM: Proper setup with memoization.

The component setup follows best practices:

  • Uses @phosphor-icons/react as per guidelines
  • Absolute imports with @/ alias
  • Proper memoization of the tag names map to avoid unnecessary recalculations

147-187: LGTM: Proper React patterns.

The component setup demonstrates good practices:

  • ForwardRef with proper TypeScript typing
  • useMemo for expensive operations (sorting, Set creation)
  • Appropriate ref and state management for scroll and highlight features

189-226: LGTM: Proper cleanup and timing logic.

The scroll and highlight effect is well-implemented:

  • Proper guard clauses
  • Both timers are stored and cleaned up correctly (addresses past review feedback)
  • Smooth scrolling with appropriate delays for UX

228-337: LGTM: Well-structured UI with proper accessibility.

The render logic demonstrates good practices:

  • Proper use of Dropdown.Portal for z-index management
  • Accessibility with aria-label attributes
  • Disabled state for already-taken tags
  • Smooth transitions for highlighting
  • Proper ref management for scroll targets

Comment on lines +67 to +74
if (!existingAppearance) {
// Create the tag appearance and wait for persistence
await createTagAppearances([
{ tag_id: targetTagId, start_page_id: targetPageId },
]);
// Allow React Query to invalidate and re-render
await queryClient.invalidateQueries();
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Read the file to see the context around lines 67-74
wc -l apps/desktop/src/components/marcher/appearance/TagAppearanceList.tsx

Repository: OpenMarch/OpenMarch

Length of output: 135


🏁 Script executed:

# Read lines 50-110 to see the full useEffect context
sed -n '50,110p' apps/desktop/src/components/marcher/appearance/TagAppearanceList.tsx

Repository: OpenMarch/OpenMarch

Length of output: 2248


🏁 Script executed:

# Search for createTagAppearances hook
rg -A 15 "createTagAppearances" apps/desktop/src/hooks --type ts --type tsx

Repository: OpenMarch/OpenMarch

Length of output: 90


🏁 Script executed:

# Search for createTagAppearancesMutationOptions or similar
rg "createTagAppearances.*[Mm]utation|mutation.*createTagAppearances" apps/desktop/src --type ts --type tsx -A 5

Repository: OpenMarch/OpenMarch

Length of output: 90


🏁 Script executed:

# Find the file containing createTagAppearances mutation
fd "use.*[Tt]ag" apps/desktop/src/hooks/queries --type f

Repository: OpenMarch/OpenMarch

Length of output: 45


🏁 Script executed:

# List hooks directory
find apps/desktop/src/hooks -type f -name "*.ts" -o -name "*.tsx" | head -20

Repository: OpenMarch/OpenMarch

Length of output: 1039


🏁 Script executed:

# Check imports in TagAppearanceList.tsx
head -50 apps/desktop/src/components/marcher/appearance/TagAppearanceList.tsx | grep -E "import|from"

Repository: OpenMarch/OpenMarch

Length of output: 661


🏁 Script executed:

# Check full imports section
sed -n '1,30p' apps/desktop/src/components/marcher/appearance/TagAppearanceList.tsx

Repository: OpenMarch/OpenMarch

Length of output: 1124


🏁 Script executed:

# Search for createTagAppearances definition across the entire codebase
rg "const createTagAppearances|function createTagAppearances|export.*createTagAppearances" apps/desktop/src --type ts --type tsx -B 2 -A 10

Repository: OpenMarch/OpenMarch

Length of output: 90


🏁 Script executed:

# Search more broadly for where createTagAppearances comes from
rg "createTagAppearances" apps/desktop/src --type ts --type tsx -l

Repository: OpenMarch/OpenMarch

Length of output: 90


🏁 Script executed:

# Look at the full file to understand the structure
cat apps/desktop/src/components/marcher/appearance/TagAppearanceList.tsx

Repository: OpenMarch/OpenMarch

Length of output: 14128


🏁 Script executed:

# Find useCreateTagAppearances
rg "useCreateTagAppearances|export.*createTagAppearances" apps/desktop/src --type ts --type tsx -l

Repository: OpenMarch/OpenMarch

Length of output: 90


🏁 Script executed:

# Search for the hook definition
rg -A 30 "export.*useCreateTagAppearances" apps/desktop/src --type ts --type tsx

Repository: OpenMarch/OpenMarch

Length of output: 90


🏁 Script executed:

# Try a broader search
rg "useCreateTagAppearances" apps/desktop/src --type ts --type tsx -A 20 | head -60

Repository: OpenMarch/OpenMarch

Length of output: 90


🏁 Script executed:

# Find all files in hooks directory
find apps/desktop/src/hooks -type f

Repository: OpenMarch/OpenMarch

Length of output: 1285


🏁 Script executed:

# Check useTagAppearances or related files
ls -la apps/desktop/src/hooks/mutations/ 2>/dev/null || ls -la apps/desktop/src/hooks/

Repository: OpenMarch/OpenMarch

Length of output: 556


🏁 Script executed:

# List all hook files
find apps/desktop/src/hooks -name "*.ts" -o -name "*.tsx"

Repository: OpenMarch/OpenMarch

Length of output: 1252


🏁 Script executed:

# Search in the entire apps/desktop directory for TagAppearance-related hooks
find apps/desktop/src -name "*[Tt]ag*" -type f | grep -i hook

Repository: OpenMarch/OpenMarch

Length of output: 45


🏁 Script executed:

# Look at directory structure
ls -la apps/desktop/src/hooks/

Repository: OpenMarch/OpenMarch

Length of output: 556


🏁 Script executed:

# Search in the specific query hooks directory
ls -la apps/desktop/src/hooks/queries/

Repository: OpenMarch/OpenMarch

Length of output: 1495


🏁 Script executed:

# Check useTagAppearances file
cat apps/desktop/src/hooks/queries/useTagAppearances.ts 2>/dev/null || echo "File not found, searching..."

Repository: OpenMarch/OpenMarch

Length of output: 91


🏁 Script executed:

# List query files
ls -1 apps/desktop/src/hooks/queries/*.ts | head -20

Repository: OpenMarch/OpenMarch

Length of output: 924


🏁 Script executed:

# Search for files containing TagAppearance
rg "TagAppearance" apps/desktop/src/hooks --type ts -l

Repository: OpenMarch/OpenMarch

Length of output: 215


🏁 Script executed:

# Check if it's in a different location
rg "useCreateTagAppearances\|createTagAppearancesMutationOptions" apps --type ts -l

Repository: OpenMarch/OpenMarch

Length of output: 45


🏁 Script executed:

# Broader search across all TypeScript files
rg "useCreateTagAppearances" . --type ts -A 20 2>/dev/null | head -100

Repository: OpenMarch/OpenMarch

Length of output: 45


🏁 Script executed:

# Search for query key factories or tag appearance query keys
rg "tagAppearance|TagAppearance" apps/desktop/src/hooks/queries --type ts -A 3 | head -80

Repository: OpenMarch/OpenMarch

Length of output: 6321


🏁 Script executed:

# Search for query key definitions
rg "queryKey.*=.*\[|queryOptions" apps/desktop/src/hooks/queries --type ts | head -30

Repository: OpenMarch/OpenMarch

Length of output: 2806


🏁 Script executed:

# Check if there's a query key factory file
find apps/desktop/src -name "*key*" -o -name "*factory*" | grep -i query

Repository: OpenMarch/OpenMarch

Length of output: 45


🏁 Script executed:

# Look for invalidateQueries usage patterns in the codebase
rg "invalidateQueries" apps/desktop/src --type ts -B 2 -A 2 | head -100

Repository: OpenMarch/OpenMarch

Length of output: 7470


🏁 Script executed:

# Double-check the exact line 73 content to ensure accuracy
sed -n '67,75p' apps/desktop/src/components/marcher/appearance/TagAppearanceList.tsx

Repository: OpenMarch/OpenMarch

Length of output: 524


Replace blanket query invalidation with selective key-based invalidation.

The queryClient.invalidateQueries() call on line 73 without arguments invalidates all cached queries, causing unnecessary re-fetches across the entire app. Project patterns use selective invalidation with specific query keys (see sharedInvalidators.ts). Either remove this manual invalidation and ensure the mutation options handle cache updates via onSuccess handlers, or replace it with selective invalidation targeting affected query keys like tag appearances:

             if (!existingAppearance) {
                 // Create the tag appearance and wait for persistence
                 await createTagAppearances([
                     { tag_id: targetTagId, start_page_id: targetPageId },
                 ]);
-                // Allow React Query to invalidate and re-render
-                await queryClient.invalidateQueries();
             } else {
📝 Committable suggestion

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

Suggested change
if (!existingAppearance) {
// Create the tag appearance and wait for persistence
await createTagAppearances([
{ tag_id: targetTagId, start_page_id: targetPageId },
]);
// Allow React Query to invalidate and re-render
await queryClient.invalidateQueries();
} else {
if (!existingAppearance) {
// Create the tag appearance and wait for persistence
await createTagAppearances([
{ tag_id: targetTagId, start_page_id: targetPageId },
]);
} else {
🤖 Prompt for AI Agents
In apps/desktop/src/components/marcher/appearance/TagAppearanceList.tsx around
lines 67 to 74, the code calls queryClient.invalidateQueries() with no args
which invalidates all queries; replace this with selective invalidation (or
remove manual invalidation and rely on the mutation's onSuccess). Specifically,
import and use the project's shared invalidator for tag appearances (e.g.
sharedInvalidators.tagAppearances or the equivalent key) or call
queryClient.invalidateQueries with the specific key for the affected data (for
example ['tagAppearances', targetTagId] or ['tagAppearances', targetPageId] as
appropriate), and/or move the cache update into the createTagAppearances
mutation’s onSuccess handler to update or invalidate only the relevant keys.

Comment on lines +129 to +139
handleCreateAppearance={(tagId) =>
void createTagAppearances([
{ tag_id: tagId, start_page_id: page.id },
])
}
handleUpdateAppearance={(modifiedAppearance) =>
void updateTagAppearances([modifiedAppearance])
}
handleDeleteAppearance={(tagId) =>
void deleteTagAppearances(new Set([tagId]))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix naming inconsistency and add error handling.

Two issues:

  1. Naming inconsistency: The handleDeleteAppearance parameter is named tagId (line 156), but it actually receives tagAppearance.id (line 321), which is the appearance ID, not the tag ID. This is misleading and could cause confusion or bugs.

  2. Fire-and-forget mutations: All three handlers use void with no error handling. If these mutations fail, the user gets no feedback.

Apply this diff to fix the naming:

-                        handleDeleteAppearance={(tagId) =>
-                            void deleteTagAppearances(new Set([tagId]))
+                        handleDeleteAppearance={(appearanceId) =>
+                            void deleteTagAppearances(new Set([appearanceId]))
                         }

And update the type definition on line 156:

-        handleDeleteAppearance: (tagId: number) => void;
+        handleDeleteAppearance: (appearanceId: number) => void;

For error handling, ensure the mutation options include onError handlers, or add .catch() handlers here:

                         handleCreateAppearance={(tagId) =>
-                            void createTagAppearances([
+                            createTagAppearances([
                                 { tag_id: tagId, start_page_id: page.id },
-                            ])
+                            ]).catch((error) => {
+                                console.error('Failed to create tag appearance:', error);
+                            })
                         }

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Contributor

@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: 0

🧹 Nitpick comments (3)
apps/desktop/e2e/tests/1-basic-render.spec.mts (3)

1-1: Prefer a type-only import for Page

Since Page is only used as a type, consider switching to a type-only import to keep runtime imports minimal and make intent explicit:

-import { expect, Page } from "@playwright/test";
+import { expect } from "@playwright/test";
+import type { Page } from "@playwright/test";

This keeps the runtime dependency surface a bit cleaner while preserving typings.


37-40: Good reuse via canvasIsVisible; consider making the locators more stable

Centralizing the canvas/timeline visibility checks in canvasIsVisible(page) is a nice cleanup and makes the intent of the tests clearer. The only concern is potential flakiness from:

  • page.getByText("Timeline") if the label text changes or appears in multiple places.
  • page.locator("canvas").nth(1) if the DOM adds/removes/reorders <canvas> elements.

Not a blocker, but if these tests start to flicker, it may be worth switching to a more stable locator (e.g., role, data-testid, or a more specific container for the main canvas) while keeping this helper as the single place to update it.

Also applies to: 51-52, 56-57


93-94: nth(1) on the Marchers workspace locator may be brittle

page.locator("#workspace div").filter({ hasText: "Marchers" }).nth(1) assumes a specific ordering/count of matching divs. If the layout or copy changes, this test could start failing even though the UI is still correct.

If possible, consider targeting a more explicit element (role/heading/region or a dedicated test id) so you don't depend on the second matching div:

  • e.g., a region/section with accessible name “Marchers”, or
  • a specific container element with a stable selector.

This would make the “Sidebars are visible” test more robust to UI tweaks.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f3c0896 and 51a8d7e.

📒 Files selected for processing (1)
  • apps/desktop/e2e/tests/1-basic-render.spec.mts (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-27T02:03:30.741Z
Learnt from: CR
Repo: OpenMarch/OpenMarch PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-27T02:03:30.741Z
Learning: Use Vitest for unit tests and Playwright for e2e tests

Applied to files:

  • apps/desktop/e2e/tests/1-basic-render.spec.mts
🧬 Code graph analysis (1)
apps/desktop/e2e/tests/1-basic-render.spec.mts (1)
apps/desktop/e2e/fixtures.mts (1)
  • test (105-206)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: build-and-release (ubuntu-latest)
  • GitHub Check: build-and-release (ubuntu-24.04-arm)
  • GitHub Check: build-and-release (windows-latest)
  • GitHub Check: build-and-release (macos-15-intel)
  • GitHub Check: build-and-release (macos-latest)
  • GitHub Check: pr-checks (build)
  • GitHub Check: desktop-e2e
  • GitHub Check: build
  • GitHub Check: test

@AlexDumo AlexDumo merged commit 3794a70 into main Dec 9, 2025
18 checks passed
@AlexDumo AlexDumo deleted the tagging branch December 9, 2025 05:14
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