Skip to content

Conversation

@Arukuen
Copy link
Contributor

@Arukuen Arukuen commented Oct 22, 2025

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced guided modal tours across the editor, sidebar, and interaction library with multi-step walkthroughs, positioning, target highlighting, navigation, CTA support, and completion handling.
  • Documentation

    • Added comprehensive guides describing tour creation, configuration, and step conventions.
  • Style

    • New styling for tour modals, overlays, glow animations, and added class hooks for targeted UI styling.

@coderabbitai
Copy link

coderabbitai bot commented Oct 22, 2025

Walkthrough

Adds a guided modal tour system: React components and utilities for guided tours, a modal tour renderer with step/position logic, tour definitions and discovery, SCSS styling, PHP settings persistence, and multiple editor integration points via component renders and a require_once in interactions.php.

Changes

Cohort / File(s) Summary
Core guided-tour components
src/editor/components/guided-modal-tour/index.js, src/editor/components/guided-modal-tour/util.js, src/editor/components/guided-modal-tour/tour-conditions.js
New GuidedModalTour React component, global tour state manager (active tour id + listeners), and exported TOUR_CONDITIONS mapping (empty) for per-tour show logic.
Modal tour renderer
src/editor/components/modal-tour/index.js, src/editor/components/modal-tour/tour-steps.js
New ModalTour component implementing multi-step modal, positioning, glow/highlight feature, event-driven navigation, lifecycle hooks, and TOUR_STEPS aggregation.
Tour definitions & discovery
src/editor/components/modal-tour/tours/index.js, src/editor/components/modal-tour/tours/*.js (e.g. editor.js, sidebar.js, interaction-library.js)
Adds multiple tour files and an index that dynamically imports and exports all tours keyed by filename; each tour exports step configurations and optional hooks/flags.
Component exports & integration
src/editor/components/index.js, src/editor/components/interaction-panel/index.js, src/editor/editor.js, src/editor/interaction-library/index.js
Exports GuidedModalTour from components barrel and injects <GuidedModalTour .../> instances into InteractionPanel, editor root, and Interaction Library modal at specific tourIds.
PHP persistence & localization
src/editor/getting-started.php, interactions.php
New Interact_Getting_Started_Screen class registering interact_guided_tour_states option, sanitization, REST exposure, and script localization; interactions.php now requires this file.
Styling
src/editor/components/guided-modal-tour/editor.scss
Adds SCSS for modal layout, overlay, placement variants, glow animations, header/content/CTA/steps styling, responsive rules, and transitions.
Tours docs & README
src/editor/components/guided-modal-tour/README.MD, src/editor/components/modal-tour/tours/README.md, src/editor/components/modal-tour/tour-steps.js
Documentation on creating/configuring tours and aggregation helper; tour-steps aggregator added.
Tour content integration
src/editor/components/modal-tour/tours/editor.js, src/editor/components/modal-tour/tours/interaction-library.js, src/editor/components/modal-tour/tours/sidebar.js
Adds specific tour configurations for editor, interaction-library, and sidebar with step definitions and pre/post-step behaviors.
UI className hooks & small tweaks
src/editor/components/timeline/index.js, src/editor/interaction-library/select-modal.js, src/editor/plugins/block-toolbar-button/index.js, src/editor/plugins/top-toolbar-button/add-interaction-button.js, src/editor/plugins/top-toolbar-button/index.js
Adds/changes className attributes for styling hooks and adjusts selector used when inserting the top toolbar Add button; one select-modal class adds category-specific class.

Sequence Diagram(s)

sequenceDiagram
    participant Editor as Editor UI
    participant GMT as GuidedModalTour
    participant Util as Tour State Manager
    participant MT as ModalTour
    participant WP as WordPress Settings

    Editor->>GMT: Render GuidedModalTour(tourId)
    GMT->>Util: isTourActive() / getActiveTourId()
    alt Active tour exists
        GMT-->>Editor: Do not mount ModalTour
    else No active tour
        GMT->>Util: addTourStateListener(...)
        GMT->>WP: read guidedTourStates
        alt Tour should show
            GMT->>MT: Mount ModalTour (lazy)
            GMT->>Util: setActiveTour(tourId)
            MT->>MT: initialize, compute position, show step
            loop User interaction
                MT->>MT: preStep/postStep, handle next/back, maybe nextEvent
                MT->>MT: update step, reposition, show glow
            end
            MT->>GMT: onClose
            GMT->>Util: clearActiveTour()
            GMT->>WP: update guidedTourStates (append tourId)
            GMT->>Editor: remove tour query param
        else Skip render
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped into the editor's light,
Built glowing steps to guide the sight,
States we keep, and confetti plays,
Tours that sparkle through the days,
A tiny rabbit cheers, "Hooray!" 🎉

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "feat: onboarding tour with name changes" accurately reflects the primary changes in this pull request. The changeset introduces a comprehensive guided modal tour system as the main feature, with supporting infrastructure including a new GuidedModalTour component, tour configuration management, modal UI rendering, and state persistence. The secondary aspect of "name changes" is evidenced throughout the changeset with classname updates (e.g., from "ugb-insert-library-button" to "interact-insert-library-button" and various CSS class additions). The title is specific, concise, and clearly communicates both the main feature and accompanying updates without ambiguity or vague terminology.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ 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-onboarding-tour

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.

@Arukuen Arukuen self-assigned this Oct 22, 2025
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: 8

🧹 Nitpick comments (13)
src/editor/getting-started.php (2)

14-16: Fix indentation consistency.

The indentation appears to use spaces instead of tabs, which is inconsistent with WordPress PHP coding standards. Ensure consistent use of tabs for indentation throughout the file.

Apply this diff to fix the indentation:

-            // Register settings.
-            add_action( 'admin_init', array( $this, 'register_settings' ) );
-            add_action( 'rest_api_init', array( $this, 'register_settings' ) );
+		// Register settings.
+		add_action( 'admin_init', array( $this, 'register_settings' ) );
+		add_action( 'rest_api_init', array( $this, 'register_settings' ) );

32-32: Clarify description text.

The description mentions "block tours" but this setting is for guided interaction tours. Consider updating the text to accurately reflect its purpose.

Apply this diff:

-'description' => __( 'An array of strings representing completed block tours.', 'interactions' ),
+'description' => __( 'An array of strings representing completed guided tours.', 'interactions' ),
src/editor/components/modal-tour/tours/index.js (2)

22-30: Consider making the export pattern more explicit.

The fallback logic that takes the first named export (line 28) could be unpredictable if a tour file accidentally exports multiple named items. Consider either:

  1. Documenting that tour files should always use default exports OR a single named export matching the filename
  2. Adding a warning when multiple named exports are found
  3. Being more specific about which named export to use (e.g., matching the filename)

Consider adding validation:

 	// Prefer default export, fallback to first named export if available
 	if ( tourModule.default ) {
 		tours[ tourName ] = tourModule.default
 	} else {
 		// If no default export, use the first named export (if any)
 		const namedExports = Object.keys( tourModule ).filter( name => name !== 'default' )
 		if ( namedExports.length > 0 ) {
+			if ( namedExports.length > 1 ) {
+				console.warn( `Tour file ${ fileName } has multiple named exports. Using the first one: ${ namedExports[ 0 ] }` )
+			}
 			tours[ tourName ] = tourModule[ namedExports[ 0 ] ]
 		}
 	}

5-5: Consider adding file extension filter for clarity.

The regex /\.js$/ will match any .js file, but for better maintainability and to avoid accidentally picking up other JavaScript files (like test files if added later), consider being more explicit or adding a naming convention check.

You could document that tour files should follow a specific naming pattern, or update the pattern to be more restrictive:

// Only match files that look like tour definitions (no .test.js, .spec.js, etc.)
const tourContext = require.context( './', false, /^(?!.*\.(test|spec)).*\.js$/ )
src/editor/components/guided-modal-tour/editor.scss (2)

64-64: Remove or uncomment the commented border style.

The commented-out border style should either be removed if not needed or uncommented if it serves a purpose during development.

 		border-radius: 16px;
-		// border: 1px solid #f00069ad;
 	}

178-190: Clean up commented styles or document their purpose.

The step indicator styling includes commented-out cursor and hover styles. If these are intentionally disabled, consider adding a comment explaining why. Otherwise, remove them to keep the code clean.

 	height: 8px;
 	border-radius: 20px;
 	background-color: #e1e1e1;
-	// cursor: pointer;
 	padding: 0 !important;
 	margin: 0 !important;

 	&--active {
 		background: #05f;
 		width: 24px;
 		border-radius: 20px;
 	}
-
-	// &:hover {
-	//     background-color: #aaa;
-	// }
 }
src/editor/components/guided-modal-tour/index.js (1)

62-63: Add defensive error handling for condition evaluation.

The condition function is called without error handling. If a tour condition throws an exception, it will break the entire component.

Add a try-catch wrapper:

 	const condition = TOUR_CONDITIONS[ tourId ]
-	const conditionResult = condition ? condition() : null
+	let conditionResult = null
+	if ( condition ) {
+		try {
+			conditionResult = condition()
+		} catch ( error ) {
+			console.error( `Error evaluating condition for tour "${ tourId }":`, error )
+			conditionResult = null // Fallback to default behavior
+		}
+	}
src/editor/components/modal-tour/tours/editor.js (1)

13-17: Stabilize and de‑duplicate the button selector

The same selector is repeated 3x. Centralize it to avoid drift and consider moving to a stable data-* hook to survive class renames.

Apply this diff:

 import { __ } from '@wordpress/i18n'
 import { createInterpolateElement } from '@wordpress/element'

+// Prefer a stable data-* selector in markup; fallback to class until available.
+const INSERT_BTN_SEL = '.interact-insert-library-button'
+
 export const editor = {
   hasConfetti: false,
   steps: [
     {
       title: '👋 ' + __( 'Welcome to Interactions', 'interactions' ),
       description: __( 'Transform your WordPress site with animations and dynamic interactions that bring your content to life. Let’s get started by exploring the Interaction Library.', 'interactions' ),
       help: createInterpolateElement( __( 'Click the <strong>Interactions</strong> button to continue.', 'interactions' ), {
         strong: <strong />,
       } ),
-      anchor: '.interact-insert-library-button',
+      anchor: INSERT_BTN_SEL,
       position: 'bottom',
-      nextEventTarget: '.interact-insert-library-button',
-      glowTarget: '.interact-insert-library-button',
+      nextEventTarget: INSERT_BTN_SEL,
+      glowTarget: INSERT_BTN_SEL,
       showNext: false,
     },
   ],
 }
src/editor/components/guided-modal-tour/util.js (1)

17-20: Avoid redundant notifications and validate listeners

Minor polish: skip notifying when the value is unchanged, and guard listener type.

Apply this diff:

 export const setActiveTour = tourId => {
-  activeTourId = tourId
-  tourStateListeners.forEach( listener => listener( tourId ) )
+  if ( activeTourId === tourId ) return
+  activeTourId = tourId
+  tourStateListeners.forEach( listener => listener( tourId ) )
 }
 
 export const clearActiveTour = () => {
-  activeTourId = null
-  tourStateListeners.forEach( listener => listener( null ) )
+  if ( activeTourId === null ) return
+  activeTourId = null
+  tourStateListeners.forEach( listener => listener( null ) )
 }
 
 export const addTourStateListener = listener => {
-  tourStateListeners.add( listener )
+  if ( typeof listener !== 'function' ) {
+    // No-op remove function for invalid input
+    return () => {}
+  }
+  tourStateListeners.add( listener )
   return () => tourStateListeners.delete( listener )
 }

Also applies to: 25-28, 50-53

src/editor/components/modal-tour/tours/sidebar.js (2)

26-33: Honor reduced‑motion preference when scrolling

Respect users with motion sensitivity by disabling smooth scroll when prefers-reduced-motion is set.

Apply this diff:

       preStep: () => {
-        // Scroll to the preview button before moving to the next step.
-        document.querySelector( '.interact-timeline__preview-button' )?.scrollIntoView( {
-          behavior: 'smooth',
-          block: 'center',
-        } )
+        // Scroll to the preview button before moving to the next step.
+        const btn = document.querySelector( '.interact-timeline__preview-button' )
+        if ( ! btn ) return
+        const prefersReduced =
+          typeof window !== 'undefined' &&
+          window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches
+        btn.scrollIntoView( {
+          behavior: prefersReduced ? 'auto' : 'smooth',
+          block: 'center',
+        } )
       },

47-61: Add guards around data store access

The block editor store should exist here, but add safe checks to avoid rare runtime errors in atypical contexts.

Apply this diff:

-        const blocks = select( 'core/block-editor' ).getBlocks()
+        const editorStore = select( 'core/block-editor' )
+        const blocks = editorStore?.getBlocks?.() || []
@@
-          dispatch( 'core/block-editor' ).selectBlock( innermostBlock.clientId )
+          dispatch( 'core/block-editor' )?.selectBlock?.( innermostBlock.clientId )
src/editor/components/modal-tour/index.js (2)

193-202: Include skipIf in deps and guard against exceptions

Ensure the latest skipIf is used and defensive against errors.

Apply this diff:

-useEffect( () => {
-  if ( skipIf() ) {
+useEffect( () => {
+  let shouldSkip = false
+  try {
+    shouldSkip = !!skipIf()
+  } catch (e) {
+    shouldSkip = false
+  }
+  if ( shouldSkip ) {
     if ( direction === 'forward' ) {
       handleNextEvent()
     } else {
       handleBackEvent()
     }
   }
-}, [ currentStep, direction ] )
+}, [ currentStep, direction, skipIf, handleNextEvent, handleBackEvent ] )

342-345: Correct glow size thresholds (medium branch unreachable for width)

The second condition repeats width > 300, making the width path for medium unreachable. Use a lower width threshold.

Apply this diff:

-    const glowTargetSize = targetRect.width > 300 || targetRect.height > 200 ? 'large'
-      : targetRect.width > 300 || targetRect.height > 100 ? 'medium'
+    const glowTargetSize = targetRect.width > 300 || targetRect.height > 200 ? 'large'
+      : targetRect.width > 200 || targetRect.height > 100 ? 'medium'
       : 'small'
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b6e34f9 and a97f1dd.

📒 Files selected for processing (23)
  • interactions.php (1 hunks)
  • src/editor/components/guided-modal-tour/README.MD (1 hunks)
  • src/editor/components/guided-modal-tour/editor.scss (1 hunks)
  • src/editor/components/guided-modal-tour/index.js (1 hunks)
  • src/editor/components/guided-modal-tour/tour-conditions.js (1 hunks)
  • src/editor/components/guided-modal-tour/util.js (1 hunks)
  • src/editor/components/index.js (1 hunks)
  • src/editor/components/interaction-panel/index.js (2 hunks)
  • src/editor/components/modal-tour/index.js (1 hunks)
  • src/editor/components/modal-tour/tour-steps.js (1 hunks)
  • src/editor/components/modal-tour/tours/README.md (1 hunks)
  • src/editor/components/modal-tour/tours/editor.js (1 hunks)
  • src/editor/components/modal-tour/tours/index.js (1 hunks)
  • src/editor/components/modal-tour/tours/interaction-library.js (1 hunks)
  • src/editor/components/modal-tour/tours/sidebar.js (1 hunks)
  • src/editor/components/timeline/index.js (1 hunks)
  • src/editor/editor.js (2 hunks)
  • src/editor/getting-started.php (1 hunks)
  • src/editor/interaction-library/index.js (3 hunks)
  • src/editor/interaction-library/select-modal.js (1 hunks)
  • src/editor/plugins/block-toolbar-button/index.js (1 hunks)
  • src/editor/plugins/top-toolbar-button/add-interaction-button.js (1 hunks)
  • src/editor/plugins/top-toolbar-button/index.js (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
src/editor/components/modal-tour/tour-steps.js (1)
src/editor/components/modal-tour/tours/index.js (1)
  • tours (6-6)
src/editor/editor.js (1)
src/editor/components/guided-modal-tour/index.js (1)
  • GuidedModalTour (27-104)
src/editor/components/interaction-panel/index.js (1)
src/editor/components/guided-modal-tour/index.js (1)
  • GuidedModalTour (27-104)
src/editor/interaction-library/index.js (1)
src/editor/components/guided-modal-tour/index.js (1)
  • GuidedModalTour (27-104)
src/editor/components/guided-modal-tour/index.js (2)
src/editor/components/guided-modal-tour/util.js (8)
  • isTourActive (35-35)
  • isTourActive (35-35)
  • getActiveTourId (42-42)
  • getActiveTourId (42-42)
  • addTourStateListener (50-53)
  • addTourStateListener (50-53)
  • clearActiveTour (25-28)
  • clearActiveTour (25-28)
src/editor/components/guided-modal-tour/tour-conditions.js (2)
  • TOUR_CONDITIONS (2-2)
  • TOUR_CONDITIONS (2-2)
src/editor/components/modal-tour/index.js (2)
src/editor/components/modal-tour/tour-steps.js (2)
  • TOUR_STEPS (4-6)
  • TOUR_STEPS (4-6)
src/editor/components/guided-modal-tour/util.js (6)
  • setActiveTour (17-20)
  • setActiveTour (17-20)
  • getActiveTourId (42-42)
  • getActiveTourId (42-42)
  • clearActiveTour (25-28)
  • clearActiveTour (25-28)
🪛 LanguageTool
src/editor/components/modal-tour/tours/README.md

[style] ~65-~65: To form a complete sentence, be sure to include a subject.
Context: ...tring, optional) The size of the modal. Can be: - 'small' (default) - 'medium' ...

(MISSING_IT_THERE)


[style] ~74-~74: To form a complete sentence, be sure to include a subject.
Context: ...on of the modal relative to the anchor. Can be: - 'left' - 'right' - 'top' - ...

(MISSING_IT_THERE)

🔇 Additional comments (22)
src/editor/plugins/block-toolbar-button/index.js (1)

130-130: LGTM! Clean styling hook addition.

The className addition follows the project's naming convention and enables targeted styling or selection for the guided tour feature without affecting existing functionality.

src/editor/components/guided-modal-tour/README.MD (1)

1-36: LGTM! Clear and helpful documentation.

The documentation provides a well-structured guide for creating new tours with clear steps and examples. This will help developers implement tours consistently across the codebase.

src/editor/getting-started.php (1)

23-45: LGTM! Setting registration is well-structured.

The setting registration properly exposes the array to the REST API with appropriate schema and sanitization. The default empty array is sensible.

src/editor/components/index.js (1)

11-11: LGTM! Export follows existing patterns.

The GuidedModalTour export is correctly added and follows the same pattern as other component exports in this file.

src/editor/components/interaction-panel/index.js (2)

2-2: LGTM! Import is correctly added.

The GuidedModalTour import is properly added to the existing import statement.


636-636: LGTM! Tour integration is well-placed.

The GuidedModalTour component with tourId="sidebar" is appropriately placed at the end of the InteractionPanel render, allowing it to overlay the panel content when active.

src/editor/components/modal-tour/tour-steps.js (1)

1-6: LGTM! Clean aggregation pattern.

The file provides a clean centralized export for tour steps by spreading the tours object. This pattern allows for easy extension and maintains a single source of truth.

src/editor/interaction-library/index.js (3)

12-12: LGTM! Import is correctly added.

The GuidedModalTour import is properly added and follows the existing import pattern.


197-200: Verify setTimeout doesn't cause issues on unmount.

Wrapping the state resets in a 100ms setTimeout may cause issues if the component unmounts before the timeout fires, potentially leading to React warnings about setting state on an unmounted component. Consider cleanup in a useEffect return or using a ref to track mount status.

If this delay is intentional (e.g., to allow the tour to render before state cleanup), consider adding a comment explaining the rationale and verifying that cleanup is properly handled:

useEffect(() => {
  let timeoutId;
  // Cleanup function
  return () => {
    if (timeoutId) {
      clearTimeout(timeoutId);
    }
  };
}, []);

276-276: LGTM! Tour placement is appropriate.

The GuidedModalTour component is correctly placed inside the Modal, allowing it to display the tour overlay for the interaction library.

interactions.php (1)

138-138: LGTM! File inclusion is properly placed.

The require_once for getting-started.php is correctly added with the proper path and follows the existing pattern in the file.

src/editor/components/timeline/index.js (1)

1113-1113: LGTM! CSS class added for styling/targeting.

The className="interact-timeline__preview-button" attribute provides a styling hook and likely helps the tour system target this button. The BEM naming convention is consistent with the codebase.

src/editor/components/guided-modal-tour/tour-conditions.js (1)

1-2: LGTM - Placeholder for future tour conditions.

The empty TOUR_CONDITIONS object with clear documentation is ready for future per-tour condition overrides. Note that individual tour configurations can also define their own condition functions (as documented in the README), so this serves as a centralized override mechanism when needed.

src/editor/components/modal-tour/tours/README.md (2)

1-164: Excellent documentation for the tour system.

The README provides comprehensive guidance on creating tours, including:

  • Tour structure and properties
  • Step-level configuration options
  • Complete working examples
  • Clear explanations of the discovery mechanism

This will significantly help future contributors create new tours.


3-3: No issues found—the referenced file path is correct.

The file src/editor/components/guided-modal-tour/README.MD exists exactly as referenced in the documentation with the uppercase .MD extension. The path and case are correct and require no changes.

src/editor/interaction-library/select-modal.js (1)

96-96: LGTM - Enables category-specific targeting for guided tours.

Adding per-category CSS classes allows the guided tour system to precisely target and highlight specific categories. This is a clean implementation that aligns with the tour's glowTarget functionality seen in src/editor/components/modal-tour/tours/interaction-library.js.

src/editor/components/modal-tour/tours/interaction-library.js (1)

1-51: LGTM - Well-structured interactive tour.

The interaction library tour provides a clear, step-by-step onboarding experience:

  • Proper i18n for all user-facing text
  • Interactive steps that wait for user actions
  • Appropriate use of glow targets and anchoring

The offset values use string format (e.g., '-400px'), which should work with CSS custom properties but consider verifying this matches the expected format in the ModalTour component.

src/editor/components/guided-modal-tour/editor.scss (1)

1-236: Comprehensive and well-structured styling.

The SCSS provides excellent visual polish for the guided tour system:

  • Proper z-index layering for overlay and glow effects
  • Smooth transitions and animations
  • Flexible positioning system with CSS custom properties
  • Multiple position variants for different anchoring scenarios
src/editor/components/guided-modal-tour/index.js (2)

42-47: LGTM - Proper cleanup of event listener.

The effect hook correctly subscribes to tour state changes and returns the cleanup function to remove the listener on unmount or when tourId changes.


27-30: LGTM - Using memo appropriately.

The component is wrapped in memo which is appropriate given it only depends on the tourId prop and includes internal state management.

src/editor/editor.js (2)

9-9: GuidedModalTour import is correctly resolved.

Verification confirms that GuidedModalTour is properly exported through the import chain:

  • src/editor/components/index.js line 11 re-exports it from ./guided-modal-tour
  • src/editor/components/guided-modal-tour/index.js line 106 exports it as default

The import in src/editor/editor.js resolves correctly. No issues found.


449-449: No issues found — editor tour is properly defined and integrated.

The editor tour exists at src/editor/components/modal-tour/tours/editor.js with a well-formed export structure. The dynamic discovery pattern in index.js correctly handles the named export, and the integration through tour-steps.js to the ModalTour component works as intended. The tourId="editor" prop will correctly resolve to the tour definition.

Comment on lines 86 to 91
// Update the interact_guided_tour_states setting
if ( ! guidedTourStates.includes( tourId ) ) {
// eslint-disable-next-line camelcase
const settings = new models.Settings( { interact_guided_tour_states: [ ...guidedTourStates, tourId ] } )
settings.save()
}
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

Add error handling for settings persistence.

The settings.save() call lacks error handling. If the API request fails, the tour will be marked as complete locally but not persisted, leading to the tour reappearing on page reload.

Add error handling:

 					// Update the interact_guided_tour_states setting
 					if ( ! guidedTourStates.includes( tourId ) ) {
 						// eslint-disable-next-line camelcase
 						const settings = new models.Settings( { interact_guided_tour_states: [ ...guidedTourStates, tourId ] } )
-						settings.save()
+						settings.save().catch( error => {
+							console.error( 'Failed to save tour state:', error )
+							// Optionally: notify the user or retry
+						} )
 					}
📝 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
// Update the interact_guided_tour_states setting
if ( ! guidedTourStates.includes( tourId ) ) {
// eslint-disable-next-line camelcase
const settings = new models.Settings( { interact_guided_tour_states: [ ...guidedTourStates, tourId ] } )
settings.save()
}
// Update the interact_guided_tour_states setting
if ( ! guidedTourStates.includes( tourId ) ) {
// eslint-disable-next-line camelcase
const settings = new models.Settings( { interact_guided_tour_states: [ ...guidedTourStates, tourId ] } )
settings.save().catch( error => {
console.error( 'Failed to save tour state:', error )
// Optionally: notify the user or retry
} )
}
🤖 Prompt for AI Agents
In src/editor/components/guided-modal-tour/index.js around lines 86–91, the
settings.save() call is invoked without error handling which lets the tour be
marked complete locally even if the API save fails; change the flow to persist
first and only update local guidedTourStates on successful save (or, if you must
update optimistically, add a catch to revert the local state), and in the catch
log the error and surface a user-facing notification (or retry) so failures are
visible and data remains consistent.

}

// Soft update the global variable to prevent the tour from being shown again.
guidedTourStates.push( tourId )
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Avoid direct mutation of the imported array.

Directly mutating guidedTourStates with .push() is a side effect that could lead to unexpected behavior in React. While this appears to be a cache optimization after the API call, consider using immutable updates or documenting this as an intentional cache sync.

If guidedTourStates is meant to be mutated as a global cache, add a comment explaining this. Otherwise, consider if this mutation is necessary since the component already handles state with setIsDone(true):

 					// Soft update the global variable to prevent the tour from being shown again.
+					// Note: This mutates the global cache to prevent re-renders in other components
 					guidedTourStates.push( tourId )

Based on learnings: In React, direct mutations of imported data structures should be avoided or clearly documented as intentional cache synchronization.

📝 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
guidedTourStates.push( tourId )
// Soft update the global variable to prevent the tour from being shown again.
// Note: This mutates the global cache to prevent re-renders in other components
guidedTourStates.push( tourId )

Comment on lines 278 to 291
const modalRect = modalRef.current.querySelector( '.interact-tour-modal' ).getBoundingClientRect()
const defaultOffset = [ `${ ( window.innerWidth / 2 ) - ( modalRect.width / 2 ) }px`, `${ ( window.innerHeight / 2 ) - ( modalRect.height / 2 ) }px` ]

if ( ! anchor ) {
return defaultOffset // This is for the entire screen.
}

// Based on the anchor and position, calculate the X and Y offsets of the modal relative to the anchor.
// We have the modalRef.current which we can use to get the modal's bounding client rect.
const anchorRect = document.querySelector( anchor )?.getBoundingClientRect()

if ( ! anchorRect ) {
return defaultOffset
}
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 modal/anchor queries to avoid runtime errors

querySelector may be unavailable or return null; bail out safely.

Apply this diff:

-    const modalRect = modalRef.current.querySelector( '.interact-tour-modal' ).getBoundingClientRect()
+    const modalEl = modalRef.current?.querySelector?.( '.interact-tour-modal' )
+    if ( ! modalEl ) {
+      return [ '', '' ]
+    }
+    const modalRect = modalEl.getBoundingClientRect()
@@
-    const anchorRect = document.querySelector( anchor )?.getBoundingClientRect()
+    const anchorRect = document.querySelector( anchor )?.getBoundingClientRect()
📝 Committable suggestion

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

Suggested change
const modalRect = modalRef.current.querySelector( '.interact-tour-modal' ).getBoundingClientRect()
const defaultOffset = [ `${ ( window.innerWidth / 2 ) - ( modalRect.width / 2 ) }px`, `${ ( window.innerHeight / 2 ) - ( modalRect.height / 2 ) }px` ]
if ( ! anchor ) {
return defaultOffset // This is for the entire screen.
}
// Based on the anchor and position, calculate the X and Y offsets of the modal relative to the anchor.
// We have the modalRef.current which we can use to get the modal's bounding client rect.
const anchorRect = document.querySelector( anchor )?.getBoundingClientRect()
if ( ! anchorRect ) {
return defaultOffset
}
const modalEl = modalRef.current?.querySelector?.( '.interact-tour-modal' )
if ( ! modalEl ) {
return [ '', '' ]
}
const modalRect = modalEl.getBoundingClientRect()
const defaultOffset = [ `${ ( window.innerWidth / 2 ) - ( modalRect.width / 2 ) }px`, `${ ( window.innerHeight / 2 ) - ( modalRect.height / 2 ) }px` ]
if ( ! anchor ) {
return defaultOffset // This is for the entire screen.
}
// Based on the anchor and position, calculate the X and Y offsets of the modal relative to the anchor.
// We have the modalRef.current which we can use to get the modal's bounding client rect.
const anchorRect = document.querySelector( anchor )?.getBoundingClientRect()
if ( ! anchorRect ) {
return defaultOffset
}
🤖 Prompt for AI Agents
In src/editor/components/modal-tour/index.js around lines 278 to 291, the code
calls modalRef.current.querySelector(...) and
document.querySelector(anchor).getBoundingClientRect() without guarding for
null/undefined which can throw; update the logic to first verify modalRef and
modalRef.current exist and that querySelector returns an element before calling
getBoundingClientRect, and likewise check document.querySelector(anchor) exists
before using its rect; if any of these are missing (or window is undefined),
immediately return the defaultOffset to bail out safely.

Comment on lines +361 to +373
useEffect( () => {
const handleHeaderClick = () => {
onClose()
}
if ( modalRef.current ) {
modalRef.current.querySelector( '.components-modal__header > .components-button' ).addEventListener( 'click', handleHeaderClick )
}
return () => {
if ( modalRef.current ) {
modalRef.current.querySelector( '.components-modal__header > .components-button' ).removeEventListener( 'click', handleHeaderClick )
}
}
}, [ modalRef.current, onClose ] )
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

Null‑safe close‑button hook and stable dependencies

Query can return null; also avoid using ref.current in deps. Hook the close button safely and re-run when the modal shows.

Apply this diff:

-useEffect( () => {
-  const handleHeaderClick = () => {
-    onClose()
-  }
-  if ( modalRef.current ) {
-    modalRef.current.querySelector( '.components-modal__header > .components-button' ).addEventListener( 'click', handleHeaderClick )
-  }
-  return () => {
-    if ( modalRef.current ) {
-      modalRef.current.querySelector( '.components-modal__header > .components-button' ).removeEventListener( 'click', handleHeaderClick )
-    }
-  }
-}, [ modalRef.current, onClose ] )
+useEffect( () => {
+  const handleHeaderClick = () => onClose()
+  const closeBtn = modalRef.current?.querySelector?.( '.components-modal__header > .components-button' )
+  closeBtn?.addEventListener( 'click', handleHeaderClick )
+  return () => closeBtn?.removeEventListener( 'click', handleHeaderClick )
+}, [ isVisible, onClose ] )

Optional: for accessibility, consider wiring onRequestClose={ onClose } to support ESC key closing while still keeping shouldCloseOnClickOutside={ false }.

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

🤖 Prompt for AI Agents
In src/editor/components/modal-tour/index.js around lines 361 to 373, the effect
assumes querySelector returns an element and uses modalRef.current in the
dependency array; update the effect to guard against null from querySelector
before adding/removing the listener, reference modalRef (not modalRef.current)
in the deps so React tracks the ref object, and include the modal visibility
prop (e.g. isOpen or similar) so the hook re-runs when the modal is shown;
attach the click listener only if the button element exists and remove it in the
cleanup safely.

}
} }
className="ugb-insert-library-button"
className="interact-insert-library-button"
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

🧩 Analysis chain

Verify selector compatibility with the wrapper element.

The className was updated to interact-insert-library-button, but in src/editor/plugins/top-toolbar-button/index.js (line 25), the code searches for .interact-insert-library-button__wrapper. This selector won't match the button's className and also won't match the wrapper div's className (interact-add-interaction-button-wrapper from line 13 of index.js).

Please confirm whether:

  1. There's another element with the interact-insert-library-button__wrapper class not shown in this diff, or
  2. The selector in index.js should be updated to match the actual wrapper className

🏁 Script executed:

#!/bin/bash
# Search for any elements with the class "interact-insert-library-button__wrapper"
# to verify if this selector will match any elements

rg -n "interact-insert-library-button__wrapper" --type js --type jsx --type ts --type tsx -C3

Length of output: 27



Let me now search the codebase directly for the files and examine them:


Verify selector compatibility with the wrapper element.

The className was updated to interact-insert-library-button, but code in src/editor/plugins/top-toolbar-button/index.js searches for .interact-insert-library-button__wrapper. Confirm whether this selector exists in the codebase or if it needs updating to match the actual wrapper classNames.


🏁 Script executed:

#!/bin/bash
# Search for the selector using correct ripgrep syntax
# The "js" type includes .js, .jsx, and .vue files
rg -n "interact-insert-library-button__wrapper" -tjs -C3

Length of output: 844


🏁 Script executed:

#!/bin/bash
# Cat the two files to examine them directly
echo "=== add-interaction-button.js (lines 20-35) ==="
cat src/editor/plugins/top-toolbar-button/add-interaction-button.js | sed -n '20,35p'

echo -e "\n=== index.js (lines 1-30) ==="
cat src/editor/plugins/top-toolbar-button/index.js | sed -n '1,30p'

echo -e "\n=== Search for the selector anywhere in the codebase ==="
rg "interact-insert-library-button__wrapper" -tjs -C2

Length of output: 2414


Update selector in index.js to match the actual button className.

The verification confirms the original review comment's concern: the selector .interact-insert-library-button__wrapper does not exist anywhere in the codebase. The button element has class interact-insert-library-button (without the __wrapper suffix), and the wrapper div has class interact-add-interaction-button-wrapper.

Line 25 of index.js must be updated to query for an element that actually exists. Either update the selector to .interact-insert-library-button, or reconsider if this fallback positioning logic is still needed.

🤖 Prompt for AI Agents
In src/editor/plugins/top-toolbar-button/add-interaction-button.js around line
26, the selector `.interact-insert-library-button__wrapper` does not match any
element; update the query to use the actual class
`.interact-insert-library-button` (or, if you intended to target the wrapper,
use `.interact-add-interaction-button-wrapper`) so the fallback positioning
logic finds the element; after updating the selector, run the fallback path to
confirm positioning still behaves as expected and remove the fallback logic if
it is no longer necessary.

Comment on lines +24 to +25
// If .interact-insert-library-button__wrapper button is present, add after this button.
const insertLibraryButton = toolbar.querySelector( '.interact-insert-library-button__wrapper' )
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

🧩 Analysis chain

Verify the selector targets an existing element.

The selector was updated to .interact-insert-library-button__wrapper, aligning with the className refactoring. However, the button created in add-interaction-button.js has className interact-insert-library-button (without __wrapper suffix), and the wrapper div created in line 13 has className interact-add-interaction-button-wrapper.

The fallback to appendChild (line 29) ensures the button will still be added even if the selector doesn't match, but please verify that .interact-insert-library-button__wrapper matches an actual DOM element for proper insertion positioning.


🏁 Script executed:

#!/bin/bash
# Search for where "interact-insert-library-button__wrapper" class is defined
# to verify the selector will match an actual element

rg -n "interact-insert-library-button__wrapper" --type js --type jsx --type ts --type tsx --type css --type scss -C3

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Search for the selector string across all files (fixing the file type error)
echo "=== Searching for 'interact-insert-library-button__wrapper' ==="
rg -n "interact-insert-library-button__wrapper" -C3

echo ""
echo "=== Searching for 'interact-insert-library-button' (broader search) ==="
rg -n "interact-insert-library-button" -C3

echo ""
echo "=== Checking add-interaction-button.js for actual classNames ==="
fd -n "add-interaction-button.js" --exec cat {}

Length of output: 3661


🏁 Script executed:

#!/bin/bash
# View the add-interaction-button.js file to understand the component structure
echo "=== Content of add-interaction-button.js ==="
cat src/editor/plugins/top-toolbar-button/add-interaction-button.js

echo ""
echo "=== Check what wraps the button (look for parent elements) ==="
rg -n "interact-insert-library-button" -B5 -A5 src/editor/plugins/top-toolbar-button/add-interaction-button.js

Length of output: 1368


Update selector to match the button's actual className.

The selector on line 25 searches for .interact-insert-library-button__wrapper, but the button in add-interaction-button.js has className interact-insert-library-button (without the __wrapper suffix). This mismatch means the selector will never find the element, causing the code to always fall back to appendChild, which breaks the intended positioning.

Update line 25 to:

const insertLibraryButton = toolbar.querySelector( '.interact-insert-library-button' )
🤖 Prompt for AI Agents
In src/editor/plugins/top-toolbar-button/index.js around lines 24 to 25, the
querySelector uses the wrong class name
('.interact-insert-library-button__wrapper') which does not match the actual
button class; change the selector to '.interact-insert-library-button' so the
code finds the existing button and inserts in the correct position instead of
always falling back to appendChild.

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

♻️ Duplicate comments (1)
src/editor/components/modal-tour/index.js (1)

367-380: Duplicate: Close button handler has unstable dependencies.

This effect still includes modalRef.current in the dependency array, which is problematic because ref mutations don't trigger re-renders. The previous review comment suggested using isVisible in the dependencies instead and adding proper guards for querySelector.

For reference, the suggested fix from the previous review:

-useEffect( () => {
-  const handleHeaderClick = () => {
-    onClose()
-  }
-  if ( modalRef.current ) {
-    modalRef.current.querySelector( '.components-modal__header > .components-button' ).addEventListener( 'click', handleHeaderClick )
-  }
-  return () => {
-    if ( modalRef.current ) {
-      modalRef.current.querySelector( '.components-modal__header > .components-button' ).removeEventListener( 'click', handleHeaderClick )
-    }
-  }
-}, [ modalRef.current, onClose ] )
+useEffect( () => {
+  const handleHeaderClick = () => onClose()
+  const closeBtn = modalRef.current?.querySelector?.( '.components-modal__header > .components-button' )
+  closeBtn?.addEventListener( 'click', handleHeaderClick )
+  return () => closeBtn?.removeEventListener( 'click', handleHeaderClick )
+}, [ isVisible, onClose ] )
🧹 Nitpick comments (2)
src/editor/components/modal-tour/index.js (2)

194-203: Include callback dependencies in effect array.

The effect calls handleNextEvent and handleBackEvent but doesn't list them in the dependency array. While they're stable due to useCallback, including them follows React best practices and satisfies the exhaustive-deps rule.

Apply this diff:

 	// If we just moved to this step, even before showing it check if we should skip it, if so, move to the next/prev step.
 	useEffect( () => {
 		if ( skipIf() ) {
 			if ( direction === 'forward' ) {
 				handleNextEvent()
 			} else {
 				handleBackEvent()
 			}
 		}
-	}, [ currentStep, direction ] )
+	}, [ currentStep, direction, handleNextEvent, handleBackEvent, skipIf ] )

274-338: Remove modalRef.current from dependencies.

Including modalRef.current in the useMemo dependency array is an anti-pattern because ref mutations don't trigger re-renders and reading ref.current during render is unreliable. The forceRefresh state already handles re-computation when needed.

Apply this diff:

-	}, [ anchor, position, modalRef.current, isVisible, isVisibleDelayed, isTransitioning, forceRefresh ] )
+	}, [ anchor, position, isVisible, isVisibleDelayed, isTransitioning, forceRefresh ] )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a97f1dd and c4a6087.

📒 Files selected for processing (3)
  • src/editor/components/guided-modal-tour/index.js (1 hunks)
  • src/editor/components/modal-tour/index.js (1 hunks)
  • src/editor/components/modal-tour/tours/sidebar.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/editor/components/modal-tour/tours/sidebar.js
🧰 Additional context used
🧬 Code graph analysis (2)
src/editor/components/guided-modal-tour/index.js (3)
src/editor/components/modal-tour/index.js (2)
  • props (496-499)
  • ModalTour (33-464)
src/editor/components/guided-modal-tour/util.js (8)
  • isTourActive (35-35)
  • isTourActive (35-35)
  • getActiveTourId (42-42)
  • getActiveTourId (42-42)
  • addTourStateListener (50-53)
  • addTourStateListener (50-53)
  • clearActiveTour (25-28)
  • clearActiveTour (25-28)
src/editor/components/guided-modal-tour/tour-conditions.js (2)
  • TOUR_CONDITIONS (2-2)
  • TOUR_CONDITIONS (2-2)
src/editor/components/modal-tour/index.js (3)
src/editor/components/guided-modal-tour/index.js (2)
  • ModalTour (73-73)
  • props (28-30)
src/editor/components/modal-tour/tour-steps.js (2)
  • TOUR_STEPS (4-6)
  • TOUR_STEPS (4-6)
src/editor/components/guided-modal-tour/util.js (6)
  • setActiveTour (17-20)
  • setActiveTour (17-20)
  • getActiveTourId (42-42)
  • getActiveTourId (42-42)
  • clearActiveTour (25-28)
  • clearActiveTour (25-28)
🔇 Additional comments (11)
src/editor/components/guided-modal-tour/index.js (4)

1-24: LGTM!

The imports are well-organized and include all necessary dependencies for tour state management, conditions, and React hooks.


27-56: LGTM!

The state management and tour conflict prevention logic is well-implemented. The listener cleanup is properly handled in the effect's return function.


58-70: LGTM!

The condition logic correctly handles the three-state system (true/false/null) and appropriately gates tour visibility based on completion state.


79-102: LGTM!

The onClose handler correctly manages tour completion: clears active tour state, persists to WordPress settings with error handling, updates the global cache, and cleans up URL parameters. Previous review concerns about error handling have been addressed.

src/editor/components/modal-tour/index.js (7)

1-46: LGTM!

The imports, constant definitions, and initial component setup are well-structured. State management and ref usage are appropriate for the tour functionality.


48-78: LGTM!

The tour configuration and step access are properly guarded with fallbacks (|| {}), preventing crashes when tourId is invalid or steps are empty. Previous review concerns have been addressed.


80-111: LGTM!

The initialization and active tour management effects are well-implemented with proper cleanup. The force refresh interval ensures modal positioning stays accurate during visibility.


113-192: LGTM!

Both navigation handlers (handleNextEvent and handleBackEvent) properly manage transitions, visibility states, and step lifecycle hooks with appropriate optional chaining. The callback dependencies are correctly specified.


205-272: LGTM!

The initial visibility delay, event listener setup (with proper delegation for click events), and glow element lifecycle are all correctly implemented with appropriate cleanup functions.


340-365: LGTM!

The glow target positioning logic correctly calculates size classes and updates the glow element's position dynamically. Proper guards ensure safe DOM access.


382-522: LGTM!

The render logic, confetti animation, and Steps indicator component are all well-implemented. The dynamic styling via inline <style> and conditional rendering are appropriate for this use case.

Comment on lines +72 to +73
// Only lazy-load ModalTour when we're actually going to render it
const ModalTour = lazy( () => import( /* webpackChunkName: "modal-tour" */ '../modal-tour' ) )
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

Move lazy() call to module scope.

The lazy() call is currently inside the component's render path, which means it's re-invoked on every render. React's lazy() must be called once at the module level to properly enable code-splitting and avoid remounting issues.

Apply this diff:

+// Only lazy-load ModalTour when we're actually going to render it
+const ModalTour = lazy( () => import( /* webpackChunkName: "modal-tour" */ '../modal-tour' ) )
+
 // The main tour component.
 const GuidedModalTour = memo( props => {
 	const {
@@ -69,9 +72,6 @@
 		}
 	}
 
-	// Only lazy-load ModalTour when we're actually going to render it
-	const ModalTour = lazy( () => import( /* webpackChunkName: "modal-tour" */ '../modal-tour' ) )
-
 	return (
 		<Suspense fallback={ null }>
 			<ModalTour
📝 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
// Only lazy-load ModalTour when we're actually going to render it
const ModalTour = lazy( () => import( /* webpackChunkName: "modal-tour" */ '../modal-tour' ) )
// Only lazy-load ModalTour when we're actually going to render it
const ModalTour = lazy( () => import( /* webpackChunkName: "modal-tour" */ '../modal-tour' ) )
// The main tour component.
const GuidedModalTour = memo( props => {
const {
tourId,
isDone,
setIsDone,
} = props
const markTourAsComplete = useCallback( async () => {
setIsDone( true )
const guidedTourStates = window.interactEvents?.guidedTourStates || []
if ( guidedTourStates.includes( tourId ) ) {
return
}
// Update the interact_guided_tour_states setting
if ( ! guidedTourStates.includes( tourId ) ) {
// eslint-disable-next-line camelcase
const settings = new models.Settings( { interact_guided_tour_states: [ ...guidedTourStates, tourId ] } )
settings.save().catch( error => {
console.error( 'Failed to save tour state:', error )
// Optionally: notify the user or retry
} )
}
// Soft update the global variable to prevent the tour from being shown again.
// Note: This mutates the global cache to prevent re-renders in other components
guidedTourStates.push( tourId )
}, [ tourId, setIsDone ] )
return (
<Suspense fallback={ null }>
<ModalTour
onClose={ markTourAsComplete }
/>
</Suspense>
)
} )
🤖 Prompt for AI Agents
In src/editor/components/guided-modal-tour/index.js around lines 72-73, the
lazy() call for ModalTour is inside the component render path causing it to be
re-invoked on every render; move the const ModalTour = lazy(() => import(/*
webpackChunkName: "modal-tour" */ '../modal-tour')) to module scope (top of the
file) so it runs once at load time, remove the in-render declaration and
reference the top-level ModalTour variable instead; also ensure React.lazy (or
lazy) is imported at the top of the file if not already.

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