Skip to content

Cytoscape refactor#1358

Merged
twangodev merged 15 commits into
mainfrom
cytoscape-refactor
Mar 30, 2026
Merged

Cytoscape refactor#1358
twangodev merged 15 commits into
mainfrom
cytoscape-refactor

Conversation

@ProfessorAtomicManiac

@ProfessorAtomicManiac ProfessorAtomicManiac commented Jan 2, 2026

Copy link
Copy Markdown
Collaborator

please make sure everything on this branch still works as intended (we should probably make tests at some point)

I split cytoscape into 4 different files

  • cytoscape-init.ts: helper funcs for initializing cytoscape instance. This is primarily created to remove the filter prop on the old cytoscape. Filtering is meant to be done before the cytoscape component is created (see course-requirements.svelte as an example). also in the future when I add ast graphs I will need to generate the element definitions from the ast and that will be another function in cytoscape-init
  • cytoscape-handlers.svelte.ts: mimics all the $effect we had in the original cytoscape file. returns an api to interact with the handlers such as onCourseClick, setElementsAreDraggable, etc
  • cytoscape-core.svelte: creates the actual cytoscape component which uses functions from both cytoscape-init and cytoscape-handlers. The cytoscape-core exposes an api that other components such as sideControls can use to respond to user events
  • cytoscape.svelte: creates cytoscape component, courseDrawer, legend, sideControls, HelpControl

I added back the legend but rn its not actually functional, purely only there visually. In a future pr we can add back the functionality.

Summary by CodeRabbit

  • New Features

    • Graph now filters prerequisites by your taken courses.
    • New core graph component exposes zoom, layout, focus, label, and subject controls.
    • Course drawer supports open/close/select actions for smoother navigation.
    • Responsive desktop detection added.
  • Bug Fixes & Improvements

    • Improved interaction handlers, tooltips, and layout flow for more reliable graph behavior.
    • Side controls now emit clear events for zoom, layout, draggability and label toggles.
    • Legend toggling simplified and made optional.
  • Refactor

    • Removed small color-dot component and inlined its behavior.

@coderabbitai

coderabbitai Bot commented Jan 2, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Refactors Cytoscape into core/init/handlers modules, adds CytoscapeCore with a public API and course-focus/tooltip handlers, filters prerequisite graphs using takenCoursesStore via filterElementsByRootCourse, removes color-dot.svelte, inlines legend buttons, and adds an isDesktop media store.

Changes

Cohort / File(s) Summary
Core modules (new / changed)
src/lib/components/cytoscape/cytoscape-core.svelte, src/lib/components/cytoscape/cytoscape-init.ts, src/lib/components/cytoscape/cytoscape-handlers.svelte.ts
Introduces CytoscapeCore component, plugin/init and layout utilities, and a handlers module. Adds public APIs (onCourseClick, zoom, runLayout, focusOnCourse, setShowCodeLabels, setHiddenSubject, setElementsAreDraggable) and filterElementsByRootCourse() for prerequisite filtering.
Top-level composition / integration
src/lib/components/cytoscape/cytoscape.svelte, src/lib/components/cytoscape/course-drawer.svelte, src/lib/components/cytoscape/side-controls.svelte
Replaces inline Cytoscape logic with CytoscapeCore composition. CourseDrawer now uses cytoscapeCoreRef and exposes methods (openDrawer, closeDrawer, setSelectedCourse). SideControls emits callbacks and exposes getters instead of manipulating Cytoscape directly.
Course prerequisites consumer
src/lib/components/course/tabs/course-prerequisites.svelte
Subscribes to takenCoursesStore, converts taken courses to ids, computes eleDefs by filtering prerequisiteElementDefinitions with filterElementsByRootCourse() (root course + taken courses), and passes filtered elements to CytoscapeCore.
Layout utilities / layouts
src/lib/components/cytoscape/graph-layout.ts
Layout signatures changed: generateLayeredLayout and generateFcoseLayout now accept explicit animate: boolean; callers must supply animation intent.
Legend / color UI
src/lib/components/cytoscape/legend.svelte, (removed) src/lib/components/cytoscape/color-dot.svelte
Removed color-dot.svelte; legend now inlines color buttons and makes hiddenSubject optional; toggling handled by button click handlers.
Handlers / interactions
src/lib/components/cytoscape/cytoscape-handlers.svelte.ts
New setupCytoscapeHandlers(cy) wiring highlighting, tooltips, taken-courses styling, code-label toggles, draggability, subject-hiding via scratch storage, and exposing cleanup & click-callback registration.
Responsive media / help
src/lib/mediaStore.ts, src/lib/components/cytoscape/help-control.svelte
Adds isDesktop writable store driven by "(min-width: 768px)" media query; help-control now reads $isDesktop.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SideControls
  participant CytoscapeCore
  participant Handlers as CytoscapeHandlers
  participant CourseDrawer
  participant Store as takenCoursesStore

  User->>SideControls: trigger zoom/layout/draggable/label
  SideControls->>CytoscapeCore: zoom / runLayout / setElementsAreDraggable / setShowCodeLabels
  Store-->>CytoscapeCore: takenCourses update
  CytoscapeCore->>CytoscapeCore: filterElementsByRootCourse(root, takenIds)
  CytoscapeCore->>Handlers: register events / apply styling
  User->>CytoscapeCore: click course node
  CytoscapeCore->>Handlers: emit courseClick(courseId)
  Handlers->>CourseDrawer: request open with courseId
  CourseDrawer-->>User: render course details
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 Hop—new nodes and tunnels neatly twined,

Core hums, handlers prance, each edge defined,
Legend buttons flick, subjects hide or show,
Taken paths shrink back so maps can grow,
I twitch my whiskers—graphs begin to go.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Cytoscape refactor' is vague and generic, using a non-descriptive term that doesn't convey the specific nature of the architectural changes to readers scanning commit history. Consider a more descriptive title such as 'Split Cytoscape implementation into modular components' or 'Refactor Cytoscape into separate initialization, handlers, and core modules' to better communicate the structural changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

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

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (5)
src/lib/mediaStore.ts (1)

6-9: Consider storing the MediaQueryList reference for potential cleanup.

While module-level listeners typically persist for the app lifetime, storing the reference enables cleanup if needed in the future (e.g., for testing or HMR scenarios).

🔎 Optional improvement
 if (browser) {
   const mediaQuery = window.matchMedia("(min-width: 768px)");
   isDesktop.set(mediaQuery.matches);
-  mediaQuery.addEventListener("change", (e) => isDesktop.set(e.matches));
+  const handleChange = (e: MediaQueryListEvent) => isDesktop.set(e.matches);
+  mediaQuery.addEventListener("change", handleChange);
+  // Export for cleanup if needed: export const cleanupMediaQuery = () => mediaQuery.removeEventListener("change", handleChange);
 }
src/lib/components/cytoscape/graph-layout.ts (1)

77-81: Minor: shorthand property available.

🔎 Use shorthand property
-    animate: animate, // Whether to animate the layout
+    animate, // Whether to animate the layout
src/lib/components/cytoscape/cytoscape.svelte (1)

27-30: Inconsistent ref initialization patterns.

cytoscapeCoreRef uses $state() with undefined, but courseDrawerRef and sideControlsRef are declared without initialization. For consistency and to avoid potential TypeScript issues, initialize all refs similarly.

🔎 Consistent initialization
   // Component refs
   let cytoscapeCoreRef: CytoscapeCore | undefined = $state();
-  let courseDrawerRef: CourseDrawer;
-  let sideControlsRef: SideControls;
+  let courseDrawerRef: CourseDrawer | undefined = $state();
+  let sideControlsRef: SideControls | undefined = $state();
src/lib/components/cytoscape/cytoscape-core.svelte (1)

84-86: Consider consistent early-return guards.

The runLayout method uses optional chaining, while other methods like zoom and focusOnCourse use explicit guards with early returns. For consistency and clarity, consider using the same pattern:

🔎 Suggested refactor for consistency
 export function runLayout(layoutOptions: LayoutOptions) {
+  if (!cy) return;
-  cy?.layout(layoutOptions).run();
+  cy.layout(layoutOptions).run();
 }
src/lib/components/cytoscape/course-drawer.svelte (1)

32-52: Good refactor to public API pattern.

The shift from prop-based state management to a ref-based public API is a solid architectural improvement. The component now owns its state and exposes control methods, which is more aligned with component composition patterns.

💡 Optional: Consider arrow functions for consistency

For consistency with modern JavaScript patterns, you could use arrow functions:

-export const closeDrawer = function () {
+export const closeDrawer = () => {
   sheetOpen = false;
-}
+};
-export const openDrawer = function () {
+export const openDrawer = () => {
   sheetOpen = true;
-}
+};
-export const setSelectedCourse = function (course: Course | undefined) {
+export const setSelectedCourse = (course: Course | undefined) => {
   selectedCourse = course;
-}
+};
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8b0fd80 and f88e5b4.

📒 Files selected for processing (12)
  • src/lib/components/course/tabs/course-prerequisites.svelte
  • src/lib/components/cytoscape/color-dot.svelte
  • src/lib/components/cytoscape/course-drawer.svelte
  • src/lib/components/cytoscape/cytoscape-core.svelte
  • src/lib/components/cytoscape/cytoscape-handlers.svelte.ts
  • src/lib/components/cytoscape/cytoscape-init.ts
  • src/lib/components/cytoscape/cytoscape.svelte
  • src/lib/components/cytoscape/graph-layout.ts
  • src/lib/components/cytoscape/help-control.svelte
  • src/lib/components/cytoscape/legend.svelte
  • src/lib/components/cytoscape/side-controls.svelte
  • src/lib/mediaStore.ts
💤 Files with no reviewable changes (1)
  • src/lib/components/cytoscape/color-dot.svelte
🧰 Additional context used
📓 Path-based instructions (3)
**/*.svelte

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.svelte: Use Svelte 5 runes ($state, $derived, $effect, $props) for reactivity in all Svelte components
Destructure component props from the $props() rune in Svelte 5 components
Avoid legacy createEventDispatcher; use modern Svelte 5 event patterns

Files:

  • src/lib/components/cytoscape/legend.svelte
  • src/lib/components/cytoscape/help-control.svelte
  • src/lib/components/cytoscape/cytoscape-core.svelte
  • src/lib/components/cytoscape/course-drawer.svelte
  • src/lib/components/cytoscape/cytoscape.svelte
  • src/lib/components/course/tabs/course-prerequisites.svelte
  • src/lib/components/cytoscape/side-controls.svelte
src/lib/components/{course,charts,cytoscape,map}/**/*.{svelte,ts,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Group feature components by functionality under course/, charts/, cytoscape/, and map/

Files:

  • src/lib/components/cytoscape/legend.svelte
  • src/lib/components/cytoscape/help-control.svelte
  • src/lib/components/cytoscape/cytoscape-init.ts
  • src/lib/components/cytoscape/cytoscape-handlers.svelte.ts
  • src/lib/components/cytoscape/cytoscape-core.svelte
  • src/lib/components/cytoscape/graph-layout.ts
  • src/lib/components/cytoscape/course-drawer.svelte
  • src/lib/components/cytoscape/cytoscape.svelte
  • src/lib/components/course/tabs/course-prerequisites.svelte
  • src/lib/components/cytoscape/side-controls.svelte
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use TypeScript with comprehensive typing throughout the codebase

Files:

  • src/lib/components/cytoscape/cytoscape-init.ts
  • src/lib/components/cytoscape/cytoscape-handlers.svelte.ts
  • src/lib/components/cytoscape/graph-layout.ts
  • src/lib/mediaStore.ts
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
Repo: twangodev/uw-coursemap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T08:36:25.768Z
Learning: Applies to src/lib/components/{course,charts,cytoscape,map}/**/*.{svelte,ts,js} : Group feature components by functionality under course/, charts/, cytoscape/, and map/
📚 Learning: 2025-08-24T08:36:25.768Z
Learnt from: CR
Repo: twangodev/uw-coursemap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T08:36:25.768Z
Learning: Applies to src/lib/components/{course,charts,cytoscape,map}/**/*.{svelte,ts,js} : Group feature components by functionality under course/, charts/, cytoscape/, and map/

Applied to files:

  • src/lib/components/cytoscape/cytoscape-init.ts
  • src/lib/components/cytoscape/cytoscape-handlers.svelte.ts
  • src/lib/components/cytoscape/cytoscape-core.svelte
  • src/lib/components/cytoscape/course-drawer.svelte
  • src/lib/components/cytoscape/cytoscape.svelte
  • src/lib/components/course/tabs/course-prerequisites.svelte
📚 Learning: 2025-08-24T08:36:25.768Z
Learnt from: CR
Repo: twangodev/uw-coursemap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T08:36:25.768Z
Learning: Applies to src/routes/**/+page.svelte : Define pages using SvelteKit file-based routing with +page.svelte

Applied to files:

  • src/lib/components/cytoscape/course-drawer.svelte
🧬 Code graph analysis (1)
src/lib/components/cytoscape/cytoscape-init.ts (3)
src/lib/components/cytoscape/graph-layout.ts (2)
  • generateFcoseLayout (77-121)
  • generateLayeredLayout (15-75)
src/lib/types/course.ts (2)
  • CourseReference (5-8)
  • CourseUtils (35-105)
src/lib/components/cytoscape/paths.ts (1)
  • getPredecessorsNotTaken (7-19)
🪛 ast-grep (0.40.3)
src/lib/components/cytoscape/cytoscape-handlers.svelte.ts

[warning] 185-190: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: div.innerHTML = <div class="bg-black text-white p-2 rounded-lg"> <h1 class="text-lg font-semibold">${targetNode.id()}</h1> <p class="text-sm">${targetNode.data("description")}</p> </div>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)


[warning] 185-190: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: div.innerHTML = <div class="bg-black text-white p-2 rounded-lg"> <h1 class="text-lg font-semibold">${targetNode.id()}</h1> <p class="text-sm">${targetNode.data("description")}</p> </div>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)

⏰ 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). (1)
  • GitHub Check: build-web
🔇 Additional comments (19)
src/lib/components/cytoscape/help-control.svelte (1)

2-15: LGTM!

The refactor from prop-based isDesktop to a centralized store is a good pattern for shared responsive state. The import structure is clean, and the store subscription via $isDesktop integrates correctly with the component.

src/lib/components/cytoscape/legend.svelte (1)

14-30: LGTM!

The inline rendering is clean and maintains the same toggling behavior. Good use of $bindable for two-way binding and the aria-label for accessibility.

src/lib/components/cytoscape/graph-layout.ts (1)

15-19: LGTM!

The signature change from focus: string | null to animate: boolean provides clearer intent and explicit control over animation behavior, aligning well with the broader refactor.

src/lib/components/cytoscape/side-controls.svelte (2)

18-32: LGTM!

The shift to callback props (onzoomin, onzoomout, etc.) is a clean pattern that decouples the UI controls from the Cytoscape instance. Proper use of Svelte 5 $props() destructuring with optional callbacks.


76-87: Refactor to use Svelte 5 patterns instead of exported getter functions.

The exported functions (getShowCodeLabels, getLayoutType, getElementsAreDraggable) are not idiomatic Svelte 5 and are unused by the parent component. Since the component already uses $state() for reactive state and $props() for props, expose the internal state directly as reactive exports or use $derived for computed values instead of getter functions.

src/lib/components/cytoscape/cytoscape.svelte (1)

76-98: LGTM on the composition structure.

The component composition with CytoscapeCore, Legend, SideControls, HelpControl, and CourseDrawer is well-organized and follows the refactored architecture. The event wiring through callbacks is clean.

src/lib/components/cytoscape/cytoscape-core.svelte (3)

1-28: Excellent Svelte 5 patterns and TypeScript usage.

The component properly uses Svelte 5 runes ($props, $state) and comprehensive TypeScript typing. The use of ReturnType<typeof setupCytoscapeHandlers> for the handler type is a good pattern.


29-61: Well-structured initialization and cleanup.

The onMount lifecycle properly handles initialization, error cases, and cleanup. The async layout computation with an explanatory comment is clear.


131-131: Clean and minimal template.

The template properly provides the container for Cytoscape with appropriate styling classes.

src/lib/components/cytoscape/course-drawer.svelte (1)

73-111: Well-implemented reactive effects.

The $effect blocks properly track dependencies and guard against undefined references. The focus management and URL parameter synchronization logic is clear and correct.

src/lib/components/cytoscape/cytoscape-handlers.svelte.ts (4)

10-24: Good use of scratch namespace for state management.

Using Cytoscape's scratch space with a namespaced key and type-safe interface is a solid pattern for storing application-specific state on the graph instance.


33-51: Proper reactive mode subscription with cleanup.

The use of $effect.root to manage the mode subscription is correct and the cleanup function is properly returned in the public API. This prevents memory leaks when the handlers are torn down.


52-127: Well-organized internal helper functions.

The helper functions are focused, well-named, and handle their specific responsibilities cleanly. The use of Cytoscape's styling API and class-based styling is appropriate.


200-238: Well-designed public API with proper cleanup.

The public API provides all necessary control methods and the cleanup function properly removes all event handlers and terminates reactive effects, preventing memory leaks.

src/lib/components/cytoscape/cytoscape-init.ts (5)

1-21: Clean imports and well-defined interfaces.

The module imports are organized and the ComputeLayoutOptions interface provides clear typing for the layout computation function.


23-53: Appropriate plugin registration pattern.

The plugin registration is properly guarded with a module-level flag to prevent duplicate registration. The tippyFactory workaround for Cytoscape Popper integration is a reasonable approach.


55-101: Well-designed initialization function.

The initializeCytoscape function provides a clean API with sensible defaults and proper TypeScript typing. The automatic plugin registration on first call is elegant.


103-120: Clean layout computation with proper async handling.

The function provides a simple abstraction for layout computation and properly handles async operations for the layered layout.


131-171: Verify fallback behavior when root course is not found.

When the root node is not found (lines 147-150), the function returns all elementDefinitions unchanged. This could lead to unexpected behavior if the root course ID is invalid or mistyped. Consider whether logging a warning or returning an empty array would be more appropriate.

Current behavior:

if (!rootNode) {
  tempCy.destroy();
  return elementDefinitions; // Returns ALL elements
}

Alternative approaches to consider:

  • Return an empty array if the root is invalid
  • Log a warning to help debug invalid root courses
  • Throw an error if the root is required

What is the intended behavior when a course isn't found in the graph?

Comment thread src/lib/components/course/tabs/course-prerequisites.svelte Outdated
Comment thread src/lib/components/cytoscape/course-drawer.svelte Outdated
Comment thread src/lib/components/cytoscape/cytoscape-handlers.svelte.ts
Comment thread src/lib/components/cytoscape/cytoscape.svelte Outdated
Comment thread src/lib/components/cytoscape/cytoscape.svelte Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
src/lib/components/cytoscape/course-drawer.svelte (1)

90-111: Invalid assignment to $derived variable.

Line 94 attempts to assign focus = null, but focus is defined as a $derived value on Line 53. In Svelte 5, derived values are read-only and cannot be assigned to directly. This will cause a compilation or runtime error.

The logic at lines 105-106 shows the correct approach: modifying page.url.searchParams directly, which will then update the derived focus value. The assignment at line 94 should be removed, as the parameter deletion at line 105 already handles clearing the focus.

🔎 Proposed fix
   $effect(() => {
     if (!cytoscapeCoreRef || !allowFocusing) return;
 
-    if (!sheetOpen) {
-      focus = null;
-    }
-    
     if (sheetOpen) {
       if (selectedCourse) {
         let courseId = CourseUtils.courseReferenceToSanitizedString(
           selectedCourse.course_reference,
         );
         page.url.searchParams.set("focus", courseId);
       }
     } else {
       page.url.searchParams.delete("focus");
     }
 
     tick().then(() => {
       pushState(page.url, page.state);
     });
   });

The page.url.searchParams.delete("focus") already handles clearing the focus parameter when the drawer closes.

♻️ Duplicate comments (4)
src/lib/components/course/tabs/course-prerequisites.svelte (1)

35-43: Previous reactivity issue has been resolved.

The takenCourses and eleDefs values are now properly reactive using Svelte 5's $derived rune, addressing the concern raised in the previous review. When takenCoursesStore updates, both values will re-compute automatically.

src/lib/components/cytoscape/cytoscape.svelte (1)

33-47: Previous issues have been resolved.

The race condition from the setTimeout has been fixed by using an $effect that properly checks for cytoscapeCoreRef availability, and error handling has been added for the fetchCourse promise. The implementation now follows Svelte 5 best practices.

src/lib/components/cytoscape/course-drawer.svelte (1)

67-71: Previous memory leak issue has been resolved.

The direct .subscribe() call has been replaced with a proper $effect using $searchModalOpen, which automatically manages subscription cleanup. This follows Svelte 5 best practices.

src/lib/components/cytoscape/cytoscape-handlers.svelte.ts (1)

183-206: Previous XSS vulnerability has been resolved.

The tooltip creation now properly uses textContent instead of innerHTML and builds the DOM using createElement and appendChild. This prevents injection of malicious HTML/JavaScript from course IDs or descriptions.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f88e5b4 and 49ccdc2.

📒 Files selected for processing (4)
  • src/lib/components/course/tabs/course-prerequisites.svelte
  • src/lib/components/cytoscape/course-drawer.svelte
  • src/lib/components/cytoscape/cytoscape-handlers.svelte.ts
  • src/lib/components/cytoscape/cytoscape.svelte
🧰 Additional context used
📓 Path-based instructions (3)
**/*.svelte

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.svelte: Use Svelte 5 runes ($state, $derived, $effect, $props) for reactivity in all Svelte components
Destructure component props from the $props() rune in Svelte 5 components
Avoid legacy createEventDispatcher; use modern Svelte 5 event patterns

Files:

  • src/lib/components/cytoscape/cytoscape.svelte
  • src/lib/components/cytoscape/course-drawer.svelte
  • src/lib/components/course/tabs/course-prerequisites.svelte
src/lib/components/{course,charts,cytoscape,map}/**/*.{svelte,ts,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Group feature components by functionality under course/, charts/, cytoscape/, and map/

Files:

  • src/lib/components/cytoscape/cytoscape.svelte
  • src/lib/components/cytoscape/cytoscape-handlers.svelte.ts
  • src/lib/components/cytoscape/course-drawer.svelte
  • src/lib/components/course/tabs/course-prerequisites.svelte
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use TypeScript with comprehensive typing throughout the codebase

Files:

  • src/lib/components/cytoscape/cytoscape-handlers.svelte.ts
🧠 Learnings (5)
📓 Common learnings
Learnt from: CR
Repo: twangodev/uw-coursemap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T08:36:25.768Z
Learning: Applies to src/lib/components/{course,charts,cytoscape,map}/**/*.{svelte,ts,js} : Group feature components by functionality under course/, charts/, cytoscape/, and map/
📚 Learning: 2025-08-24T08:36:25.768Z
Learnt from: CR
Repo: twangodev/uw-coursemap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T08:36:25.768Z
Learning: Applies to src/lib/components/{course,charts,cytoscape,map}/**/*.{svelte,ts,js} : Group feature components by functionality under course/, charts/, cytoscape/, and map/

Applied to files:

  • src/lib/components/cytoscape/cytoscape.svelte
  • src/lib/components/cytoscape/cytoscape-handlers.svelte.ts
  • src/lib/components/cytoscape/course-drawer.svelte
  • src/lib/components/course/tabs/course-prerequisites.svelte
📚 Learning: 2025-08-24T08:36:25.768Z
Learnt from: CR
Repo: twangodev/uw-coursemap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T08:36:25.768Z
Learning: Applies to **/*.svelte : Use Svelte 5 runes ($state, $derived, $effect, $props) for reactivity in all Svelte components

Applied to files:

  • src/lib/components/cytoscape/course-drawer.svelte
  • src/lib/components/course/tabs/course-prerequisites.svelte
📚 Learning: 2025-08-24T08:36:25.768Z
Learnt from: CR
Repo: twangodev/uw-coursemap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T08:36:25.768Z
Learning: Applies to **/*.svelte : Avoid legacy createEventDispatcher; use modern Svelte 5 event patterns

Applied to files:

  • src/lib/components/cytoscape/course-drawer.svelte
📚 Learning: 2025-08-24T08:36:25.768Z
Learnt from: CR
Repo: twangodev/uw-coursemap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T08:36:25.768Z
Learning: Applies to src/routes/**/+page.svelte : Define pages using SvelteKit file-based routing with +page.svelte

Applied to files:

  • src/lib/components/cytoscape/course-drawer.svelte
⏰ 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). (2)
  • GitHub Check: build-web
  • GitHub Check: build-web

Comment thread src/lib/components/cytoscape/cytoscape-handlers.svelte.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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)
src/lib/components/cytoscape/cytoscape-handlers.svelte.ts (3)

37-37: Consider typing currentTip more specifically.

The currentTip variable is typed as any. Consider using a more specific type based on the Cytoscape popper API return type for better type safety.


208-210: Consider reordering tooltip lifecycle for clarity.

The current order (show new tip → destroy old tip → store new tip) works correctly but is less clear than: destroy old tip → create new tip → show new tip → store new tip.

🔎 Suggested refactor
+    destroyTip();
     const tip = targetNode.popper({
       content: () => {
         const div = document.createElement("div");
         const container = document.createElement("div");
         container.className = "bg-black text-white p-2 rounded-lg";
         
         const heading = document.createElement("h1");
         heading.className = "text-lg font-semibold";
         heading.textContent = targetNode.id();
         
         const para = document.createElement("p");
         para.className = "text-sm";
         para.textContent = targetNode.data("description");
         
         container.appendChild(heading);
         container.appendChild(para);
         div.appendChild(container);
         return div;
       },
     });
     tip.show();
-    destroyTip();
     currentTip = tip;

1-249: Suggest verification of key behaviors.

As noted in the PR objectives, please verify that the refactored behavior remains correct, particularly:

  • The isDesktop store correctly switches between mobile tooltip and desktop drawer behavior
  • The mode.current reactive updates apply theme changes properly
  • The takenCoursesStore subscription updates graph styling when courses are marked as taken

Run the following verification steps manually:

  1. Toggle between mobile and desktop viewports and verify tap behavior switches between tooltip and drawer
  2. Switch between light/dark mode and verify graph colors update
  3. Mark courses as taken and verify graph styling updates immediately

If you'd like, I can help create automated tests for these behaviors in a future PR.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 49ccdc2 and ebb02a6.

📒 Files selected for processing (2)
  • src/lib/components/cytoscape/course-drawer.svelte
  • src/lib/components/cytoscape/cytoscape-handlers.svelte.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/components/cytoscape/course-drawer.svelte
🧰 Additional context used
📓 Path-based instructions (2)
src/lib/components/{course,charts,cytoscape,map}/**/*.{svelte,ts,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Group feature components by functionality under course/, charts/, cytoscape/, and map/

Files:

  • src/lib/components/cytoscape/cytoscape-handlers.svelte.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use TypeScript with comprehensive typing throughout the codebase

Files:

  • src/lib/components/cytoscape/cytoscape-handlers.svelte.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: twangodev/uw-coursemap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T08:36:25.768Z
Learning: Applies to src/lib/components/{course,charts,cytoscape,map}/**/*.{svelte,ts,js} : Group feature components by functionality under course/, charts/, cytoscape/, and map/
📚 Learning: 2025-08-24T08:36:25.768Z
Learnt from: CR
Repo: twangodev/uw-coursemap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T08:36:25.768Z
Learning: Applies to src/lib/components/{course,charts,cytoscape,map}/**/*.{svelte,ts,js} : Group feature components by functionality under course/, charts/, cytoscape/, and map/

Applied to files:

  • src/lib/components/cytoscape/cytoscape-handlers.svelte.ts
🧬 Code graph analysis (1)
src/lib/components/cytoscape/cytoscape-handlers.svelte.ts (5)
src/lib/takenCoursesStore.ts (1)
  • takenCoursesStore (6-6)
src/lib/theme.ts (2)
  • getTextColor (12-21)
  • getTextOutlineColor (23-32)
src/lib/types/course.ts (1)
  • CourseUtils (35-105)
src/lib/components/cytoscape/paths.ts (2)
  • markNextCourses (37-69)
  • highlightPath (71-97)
src/lib/mediaStore.ts (1)
  • isDesktop (4-4)
⏰ 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). (2)
  • GitHub Check: build-web
  • GitHub Check: build-web
🔇 Additional comments (8)
src/lib/components/cytoscape/cytoscape-handlers.svelte.ts (8)

10-24: LGTM: Clean scratch namespace pattern.

The scratch namespace implementation follows best practices for storing app-specific state on the Cytoscape instance with proper type safety and lazy initialization.


33-55: Previous reactivity issue has been resolved.

The takenCoursesStore subscription (lines 53-55) and cleanup (line 246) have been properly implemented. The store will now trigger applyTakenCoursesStyle reactively whenever courses are marked as taken.


59-94: LGTM: Theme and styling helpers are well-implemented.

The destroyTip and applyThemeAndLabelStyle functions correctly handle cleanup and theme-based styling with appropriate color selections for light/dark modes.


96-119: LGTM: Taken courses styling logic is correct.

The function correctly applies styling based on the taken courses store. Using get() for a one-time read is appropriate here since the function is called reactively via the store subscription (line 54).


121-131: LGTM: Subject hiding logic is correct.

The function properly manages node visibility by restoring previously hidden nodes before hiding new ones, preventing state accumulation issues.


135-173: LGTM: Event handlers implement correct interaction behavior.

The mouse event handlers properly manage highlighting, dragging, and tooltip lifecycle. The integration with path visualization is appropriate.


175-211: Previous XSS vulnerability has been resolved.

The tooltip creation now correctly uses textContent (lines 196, 200) instead of innerHTML, which prevents XSS attacks from malicious course IDs or descriptions.


213-248: LGTM: Clean public API with proper cleanup.

The event registration and public API provide a well-structured interface for controlling the Cytoscape instance. The cleanup method properly unregisters all handlers and unsubscribes from stores (line 246).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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)
src/lib/components/cytoscape/cytoscape-handlers.svelte.ts (3)

37-37: Consider typing currentTip more specifically.

The currentTip variable is typed as any. Based on line 190 where it's assigned from targetNode.popper(), it could be typed as ReturnType<cytoscape.NodeSingular['popper']> or a Cytoscape popper type if available.

🔎 Suggested improvement
-  let currentTip: any = undefined;
+  let currentTip: ReturnType<cytoscape.NodeSingular['popper']> | undefined = undefined;

137-137: Consider typing event parameters more specifically.

The event handler parameters are typed as any. They could be typed as cytoscape.EventObject for better type safety and IDE support.

🔎 Suggested improvement
-  const mouseoverHandler = (event: any) => {
+  const mouseoverHandler = (event: cytoscape.EventObject) => {
     const targetNode = event.target;
     // ...
   };

-  const dbltapHandler = (event: any) => {
+  const dbltapHandler = (event: cytoscape.EventObject) => {
     const targetNode = event.target;
     // ...
   };

-  const onetapHandler = async (event: any) => {
+  const onetapHandler = async (event: cytoscape.EventObject) => {
     const targetNode = event.target;
     // ...
   };

Also applies to: 165-165, 177-177


177-177: Remove unnecessary async keyword.

The onetapHandler function is declared as async but doesn't use await anywhere in its body. The async keyword adds unnecessary overhead.

🔎 Suggested improvement
-  const onetapHandler = async (event: any) => {
+  const onetapHandler = (event: cytoscape.EventObject) => {
     const targetNode = event.target;
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ebb02a6 and 179eea7.

📒 Files selected for processing (1)
  • src/lib/components/cytoscape/cytoscape-handlers.svelte.ts
🧰 Additional context used
📓 Path-based instructions (2)
src/lib/components/{course,charts,cytoscape,map}/**/*.{svelte,ts,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Group feature components by functionality under course/, charts/, cytoscape/, and map/

Files:

  • src/lib/components/cytoscape/cytoscape-handlers.svelte.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use TypeScript with comprehensive typing throughout the codebase

Files:

  • src/lib/components/cytoscape/cytoscape-handlers.svelte.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: twangodev/uw-coursemap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T08:36:25.768Z
Learning: Applies to src/lib/components/{course,charts,cytoscape,map}/**/*.{svelte,ts,js} : Group feature components by functionality under course/, charts/, cytoscape/, and map/
📚 Learning: 2025-08-24T08:36:25.768Z
Learnt from: CR
Repo: twangodev/uw-coursemap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T08:36:25.768Z
Learning: Applies to src/lib/components/{course,charts,cytoscape,map}/**/*.{svelte,ts,js} : Group feature components by functionality under course/, charts/, cytoscape/, and map/

Applied to files:

  • src/lib/components/cytoscape/cytoscape-handlers.svelte.ts
🧬 Code graph analysis (1)
src/lib/components/cytoscape/cytoscape-handlers.svelte.ts (5)
src/lib/theme.ts (2)
  • getTextColor (12-21)
  • getTextOutlineColor (23-32)
src/lib/takenCoursesStore.ts (1)
  • takenCoursesStore (6-6)
src/lib/types/course.ts (1)
  • CourseUtils (35-105)
src/lib/components/cytoscape/paths.ts (3)
  • markNextCourses (37-69)
  • clearPath (99-107)
  • highlightPath (71-97)
src/lib/mediaStore.ts (1)
  • isDesktop (4-4)
⏰ 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). (2)
  • GitHub Check: build-web
  • GitHub Check: build-web
🔇 Additional comments (7)
src/lib/components/cytoscape/cytoscape-handlers.svelte.ts (7)

1-24: LGTM!

The imports are appropriate and the scratch namespace pattern is a clean way to store app-specific state on the Cytoscape instance. The type definitions and helper function are well-structured.


50-105: LGTM!

The styling functions correctly apply theme-based colors and manage taken/next course states. The previous reactivity concern has been properly addressed by the subscription on lines 131-133, ensuring styling updates whenever takenCoursesStore changes.


107-117: LGTM!

The subject visibility logic correctly manages node removal and restoration using the scratch namespace. The optional chaining safely handles null cases.


122-133: LGTM!

The reactive subscriptions are properly implemented using Svelte 5's $effect.root for mode tracking and standard store subscription for taken courses. Both cleanup functions are correctly captured and called in the public cleanup method.


190-207: Excellent fix for the XSS vulnerability!

The tooltip creation now correctly uses textContent instead of innerHTML and builds the DOM structure using document.createElement. This prevents XSS attacks that could occur if course IDs or descriptions contained malicious HTML/JavaScript.


215-219: LGTM!

All event handlers are properly registered with appropriate event types and element selectors.


223-250: LGTM!

The public API is well-designed with a clean separation of concerns. The cleanup method comprehensively removes all event listeners, destroys the tooltip, and unsubscribes from reactive dependencies, preventing memory leaks. The closure-based state management provides good encapsulation.

@twangodev twangodev enabled auto-merge March 30, 2026 03:46
@twangodev twangodev merged commit 5f48197 into main Mar 30, 2026
16 of 17 checks passed
@twangodev twangodev deleted the cytoscape-refactor branch March 30, 2026 03:49
@coderabbitai coderabbitai Bot mentioned this pull request Mar 30, 2026
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.

2 participants