Skip to content

feat: multi-node right menu #1248

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 24, 2025
Merged

Conversation

SonyLeo
Copy link
Contributor

@SonyLeo SonyLeo commented Mar 25, 2025

English | 简体中文

PR

PR Checklist

Please check if your PR fulfills the following requirements:

  • The commit message follows our Commit Message Guidelines
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)
  • Built its own designer, fully self-validated

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

Background and solution

What is the current behavior?

目前不支持画布多选节点右键菜单

Issue Number: N/A

What is the new behavior?

支持画布多选节点右键菜单, 支持以下操作

  1. 删除
    多选节点后右键点击删除,可以批量删除节点元素

  2. 复制
    多选节点后右键点击复制,可以批量复制元素

  3. 添加父级:容器(批量)、容器(公共父级)、文字提示(公共父级)、弹出框(公共父级)
    批量添加:可以给多选节点批量添加 div 容器
    公共父级:可以给多选节点添加一个共同的父级容器、可以选择容器(div)、文字提示(Tooltip)、弹出框 (Popover)

备注:如果是兄弟节点(相同父级且为连续节点),既可以选择批量添加、也可以添加公共父级
如果是非兄弟节点,则只能选择批量添加

  1. 新建区块

保存页面后,多选节点。右键选择新建区块,输入 区块名称 和 区块 ID 后,即可新建区块

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features
    • Added multi-selection support for selecting and manipulating multiple nodes on the canvas.
    • Enhanced context menu with multi-select options including batch delete, copy, and grouping/wrapping of selected nodes.
    • Introduced node replacement insertion logic to support advanced multi-node operations.
  • Bug Fixes
    • Right-click context menu now opens immediately, preventing unintended node selection changes.
  • Improvements
    • Improved schema handling when creating new blocks to better support single and multiple selection scenarios.

Copy link
Contributor

coderabbitai bot commented Mar 25, 2025

Walkthrough

This set of changes introduces comprehensive multi-selection support across the canvas editing interface. The core logic for handling multiple selected nodes is refactored and expanded, including new methods for grouping, batch wrapping, and manipulating multiple nodes within the composables and container logic. The context menu is updated to provide multi-select-specific actions and UI adjustments, while the design canvas and container components integrate the new selection logic for improved interaction handling. Additionally, the node insertion and block creation logic are enhanced to accommodate multi-selection scenarios, ensuring consistent behavior throughout the editor.

Changes

File(s) Change Summary
DesignCanvas.vue Integrated useMultiSelect to track and handle multiple selected nodes. Updated selection logic to set the current schema as an array when multiple nodes are selected.
useCanvas.ts Added a new 'replace' case to the node insertion logic, enabling replacement of a child node at a specific index.
CanvasContainer.vue Refactored right-click handling in node selection to immediately open the context menu and prevent further processing on right-click events.
CanvasMenu.vue Introduced multi-selection support in the context menu. Added multi-select menu items and actions, adjusted menu width and positioning, and updated logic to switch menus based on selection state. Added new operations for batch and group node wrapping, and updated filtering and disabling logic for menu actions.
useMultiSelect.ts Refactored selection state interfaces and added new methods: checking if selected nodes are siblings, grouping selected nodes under a new parent, batch wrapping nodes, and generating wrapper schemas. Updated function signatures and exports to support new multi-selection features.
container.ts Added REPLACE to the POSITION enum and implemented the insertReplace function. Updated insertNode to handle the new replace position, supporting node replacement in multi-selection contexts.
useBlock.ts Improved block creation logic to correctly handle raw and array schemas, ensuring proper extraction and assignment of children when creating blocks from multi-selection or page nodes.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant DesignCanvas
    participant useMultiSelect
    participant CanvasMenu
    participant Container

    User->>DesignCanvas: Select multiple nodes
    DesignCanvas->>useMultiSelect: Update multiSelectedStates
    DesignCanvas->>DesignCanvas: Compute multiStateLength
    DesignCanvas->>DesignCanvas: Set currentSchema (array if multi-selected)
    User->>CanvasMenu: Right-click (open context menu)
    CanvasMenu->>useMultiSelect: Determine isMultiSelect
    CanvasMenu->>CanvasMenu: Show multi-select menu actions
    User->>CanvasMenu: Choose "Group Wrap" or "Batch Wrap"
    CanvasMenu->>useMultiSelect: groupAddParent()/batchAddParent()
    useMultiSelect->>Container: insertNode (with REPLACE or parent wrap)
    Container->>Container: Insert or wrap nodes accordingly
Loading

Poem

Oh, what a hop in the editor’s land,
Now bunnies can multi-select with a wave of their hand!
Group, batch, or replace—nodes gather in glee,
Context menus bloom with new possibility.
With every right-click and selection anew,
The canvas grows clever, for rabbits and you!
🐇✨


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd65991 and 7469e89.

📒 Files selected for processing (2)
  • packages/canvas/container/src/components/CanvasMenu.vue (9 hunks)
  • packages/canvas/container/src/composables/useMultiSelect.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/canvas/container/src/components/CanvasMenu.vue
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: push-check
🔇 Additional comments (13)
packages/canvas/container/src/composables/useMultiSelect.ts (13)

2-3: Imports are appropriate for the enhanced functionality.

The updated imports from @opentiny/tiny-engine-meta-register and @opentiny/tiny-engine-utils provide the necessary dependencies for the new multi-selection features.


7-18: Good refactoring of the selection state interface.

The interface is now more concise with optional properties that clearly represent a node's selection state. The parent field structure with id and children array is well-defined for hierarchical operations.


21-21: Type refinement improves code clarity.

The explicit typing of multiSelectedStates as SelectionState[] improves type safety and code readability.


23-69: Well-structured utility function for creating TinyPopover components.

The createTinyPopoverSchema helper function encapsulates the complex structure of a TinyPopover component, properly handling both single and array content inputs, and setting up the required template slots.


78-78: Improved function signature with explicit types.

The updated signature for toggleMultiSelection with proper TypeScript typing enhances code readability and maintainability.


102-102: Type refinement for selection state refreshing.

The explicit return type for refreshSelectionState improves function clarity.


119-119: Explicit void return type added.

Adding the explicit void return type to clearMultiSelection follows TypeScript best practices for functions that don't return values.


123-134: Well-implemented utility function for node indices.

The getSelectedNodeIndices function correctly:

  1. Maps selected IDs to indices in the children array
  2. Filters out any non-found indices (-1)
  3. Sorts indices for consistent processing order

This provides a solid foundation for the sibling node detection and parent addition functions.


136-162: Efficient sibling nodes detection algorithm.

The areSiblingNodes function efficiently determines if selected nodes share the same parent and are consecutive in the parent's children array. The implementation correctly:

  1. Validates minimum selection requirement
  2. Gets node information with parent context
  3. Checks for same parent ID across all nodes
  4. Verifies consecutive ordering using the getSelectedNodeIndices helper

The optimization using every method for checking consecutive indices aligns with a previous review suggestion.


164-227: The group parent addition implementation has been corrected.

The groupAddParent function properly:

  1. Validates siblings relationship
  2. Collects and preserves selected nodes
  3. Creates an appropriate wrapper (with special handling for TinyPopover)
  4. Correctly removes original nodes from parents
  5. Inserts the new wrapper at the correct position

The implementation now correctly returns true on success, addressing a previous review comment.


229-253: Good abstraction for wrapper schema creation.

The createWrapperSchema function properly abstracts the creation of wrapper components with special handling for TinyPopover. This reduces code duplication between the group and batch parent addition functions.


297-305: Comprehensive exports for multi-selection functionality.

The updated exports correctly include all the new functions: areSiblingNodes, batchAddParent, and groupAddParent, along with the existing functionality.


283-291: Note about future optimization for batch operations.

As mentioned in a previous comment on line 283, the current implementation of insertNode adds a history entry for each operation. For batch operations like batchAddParent, it would be more efficient to have a single history entry for the entire batch.

Consider enhancing the insertNode API in the future to support batch operations with unified history entries.

✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot added the enhancement New feature or request label Mar 25, 2025
@SonyLeo SonyLeo force-pushed the feat/multi-node-right-menu branch from 358e791 to 7a5941c Compare April 22, 2025 06:23
@SonyLeo SonyLeo marked this pull request as ready for review April 22, 2025 06:24
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (7)
packages/canvas/DesignCanvas/src/api/useCanvas.ts (1)

350-354: Implement replace operation for multi-node selection support.

The new replace case in the switch statement provides a direct replacement operation without copying children, unlike the out case which preserves the child nodes. This operation is essential for supporting the multi-node functionality introduced in this PR.

This is a cleaner approach than reusing the existing out case with a flag parameter. Consider adding JSDoc comments to document the difference between out and replace operations for future maintainers.

packages/plugins/block/src/composable/useBlock.ts (1)

328-344: Enhance schema handling for multi-node selection.

This comprehensive refactoring of the schema handling logic now properly supports three scenarios:

  1. Null/undefined schema (returns empty array)
  2. Multiple schemas (when multiple nodes are selected)
  3. Single schema (traditional case)

The updated logic ensures blocks can be created from multiple selected nodes, supporting the multi-node right-click feature.

Consider adding a detailed comment explaining why toRaw is necessary here. It helps prevent reactivity-related issues when working with deep clones of reactive objects.

packages/canvas/DesignCanvas/src/DesignCanvas.vue (1)

183-185: Update schema handling for multi-node selection.

Enhanced the nodeSelected function to work with multiple selected schemas:

  • When multiple nodes are selected, sets current schema to the array of schemas
  • Otherwise, falls back to the single selected node or page schema

This properly integrates with the new multi-node right menu feature.

Consider extracting the multi-selection handling logic into a named function for better readability and testability.

packages/canvas/container/src/components/CanvasMenu.vue (2)

215-223: multiDel / multiCopy side‑effects may wipe selection mid‑loop

removeNodeById and copyNode both end up calling clearSelect, clearing multiSelectedStates during the first iteration. Subsequent IDs in the cached array are still processed, but the UI flickers and emits multiple selected events.

Consider batching:

const ids = [...multiSelectedStates.value.map(s => s.id)]
closeMenu()
ids.forEach(removeNodeById)

and defer clearSelect() until all operations finish.


317-324: actionDisabled can be simplified & made future‑proof

Rather than hard‑coding two separate white‑lists, derive them from the current selection count:

const needSelection = isMultiSelect.value
  ? ['multiDel','multiCopy','multiAddParent']
  : ['del','copy','addParent'];

return needSelection.includes(actionItem.code) &&
       (isMultiSelect.value ? !multiSelectedStates.value.length : !getCurrent().schema?.id)

This removes duplication and scales when new actions are added.

packages/canvas/container/src/composables/useMultiSelect.ts (2)

85-117: areSiblingNodes does two passes over the tree and can be O(N²)

You already obtain nodesWithParent; reuse that array to avoid a second findIndex per node:

const indices = nodesWithParent.map(({ parent, node }) =>
  parent.children.findIndex((c) => c.id === node.id)
)

Minor, but helps when thousands of nodes are selected.


302-318: Potential performance hit in batchAddParent

Each insertNode triggers getController().addHistory() inside container.ts, resulting in n history entries and re‑renders. Consider wrapping the loop in a single history transaction or exposing a bulk API.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9de1275 and 7a5941c.

📒 Files selected for processing (7)
  • packages/canvas/DesignCanvas/src/DesignCanvas.vue (4 hunks)
  • packages/canvas/DesignCanvas/src/api/useCanvas.ts (1 hunks)
  • packages/canvas/container/src/CanvasContainer.vue (1 hunks)
  • packages/canvas/container/src/components/CanvasMenu.vue (9 hunks)
  • packages/canvas/container/src/composables/useMultiSelect.ts (3 hunks)
  • packages/canvas/container/src/container.ts (4 hunks)
  • packages/plugins/block/src/composable/useBlock.ts (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: push-check
🔇 Additional comments (6)
packages/canvas/container/src/CanvasContainer.vue (1)

137-141: Improve right-click handling for multi-selection support.

Moving the right-click menu handling to the beginning of the function and immediately returning prevents selection logic from executing on right-click. This allows users to open the context menu on multiple selected nodes without disrupting the selection state.

packages/plugins/block/src/composable/useBlock.ts (1)

13-13: Add toRaw import for better handling of reactive state.

Added toRaw import from Vue to properly handle reactive state in the createBlock function.

packages/canvas/DesignCanvas/src/DesignCanvas.vue (2)

25-25: Add imports for multi-selection support.

Added necessary imports to support multi-node selection functionality.

Also applies to: 46-46


156-158: Initialize multi-selection state tracking.

Added state tracking for multiple selected nodes using the useMultiSelect composable, which is essential for the multi-node context menu functionality.

packages/canvas/container/src/container.ts (1)

890-895: insertNode switch: new POSITION.REPLACE is fine

Adding the branch keeps behaviour explicit and avoids falling through to IN; good job keeping the control‑flow readable.

packages/canvas/container/src/components/CanvasMenu.vue (1)

122-162: Good separation between single‑ and multi‑select menu definitions

Defining multiSelectMenus alongside the original list keeps concerns clean and avoids runtime if/else clutter inside templates.

@hexqi hexqi added this to the v2.5.0 milestone Apr 22, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
packages/canvas/container/src/components/CanvasMenu.vue (1)

73-78: ⚠️ Potential issue

Sub-menu placement condition is reversed.

The condition logic for submenu positioning is incorrect. When right + 137 < canvasRect.right, it means there is enough space on the right side of the screen, but the code sets the submenu to appear to the right of the parent menu with right: '-137px', which is counterintuitive.

Apply this fix to correct the submenu positioning:

-      if (right + 137 < canvasRect.right) {
-        subMenuStyles.value = { right: '-137px' }
-      } else {
-        subMenuStyles.value = { left: '-137px' }
-      }
+      if (right + 137 > canvasRect.right) {
+        subMenuStyles.value = { left: '-137px' }
+      } else {
+        subMenuStyles.value = { right: '-137px' }
+      }
packages/canvas/container/src/composables/useMultiSelect.ts (1)

280-314: ⚠️ Potential issue

Fix the return value in batchAddParent.

The batchAddParent function always returns false even when operations succeed. It should also refresh the selection state to update the visual selection rectangles.

  multiSelectedStates.value.forEach(({ schema, parent }) => {
    // ... existing implementation ...
  })

-  return false
+  refreshSelectionState()
+  return true
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7a5941c and fd65991.

📒 Files selected for processing (3)
  • packages/canvas/container/src/components/CanvasMenu.vue (9 hunks)
  • packages/canvas/container/src/composables/useMultiSelect.ts (3 hunks)
  • packages/canvas/container/src/container.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/canvas/container/src/container.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/canvas/container/src/composables/useMultiSelect.ts (3)
packages/canvas/types.ts (1)
  • Node (1-6)
packages/register/src/hooks.ts (2)
  • useCanvas (77-77)
  • useHistory (79-79)
packages/canvas/container/src/container.ts (3)
  • selectNode (817-859)
  • insertNode (868-905)
  • POSITION (41-49)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: push-check
🔇 Additional comments (4)
packages/canvas/container/src/components/CanvasMenu.vue (1)

122-162: Good implementation of multi-select menu items.

The multi-select menu is well-structured with appropriate operations for deleting, copying, and adding parent containers to multiple selected nodes. The code also properly checks if nodes are siblings for operations that require contiguous node selection.

packages/canvas/container/src/composables/useMultiSelect.ts (3)

81-114: Well-structured selection utility functions.

The getSelectedNodeIndices and areSiblingNodes functions are well-implemented. The code efficiently uses every to check for consecutive indices, which is a clean optimization over using a loop.


122-210: Good implementation of group parent addition.

The groupAddParent method effectively handles the grouping of sibling nodes with special handling for different component types like TinyPopover. The approach of deleting original nodes and inserting the new wrapper at the first selected node's position is sound.


219-272: Well-implemented wrapper schema creation.

The createWrapperSchema function cleanly handles the creation of wrapper components with special cases for components like TinyPopover. The function properly merges provided props with default ones.

hexqi
hexqi previously approved these changes Apr 24, 2025
@hexqi hexqi merged commit 1f02390 into opentiny:develop Apr 24, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants