Skip to content

Conversation

@Ja4V8s28Ck
Copy link
Contributor

@Ja4V8s28Ck Ja4V8s28Ck commented Oct 30, 2025

Implemented

  • Added Lexorank for a better roadmap indexing and drag and drop.
  • Fixes all the issue that previous n + 1 indexing had.
  • Added new integration test for sorting

Backward Compatibility

  • Not implemented as of now, because this is still a RFC Feature.

Summary by CodeRabbit

  • New Features

    • Enhanced roadmap reordering with improved drag-and-drop handling and more flexible positioning logic.
  • Bug Fixes

    • Improved roadmap sorting reliability with validation checks for adjacent positions.
@coderabbitai
Copy link

coderabbitai bot commented Oct 30, 2025

📝 Walkthrough

Walkthrough

This PR replaces integer-based roadmap indexing with LexoRank-based lexicographic ranking. Changes include adding the lexorank dependency, updating database schema and migrations, refactoring the create and sort endpoints with LexoRank logic, updating frontend drag-drop handlers to call the new sort API with revised parameters, and updating type definitions across the stack.

Changes

Cohort / File(s) Change Summary
Dependencies
packages/server/package.json
Added lexorank dependency (^1.0.5) for lexicographic ranking calculations
Database Schema
packages/server/src/database/migrations/20201224104136_add-roadmaps-table.ts
Changed roadmaps table "index" column type from integer to string(50) to accommodate LexoRank values
Backend: Roadmap Creation
packages/server/src/ee/controllers/v1/roadmaps/create.ts
Replaced numeric index increment logic with LexoRank initialization; uses LexoRank.middle() for first roadmap and genNext() for subsequent ones
Backend: Roadmap Sorting
packages/server/src/ee/controllers/v1/roadmaps/sort.ts
Added Valibot schema validation, introduced calculateRankIndex() function using LexoRank, replaced swap logic with transactional reordering using prev/next roadmap IDs, added neighbour validation and error handling
Backend: Error Responses
packages/server/src/errorResponse.json
Added three new roadmap error keys: idMissing, idInvalid, neighbourInvalid
Backend: Tests & Utilities
packages/server/tests/integration/v1/roadmaps.spec.ts, packages/server/tests/utils/generators.ts
Added comprehensive PATCH sort endpoint tests with validation, permission, and reordering scenarios; updated roadmap generator to use LexoRank string indices instead of numeric
Frontend: Drag-Drop Handlers
packages/theme/src/ee/components/dashboard/roadmap/TabularView.vue
Replaced draggable event handlers with onDragStart and onDragEnd; onDragEnd derives prev/next adjacent roadmaps and calls sortRoadmap with new ID-based parameters, then updates index via updateRoadmapIndex
Frontend: Roadmap Module
packages/theme/src/ee/modules/roadmaps.ts
Updated sortRoadmap function signature from { from, to } to { id, prevRoadmapId, nextRoadmapId } parameters
Frontend: Store
packages/theme/src/ee/store/dashboard/roadmaps.ts
Removed sortRoadmap(fromIndex, toIndex) method; added new updateRoadmapIndex(roadmapId, roadmapRankIndex) to update a single roadmap's index post-sort
Type Definitions
packages/types/src/roadmap.ts
Updated IRoadmapPrivate.index from number to string; restructured ISortRoadmapRequestBody from nested { from, to } to flat { id, prevRoadmapId, nextRoadmapId }; changed TSortRoadmapResponseBody from literal "OK" to { index: string }

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Frontend as Frontend<br/>(TabularView)
    participant Module as Module<br/>(roadmaps.ts)
    participant API as API<br/>(sort endpoint)
    participant Store as Store<br/>(dashboard/roadmaps)
    participant DB as Database

    rect rgb(200, 220, 240)
    Note over Frontend,Store: Drag End Handler
    User->>Frontend: Release dragged roadmap
    Frontend->>Frontend: onDragEnd(oldIndex, newIndex)
    Frontend->>Frontend: Compute dragged roadmap ID<br/>and adjacent prev/next IDs
    
    alt Positions unchanged
        Frontend->>Frontend: Return (no-op)
    else Valid reorder
        Frontend->>Module: sortRoadmap(id, prevId, nextId)
        end
    end

    rect rgb(220, 240, 220)
    Note over Module,DB: Sort via API
    Module->>API: PATCH /api/v1/roadmaps/sort<br/>{ id, prevRoadmapId, nextRoadmapId }
    API->>API: Validate schema & permissions
    API->>DB: Fetch prev/next roadmaps
    API->>API: calculateRankIndex(prev, next)<br/>using LexoRank
    API->>DB: Update roadmap index (transaction)
    API->>Module: 200 { index: "<LexoRank>" }
    end

    rect rgb(240, 220, 220)
    Note over Frontend,Store: Update UI State
    Module->>Store: updateRoadmapIndex(roadmapId, newIndex)
    Store->>Store: Locate roadmap, update index
    Store->>Frontend: Return updated index
    Frontend->>Frontend: UI reflects new ordering
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas requiring extra attention:

  • packages/server/src/ee/controllers/v1/roadmaps/sort.ts — High-density logic with LexoRank calculations, transactional ordering, and multiple validation branches; verify correctness of neighbour validation and rank generation edge cases
  • packages/theme/src/ee/components/dashboard/roadmap/TabularView.vue — Complex drag/drop refactor that derives prev/next indices from DOM state; ensure adjacent roadmap calculation is robust and handles boundary cases (first/last items)
  • Database migration — Changing index column type from integer to string(50) in production; confirm data migration path and potential impact on existing indices
  • Type signature changes — Breaking changes to ISortRoadmapRequestBody and TSortRoadmapResponseBody across frontend and backend; verify all consumers are updated consistently

Suggested labels

area:frontend, area:api, High (P2)

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Title Check ✅ Passed The pull request title "Feat/Lexorank implementation for roadmaps" clearly and directly reflects the main objective of this changeset. The modifications throughout the codebase are centered on replacing the previous integer-based roadmap indexing system with a LexoRank-based lexicographical ranking system, which is exactly what the title communicates. The title is concise, specific, and avoids vague terminology or noise, making it immediately clear to teammates reviewing the commit history that this PR introduces LexoRank functionality for roadmap ordering.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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 feat/lexographical-ranking-for-roadmaps

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.

@github-actions
Copy link

github-actions bot commented Oct 30, 2025

Lighthouse report

Category Score Status
Performance 98 🟢
Accessibility 94 🟢
Best Practices 100 🟢
SEO 100 🟢
PWA N/A

View workflow run

Copy link

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

Caution

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

⚠️ Outside diff range comments (1)
packages/server/tests/utils/generators.ts (1)

44-69: Restore roadmap override values in the test generator.

generateRoadmap now hard-codes index, color, and display, ignoring any values passed via the roadmap argument. The GET tests rely on generateRoadmap({ display: isPublic }) to seed 50 public vs 50 private entries; with the new code this becomes random, making those tests (and any others needing deterministic fixtures) fail or flake. Please reinstate the previous roadmap?.display ?? … / roadmap?.color ?? … fallback (and similar for index if callers supply one).

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3eb2375 and 85a024f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (12)
  • packages/server/package.json (1 hunks)
  • packages/server/src/database/migrations/20201224104136_add-roadmaps-table.ts (1 hunks)
  • packages/server/src/ee/controllers/v1/roadmaps/create.ts (2 hunks)
  • packages/server/src/ee/controllers/v1/roadmaps/sort.ts (4 hunks)
  • packages/server/src/errorResponse.json (1 hunks)
  • packages/server/tests/integration/v1/roadmaps.spec.ts (3 hunks)
  • packages/server/tests/utils/generators.ts (2 hunks)
  • packages/theme/package.json (1 hunks)
  • packages/theme/src/ee/components/dashboard/roadmap/TabularView.vue (2 hunks)
  • packages/theme/src/ee/modules/roadmaps.ts (2 hunks)
  • packages/theme/src/ee/store/dashboard/roadmaps.ts (4 hunks)
  • packages/types/src/roadmap.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
packages/server/src/ee/controllers/v1/roadmaps/create.ts (1)
packages/theme/src/ee/modules/roadmaps.ts (1)
  • createRoadmap (22-37)
packages/server/tests/utils/generators.ts (1)
packages/server/src/helpers.ts (1)
  • sanitiseURL (190-190)
packages/theme/src/ee/store/dashboard/roadmaps.ts (3)
packages/types/src/roadmap.ts (1)
  • IRoadmapPrivate (14-18)
packages/theme/src/ee/modules/roadmaps.ts (2)
  • updateRoadmap (49-64)
  • sortRoadmap (69-90)
packages/server/src/ee/controllers/v1/roadmaps/updateRoadmap.ts (1)
  • updateRoadmap (58-127)
packages/server/src/ee/controllers/v1/roadmaps/sort.ts (1)
packages/types/src/roadmap.ts (1)
  • ISortRoadmapRequestBody (67-71)
packages/server/tests/integration/v1/roadmaps.spec.ts (3)
packages/server/tests/utils/seed/user.ts (1)
  • createUser (27-73)
packages/server/tests/utils/createRoleWithPermissions.ts (1)
  • createRoleWithPermissions (10-58)
packages/types/src/roadmap.ts (2)
  • ISortRoadmapRequestBody (67-71)
  • IRoadmapPrivate (14-18)
⏰ 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). (7)
  • GitHub Check: API / Integration Tests - OS ubuntu-24.04 - Node v22 - Postgres v18
  • GitHub Check: API / Integration Tests - OS ubuntu-24.04 - Node v22 - Postgres v14
  • GitHub Check: API / Integration Tests - OS ubuntu-24.04 - Node v22 - Postgres v17
  • GitHub Check: API / Integration Tests - OS ubuntu-24.04 - Node v22 - Postgres v12.18
  • GitHub Check: API / Integration Tests - OS ubuntu-24.04 - Node v22 - Postgres v16
  • GitHub Check: test
  • GitHub Check: lighthouse
table.string("name", 50).notNullable();
table.string("url", 50).notNullable().unique();
table.integer("index").notNullable();
table.string("index", 50).notNullable();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Do not rewrite historical migration; provide a forward migration.

Changing the original 20201224104136 migration from integer to string(50) won’t touch already-provisioned databases. Those environments still have roadmaps.index as an INTEGER, so the new LexoRank strings will fail to insert (invalid input syntax for type integer). Please keep the original migration intact and add a new migration that alters the column type (and backfills existing rows) in place.

🤖 Prompt for AI Agents
packages/server/src/database/migrations/20201224104136_add-roadmaps-table.ts
lines ~10: do not modify this historical migration; instead create a new
migration file that alters roadmaps.index from integer to string(50) and
backfills existing rows. The new migration should run in a transaction, add a
temporary varchar(50) column, compute LexoRank strings for all existing integer
index values (and handle NULLs by assigning appropriate LexoRank positions),
write those strings into the temp column, migrate any constraints/indexes, drop
the old integer column, rename the temp column to index (preserving
notNullable), and in the down migration reverse the steps (convert the string
values back to integers or a safe default, restore original type and
constraints). Ensure the migration handles casting safely (no direct ALTER
TYPE), preserves existing data, and is idempotent for already-provisioned DBs.
Comment on lines +47 to +60
const lastRoadmap = await database
.select("index")
.from("roadmaps")
.orderBy("index", "desc")
.first();

let nextIndex: string;

if (!lastRoadmap) {
nextIndex = LexoRank.middle().toString();
} else {
const prevLexoRank = LexoRank.parse(lastRoadmap.index);
nextIndex = prevLexoRank.genNext().toString();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against invalid legacy roadmap indexes before parsing

If any existing roadmap still carries a null, empty or pre-LexoRank index (e.g. after the type change but before a data backfill), LexoRank.parse(lastRoadmap.index) will throw and the create flow will 500. We should treat an absent/invalid index as “no prior rank” and fall back to LexoRank.middle() (ideally logging the anomaly).

-    if (!lastRoadmap) {
-      nextIndex = LexoRank.middle().toString();
-    } else {
-      const prevLexoRank = LexoRank.parse(lastRoadmap.index);
-      nextIndex = prevLexoRank.genNext().toString();
-    }
+    const lastRoadmapIndex = lastRoadmap?.index;
+
+    if (!lastRoadmapIndex) {
+      nextIndex = LexoRank.middle().toString();
+    } else {
+      try {
+        nextIndex = LexoRank.parse(lastRoadmapIndex).genNext().toString();
+      } catch {
+        logger.warn({
+          message: `Invalid LexoRank index detected while creating roadmap: ${lastRoadmapIndex}`,
+        });
+        nextIndex = LexoRank.middle().toString();
+      }
+    }
🤖 Prompt for AI Agents
In packages/server/src/ee/controllers/v1/roadmaps/create.ts around lines 47 to
60, guard against null/empty/legacy indexes before calling LexoRank.parse: if
lastRoadmap is missing or lastRoadmap.index is falsy or fails to parse as a
LexoRank, treat it as “no prior rank” and use LexoRank.middle() for nextIndex;
if parsing fails catch the error, fall back to LexoRank.middle(), and log the
anomaly (including the offending index and roadmap id/context) so we can
identify legacy data issues.
Comment on lines +68 to 71
id: string;
prevRoadmapId: string;
nextRoadmapId: string;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make neighbour roadmap ids optional in the contract

The server schema now allows either prevRoadmapId or nextRoadmapId, but the type keeps both mandatory, forcing callers to cast and making valid payloads appear illegal. Loosen the shape so TypeScript reflects reality.

 export interface ISortRoadmapRequestBody {
   id: string;
-  prevRoadmapId: string;
-  nextRoadmapId: string;
+  prevRoadmapId?: string;
+  nextRoadmapId?: string;
 }
📝 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
id: string;
prevRoadmapId: string;
nextRoadmapId: string;
}
export interface ISortRoadmapRequestBody {
id: string;
prevRoadmapId?: string;
nextRoadmapId?: string;
}
🤖 Prompt for AI Agents
In packages/types/src/roadmap.ts around lines 68 to 71, the interface currently
requires both prevRoadmapId and nextRoadmapId even though the server allows
either to be absent; change their types to be optional (e.g., prevRoadmapId?:
string and nextRoadmapId?: string) so callers no longer need to cast and
TypeScript accurately represents the permitted payload shape.
@Ja4V8s28Ck Ja4V8s28Ck force-pushed the feat/lexographical-ranking-for-roadmaps branch from 665a29c to 13dcba1 Compare October 30, 2025 10:59
@Ja4V8s28Ck
Copy link
Contributor Author

@mittalyashu If you have the chance, can you do a stress test on this implementation and see how it performs?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants