Skip to content

Ability to select multiple blocks on Drag and Drop App Builder #1151

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 25 commits into from
Jun 13, 2025

Conversation

NaveenRamasamy028
Copy link
Contributor

Now the user can able to select multiple blocks.

Copy link

@CodiumAI-Agent /describe

@CodiumAI-Agent
Copy link

Title

Ability to select multiple blocks on Drag and Drop App Builder


User description

Now the user can able to select multiple blocks.


PR Type

Enhancement


Description

  • Support ctrl/meta/shift multi-block selection

  • Show multiple blocks indicator UI

  • Enable drag & drop of multiple blocks

  • Track multi-selected IDs in store


Changes walkthrough 📝

Relevant files
Enhancement
SelectedBlockPanel.tsx
Update selection panel for multiple blocks                             

packages/client/src/components/blocks-workspace/panels/SelectedBlockPanel.tsx

  • Added styled components for multi-block UI
  • Display message when multiple blocks selected
  • Updated no-selection panel styling and icon
  • +92/-5   
    AddBlocksMenuCard.tsx
    Clear multi-selection on new block add                                     

    packages/client/src/components/designer/AddBlocksMenuCard.tsx

    • Reset multiselectedIds when adding a block
    +3/-0     
    Screen.tsx
    Handle multi-block selection clicks                                           

    packages/client/src/components/designer/Screen.tsx

  • Added handleMultipleSelection for ctrl/meta/shift clicks
  • Support accumulating and clearing multi-selection
  • Show selection mask when multiple blocks selected
  • +32/-2   
    SelectedMask.tsx
    Extend mask logic for multiple blocks                                       

    packages/client/src/components/designer/SelectedMask.tsx

  • Added areAllBlocksDraggable check for multi-selection
  • Support multi-block drag activation and movement
  • Dispatch MOVE_BLOCK/SET_BLOCK_DATA for each selected block
  • Render individual masks for each selected block
  • +250/-96
    State
    designer.store.ts
    Add multi-selection state to designer store                           

    packages/client/src/stores/designer/designer.store.ts

  • Introduced multiselectedIds array in store
  • Added getter and setMultiSelectedIds setter
  • +24/-0   

    Need help?
  • Type /help how to ... in the comments thread for any questions about PR-Agent usage.
  • Check out the documentation for more information.
  • Copy link

    @CodiumAI-Agent /review

    Copy link

    @CodiumAI-Agent /improve

    @CodiumAI-Agent
    Copy link

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

    ⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
    🧪 No relevant tests
    🔒 No security concerns identified
    ⚡ Recommended focus areas for review

    Multi-select state

    The setMultiSelectedIds API uses string flags and pushes IDs directly, risking duplicates and unclear reset logic. Consider using a clearer signature (e.g., accept arrays or separate add/remove) and deduplicating entries.

    /**
     * Set the multi-selected block IDs
     * @param id - id of the block to add to the multi-selected list, or "clear" to reset the list
     */
    setMultiSelectedIds(id: string) {
        if (id === 'clear') {
            this._store.multiselectedIds = [];
        } else {
            this._store.multiselectedIds.push(id);
        }
    }
    Selection handling

    The handleMultipleSelection logic resets selected when multiple IDs are chosen, which may lead to inconsistent UI states. Verify the flow between selected and multiselectedIds, and ensure proper toggling and clearing.

    const handleMultipleSelection = (event) => {
        if (!designer.hovered || designer.hovered === designer.selected) {
            return;
        }
        const id = getNearestBlock(event.target as Element);
    
        // prevent events for elements until selected
        event.stopPropagation();
        event.preventDefault();
        if (designer.multiselectedIds.includes(id)) {
            return; // Do nothing if the id is already selected
        }
    
        designer.setSelected(id);
        designer.setMultiSelectedIds(id);
        if (designer.multiselectedIds.length > 1) {
            designer.setSelected('');
        }
    };
    Drag-and-drop complexity

    The multi-block drag-and-drop logic in handleDocumentMouseUp is complex and duplicates code paths. Ensure correct order when moving blocks, handle failures gracefully, and consider refactoring to reduce duplication and improve maintainability.

    if (designer.multiselectedIds.length > 1) {
        // Handle multiple block movements
        let lastSiblingId = placeholderAction.id; // Start with the placeholder ID
        designer.multiselectedIds.forEach((id) => {
            const sw = state.getBlock(placeholderAction.id);
    
            if (
                placeholderAction.type === 'before' ||
                placeholderAction.type === 'after'
            ) {
                const siblingWidget = state.getBlock(lastSiblingId); // Use the last sibling ID
    
                if (siblingWidget.parent) {
                    const parent = state.getBlock(sw.parent.id);
                    if (parent.widget === 'iteration') {
                        if (parent.slots.children.children.length) {
                            notification.add({
                                color: 'error',
                                message:
                                    'Please delete block within iterator before adding another child',
                            });
                            designer.deactivateDrag();
                            return;
                        }
                    }
                    state.dispatch({
                        message: ActionMessages.MOVE_BLOCK,
                        payload: {
                            id,
                            position: {
                                parent: siblingWidget.parent.id,
                                slot: siblingWidget.parent.slot,
                                sibling:
                                    placeholderAction.type === 'before'
                                        ? siblingWidget.id
                                        : lastSiblingId,
                                type: placeholderAction.type,
                            },
                        },
                    });
    
                    // Update the lastSiblingId to the current block
                    lastSiblingId = id;
                }
            } else if (placeholderAction.type === 'replace') {
                if (sw.widget !== 'iteration') {
                    state.dispatch({
                        message: ActionMessages.SET_BLOCK_DATA,
                        payload: {
                            id: placeholderAction.id,
                            path: 'child',
                            value: state.getBlock(id),
                        },
                    });
                }
                if (sw.widget !== 'iteration') {
                    state.dispatch({
                        message: ActionMessages.MOVE_BLOCK,
                        payload: {
                            id,
                            position: {
                                parent: placeholderAction.id,
                                slot: placeholderAction.slot,
                            },
                        },
                    });
                }
            }
        });

    @CodiumAI-Agent
    Copy link

    PR Code Suggestions ✨

    CategorySuggestion                                                                                                                                    Impact
    Possible issue
    Use correct block reference

    When iterating multiple selected IDs, each block’s own reference should be used
    instead of always using placeholderAction.id. Update the lookup to use lastSiblingId
    (or id as appropriate) so that the correct block is fetched for movement
    calculations.

    packages/client/src/components/designer/SelectedMask.tsx [155-156]

     designer.multiselectedIds.forEach((id) => {
    -    const sw = state.getBlock(placeholderAction.id);
    +    const sw = state.getBlock(lastSiblingId);
    Suggestion importance[1-10]: 8

    __

    Why: The loop incorrectly always fetches the same block via placeholderAction.id; using lastSiblingId fixes subsequent sibling lookups and ensures correct move calculations.

    Medium
    General
    Show alias per block

    The variableName only applies to a single selected block and will be empty in
    multi-select mode. Retrieve the alias per block inside the map, e.g.
    state.getAlias(id), so each mask shows its correct label.

    packages/client/src/components/designer/SelectedMask.tsx [386-388]

     <Typography variant={'body2'}>
    -    {variableName ? variableName : id}
    +    {state.getAlias(id) || id}
     </Typography>
    Suggestion importance[1-10]: 7

    __

    Why: In multi-select mode, variableName remains the alias of the last single selection; using state.getAlias(id) ensures each mask displays the correct label.

    Medium
    Include initial selected block

    When starting a multi-selection, the original selected block should also be
    included. If multiselectedIds is empty, first push the current designer.selected
    before adding the new id. This ensures the first and subsequent blocks are grouped.

    packages/client/src/components/designer/Screen.tsx [120-123]

    +if (designer.multiselectedIds.length === 0 && designer.selected) {
    +    designer.setMultiSelectedIds(designer.selected);
    +}
     designer.setSelected(id);
     designer.setMultiSelectedIds(id);
     if (designer.multiselectedIds.length > 1) {
         designer.setSelected('');
     }
    Suggestion importance[1-10]: 7

    __

    Why: Without adding the original selected to multiselectedIds, the first block is never grouped, so pushing it when the array is empty fixes complete multi-selection behavior.

    Medium
    Avoid mutating ID array

    Mutating the array in place can break reactivity and allow duplicates. Replace push
    with array spreading to create a new array on each update.

    packages/client/src/stores/designer/designer.store.ts [151-154]

     if (id === 'clear') {
         this._store.multiselectedIds = [];
     } else {
    -    this._store.multiselectedIds.push(id);
    +    this._store.multiselectedIds = [...this._store.multiselectedIds, id];
     }
    Suggestion importance[1-10]: 5

    __

    Why: Replacing in-place push with array spreading creates a new array reference for better reactivity; however, it doesn’t inherently prevent duplicate IDs.

    Low

    pallabi2303 and others added 8 commits May 22, 2025 13:30
    … page of pagination
    @NaveenRamasamy028 NaveenRamasamy028 linked an issue Jun 12, 2025 that may be closed by this pull request
    2 tasks
    Copy link
    Contributor

    @johbaxter johbaxter left a comment

    Choose a reason for hiding this comment

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

    Nice feature @NaveenRamasamy028

    @johbaxter johbaxter merged commit 74d9c08 into dev Jun 13, 2025
    3 checks passed
    @johbaxter johbaxter deleted the feat/MultipleBlockSelection branch June 13, 2025 22:08
    Copy link

    @CodiumAI-Agent /update_changelog

    @CodiumAI-Agent
    Copy link

    Changelog updates: 🔄

    2025-06-13 *

    Added

    • ability to select and drag multiple blocks in the app builder canvas
    • updated panel and mask UI to reflect multi-block selection states

    to commit the new content to the CHANGELOG.md file, please type:
    '/update_changelog --pr_update_changelog.push_changelog_changes=true'

    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.

    [Enhancement] Keyboard shortcut to delete blocks
    6 participants