Skip to content

feat: add copy paste feature for folder#6097

Merged
sid-bruno merged 9 commits intousebruno:mainfrom
pooja-bruno:feat/add-copy-paste-for-folder
Dec 2, 2025
Merged

feat: add copy paste feature for folder#6097
sid-bruno merged 9 commits intousebruno:mainfrom
pooja-bruno:feat/add-copy-paste-for-folder

Conversation

@pooja-bruno
Copy link
Collaborator

@pooja-bruno pooja-bruno commented Nov 14, 2025

Description

Features Added

  • Copy/Paste Support: Users can now copy and paste folders and requests within collections
  • Keyboard Shortcuts:
    • Mac: Cmd+C to copy, Cmd+V to paste
    • Windows/Linux: Ctrl+C to copy, Ctrl+V to paste

JIRA: BRU-1923, BRU-2164

Contribution Checklist:

  • The pull request only addresses one issue or adds one feature.
  • The pull request does not introduce any breaking changes
  • I have added screenshots or gifs to help explain the change if applicable.
  • I have read the contribution guidelines.
  • Create an issue and link to the pull request.
Screen.Recording.2025-11-14.at.12.53.04.PM.mov

Summary by CodeRabbit

  • New Features

    • Cmd/Ctrl+C and Cmd/Ctrl+V keyboard shortcuts for copy/paste
    • Copy/paste now supports folders and preserves nested content
    • Improved duplicate naming on paste/cloning
  • UX / Bug Fixes

    • Clearer success/error toasts for copy/paste actions
    • Paste into non-folder now shows an error
  • Style

    • Visible keyboard-focus styling in the sidebar
  • Tests

    • Added keyboard shortcut and folder copy/paste tests

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 1, 2025

Walkthrough

Adds keyboard accessibility to collection and item rows with Cmd/Ctrl+C/V handling and focus styling; extends paste/clone flows to support folders with unique-name generation; updates IPC handlers to accept collection context and to use collection-format-aware file extensions; theme values and tests updated accordingly.

Changes

Cohort / File(s) Summary
Keyboard focus styling
packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js, packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js
Adds CSS classes (item-keyboard-focused, collection-keyboard-focused) that apply theme.sidebar.collection.item.keyboardFocusBg and remove outline; enforces same background on hover.
Collection component (keyboard & paste)
packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js
Adds isKeyboardFocused state, focus/blur and keydown handlers (Cmd/Ctrl+V paste), applies tabIndex and keyboard classes, renames/uses handlePasteItem, updates toast messaging and paste flow to support folder-aware logic.
Collection item component (keyboard & copy/paste)
packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js
Adds isKeyboardFocused state, focus/blur and keydown handlers (Cmd/Ctrl+C, Cmd/Ctrl+V with platform detection), sets tabIndex, introduces handleCopyItem/handlePasteItem, restricts paste target to folders and updates toasts.
Theme additions
packages/bruno-app/src/themes/dark.js, packages/bruno-app/src/themes/light.js
Adds keyboardFocusBg: 'rgba(10, 132, 255, 0.2)' under sidebar.collection.item for both themes.
Redux paste/clone logic
packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
Adds generateUniqueName helper to derive conflict-free names/filenames; extends pasteItem to handle folder cloning (uses unique names), makes IPC calls include collection.pathname, and updates delete/clone invocations.
IPC collection handler
packages/bruno-electron/src/ipc/collection.js
renderer:delete-item handler signature extended to accept collectionPathname; clone-folder path traversal now uses collection format to pick .yml vs .bru extensions for cloned request files.
YML variable handling
packages/bruno-filestore/src/formats/yml/common/variables.ts
Replaces implicit merging with explicit array guards (reqVarsArray, resVarsArray) and recomputes allVars to improve type-safety and distinction logic.
Tests: copy/paste & keyboard shortcuts
tests/request/copy-request/copy-folder.spec.ts, tests/request/copy-request/copy-request.spec.ts, tests/request/copy-request/keyboard-shortcuts.spec.ts
Adds tests covering copying/pasting folders and keyboard shortcuts (Cmd/Ctrl+C/V); removes an unnecessary 2s wait in an existing paste test.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as App UI (Collection / Item)
    participant Handlers as Component Handlers
    participant Redux as Redux Actions/Sagas
    participant IPC as IPC (Main)
    participant FS as File System

    User->>UI: Focus item / press Cmd/Ctrl+C
    UI->>Handlers: onKeyDown -> detect Copy
    Handlers->>Redux: dispatch(copyItem)
    Redux-->>Handlers: copiedItem metadata (clipboard)

    User->>UI: Focus target / press Cmd/Ctrl+V
    UI->>Handlers: onKeyDown -> detect Paste
    Handlers->>Redux: dispatch(pasteItem with target)
    Redux->>Redux: generateUniqueName(copiedItem, target)
    alt copiedItem.type == folder
        Redux->>IPC: invoke('renderer:clone-folder', item, destPath, collectionPath)
        IPC->>FS: recursively copy folder (use target collection format -> .yml/.bru)
    else copiedItem.type == request
        Redux->>IPC: invoke('renderer:new-request', itemData, destPath, collectionPath)
        IPC->>FS: write request file (use resolved filename)
    end
    IPC-->>Redux: success / new item metadata
    Redux-->>Handlers: paste result
    Handlers->>UI: show toast ("Item pasted successfully") and update UI
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

  • Focus areas requiring extra attention:
    • generateUniqueName correctness (suffix stripping, filename vs name handling, edge cases)
    • IPC signature change for renderer:delete-item — ensure all callers updated
    • Clone-folder logic for extension selection based on collection format
    • Keyboard event handling: platform modifier detection and preventing accidental text-input capture
    • Tests for folder copy/paste and keyboard shortcuts (flaky timing or environment-specific key modifiers)

Poem

⌨️ Keys hum softly, focus finds its place,
Copy, paste, and folders join the race,
Unique names blossom where collisions lay,
IPC and theme align to light the way,
A small UX win — accessibility plays.

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 directly matches the main feature: copy-paste functionality for folders with keyboard shortcuts, which is comprehensively covered in the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

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

🧹 Nitpick comments (6)
tests/request/copy-request/copy-folder.spec.ts (2)

9-91: Consider using test.step for better test reporting.

Breaking down each test into logical steps using test.step would improve readability in generated reports. For example:

test('should copy and paste a folder within the same collection', async ({ page, createTmpDir }) => {
  await test.step('Create collection and folder', async () => {
    await createCollection(page, 'test-collection', await createTmpDir('test-collection'), { openWithSandboxMode: 'safe' });
    // ... folder creation logic
  });
  
  await test.step('Add request to folder', async () => {
    // ... request creation logic
  });
  
  await test.step('Copy and paste folder', async () => {
    // ... copy/paste logic
  });
});

As per coding guidelines, test.step usage is promoted for clearer test reports.


9-91: Consider extracting more locators into variables.

While some locators are already stored in variables (e.g., collection, folder), frequently used locators like dropdown items and buttons could be extracted for better maintainability:

const newFolderMenuItem = page.locator('.dropdown-item').filter({ hasText: 'New Folder' });
const copyMenuItem = page.locator('.dropdown-item').filter({ hasText: 'Copy' });
const pasteMenuItem = page.locator('.dropdown-item').filter({ hasText: 'Paste' });
tests/request/copy-request/keyboard-shortcuts.spec.ts (3)

4-7: Test isolation concern: second test depends on first test's state.

The folder test (line 53) relies on the collection created by the request test. If the first test fails or is skipped, the second will fail. Consider creating the collection in a beforeAll or beforeEach hook, or making each test independent.

 test.describe('Copy and Paste with Keyboard Shortcuts', () => {
+  test.beforeAll(async ({ page, createTmpDir }) => {
+    await createCollection(page, 'keyboard-test', await createTmpDir('keyboard-test'), { openWithSandboxMode: 'safe' });
+  });
+
   test.afterAll(async ({ page }) => {
     await closeAllCollections(page);
   });

9-51: Consider wrapping logical steps with test.step for improved report readability.

Per coding guidelines, using test.step makes generated reports easier to read. This test has clear logical phases that would benefit from this pattern.

   test('should copy and paste request using keyboard shortcuts', async ({ page, createTmpDir }) => {
-    await createCollection(page, 'keyboard-test', await createTmpDir('keyboard-test'), { openWithSandboxMode: 'safe' });
-    const collection = page.locator('.collection-name').filter({ hasText: 'keyboard-test' });
-
-    // Create a request
-    await collection.locator('.collection-actions').hover();
-    ...
+    await test.step('Create collection and request', async () => {
+      await createCollection(page, 'keyboard-test', await createTmpDir('keyboard-test'), { openWithSandboxMode: 'safe' });
+      const collection = page.locator('.collection-name').filter({ hasText: 'keyboard-test' });
+      await collection.locator('.collection-actions').hover();
+      // ... request creation logic
+    });
+
+    await test.step('Copy request using keyboard shortcut', async () => {
+      // ... copy logic
+    });
+
+    await test.step('Paste request using keyboard shortcut', async () => {
+      // ... paste logic
+    });

25-30: Redundant .click() before .focus()click() already focuses the element.

Calling click() followed by focus() is redundant since clicking an element naturally focuses it. This pattern is repeated for both request and folder tests.

     // Focus the request item
     await requestItem.click();
-    await requestItem.focus();
packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js (1)

610-618: Consider using a distinct icon for Copy vs Clone.

Both "Clone" (line 606) and "Copy" (line 615) use IconCopy. This may confuse users about the difference between the two actions.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dd72ee5 and b74e13a.

📒 Files selected for processing (12)
  • packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js (1 hunks)
  • packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js (5 hunks)
  • packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js (1 hunks)
  • packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js (5 hunks)
  • packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js (4 hunks)
  • packages/bruno-app/src/themes/dark.js (1 hunks)
  • packages/bruno-app/src/themes/light.js (1 hunks)
  • packages/bruno-electron/src/ipc/collection.js (2 hunks)
  • packages/bruno-filestore/src/formats/yml/common/variables.ts (1 hunks)
  • tests/request/copy-request/copy-folder.spec.ts (1 hunks)
  • tests/request/copy-request/copy-request.spec.ts (0 hunks)
  • tests/request/copy-request/keyboard-shortcuts.spec.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • tests/request/copy-request/copy-request.spec.ts
🧰 Additional context used
📓 Path-based instructions (1)
tests/**/**.*

⚙️ CodeRabbit configuration file

tests/**/**.*: Review the following e2e test code written using the Playwright test library. Ensure that:

  • Follow best practices for Playwright code and e2e automation

  • Try to reduce usage of page.waitForTimeout(); in code unless absolutely necessary and the locator cannot be found using existing expect() playwright calls

  • Avoid using page.pause() in code

  • Use locator variables for locators

  • Avoid using test.only

  • Use multiple assertions

  • Promote the use of test.step as much as possible so the generated reports are easier to read

  • Ensure that the fixtures like the collections are nested inside the fixtures folder

    Fixture Example*: Here's an example of possible fixture and test pair

    .
    ├── fixtures
    │   └── collection
    │       ├── base.bru
    │       ├── bruno.json
    │       ├── collection.bru
    │       ├── ws-test-request-with-headers.bru
    │       ├── ws-test-request-with-subproto.bru
    │       └── ws-test-request.bru
    ├── connection.spec.ts # <- Depends on the collection in ./fixtures/collection
    ├── headers.spec.ts
    ├── persistence.spec.ts
    ├── variable-interpolation
    │   ├── fixtures
    │   │   └── collection
    │   │       ├── environments
    │   │       ├── bruno.json
    │   │       └── ws-interpolation-test.bru
    │   ├── init-user-data
    │   └── variable-interpolation.spec.ts # <- Depends on the collection in ./variable-interpolation/fixtures/collection
    └── subproto.spec.ts
    

Files:

  • tests/request/copy-request/keyboard-shortcuts.spec.ts
  • tests/request/copy-request/copy-folder.spec.ts
🧬 Code graph analysis (5)
packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js (5)
packages/bruno-app/src/utils/common/regex.js (3)
  • sanitizeName (9-15)
  • sanitizeName (9-15)
  • i (45-45)
packages/bruno-electron/src/ipc/network/index.js (2)
  • collectionPath (345-345)
  • path (3-3)
packages/bruno-app/src/utils/tabs/index.js (2)
  • isItemAFolder (7-9)
  • isItemAFolder (7-9)
packages/bruno-app/src/utils/common/ipc.js (1)
  • window (8-8)
packages/bruno-app/src/utils/common/platform.js (2)
  • resolveRequestFilename (13-15)
  • resolveRequestFilename (13-15)
tests/request/copy-request/keyboard-shortcuts.spec.ts (2)
tests/utils/page/actions.ts (1)
  • closeAllCollections (130-130)
packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js (2)
  • createCollection (2258-2267)
  • createCollection (2258-2267)
tests/request/copy-request/copy-folder.spec.ts (2)
tests/utils/page/actions.ts (1)
  • closeAllCollections (130-130)
packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js (1)
  • collection (63-63)
packages/bruno-electron/src/ipc/collection.js (1)
packages/bruno-electron/src/utils/filesystem.js (2)
  • searchForRequestFiles (172-181)
  • format (173-173)
packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js (3)
packages/bruno-app/src/selectors/tab.js (2)
  • isTabForItemActive (3-5)
  • isTabForItemActive (3-5)
packages/bruno-app/src/providers/ReduxStore/slices/app.js (2)
  • copyRequest (191-195)
  • copyRequest (191-195)
packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js (2)
  • pasteItem (960-1044)
  • pasteItem (960-1044)
⏰ 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). (6)
  • GitHub Check: Playwright E2E Tests
  • GitHub Check: Unit Tests
  • GitHub Check: CLI Tests
  • GitHub Check: SSL Tests - Windows
  • GitHub Check: SSL Tests - Linux
  • GitHub Check: SSL Tests - macOS
🔇 Additional comments (15)
packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js (1)

134-141: LGTM! Keyboard focus styling enhancement.

The new item-keyboard-focused class provides clear visual feedback for keyboard navigation, using the theme property keyboardFocusBg and ensuring consistent hover behavior.

packages/bruno-app/src/themes/light.js (1)

121-121: LGTM! Theme property addition.

The keyboardFocusBg property provides a semi-transparent blue background for keyboard-focused collection items, maintaining visual consistency with the accessibility enhancements.

packages/bruno-filestore/src/formats/yml/common/variables.ts (1)

12-22: LGTM! Defensive array handling.

The explicit normalization of reqVars and resVars to arrays prevents potential runtime errors when these values are unexpectedly non-array types, and simplifies the isResVar logic.

packages/bruno-electron/src/ipc/collection.js (2)

776-784: LGTM! Collection context added to delete operation.

The additional collectionPathname parameter ensures that file format detection uses the correct collection context when searching for request files to delete.


950-954: LGTM! Format-aware file extension handling.

The logic correctly updates request file extensions during folder cloning to match the target collection's format, enabling cross-format cloning operations.

packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js (3)

84-118: LGTM! Centralized unique name generation.

The generateUniqueName function provides a clean, centralized approach to generating conflict-free names for both folders and requests during paste/clone operations. The logic correctly:

  • Strips existing numeric suffixes to find the base name
  • Filters items by type (folder vs request)
  • Removes file extensions for requests
  • Finds the highest counter and increments it

890-890: LGTM! Collection context added to folder cloning.

The additional collection.pathname argument enables format-aware folder cloning operations.


998-1036: LGTM! Comprehensive folder paste support.

The pasteItem function now properly handles both folder and request pasting:

  • Uses generateUniqueName to avoid naming conflicts
  • Sets appropriate sequence numbers with safe null handling
  • Invokes the correct IPC methods for each item type
  • Queues OPEN_REQUEST tasks for pasted requests
packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js (1)

98-105: LGTM! Collection-level keyboard focus styling.

The collection-keyboard-focused class mirrors the keyboard focus styling pattern used for collection items, providing consistent visual feedback for keyboard navigation at the collection level.

packages/bruno-app/src/themes/dark.js (1)

118-118: LGTM! Dark theme keyboard focus property.

The keyboardFocusBg addition maintains theme consistency with the light theme, using the same semi-transparent blue for keyboard focus indication.

packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js (2)

251-256: LGTM on the className conditional logic.

The keyboard focus styling is correctly separated from the tab-focus styling, ensuring proper visual feedback for different interaction modes.


288-291: Good accessibility addition with tabIndex={0} and keyboard handlers.

This enables keyboard navigation and interaction on the collection row, improving accessibility.

packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js (3)

405-421: Good defensive validation before paste operation.

The early return with user feedback when attempting to paste into a non-folder is a solid UX pattern.


398-403: LGTM on the item-type-aware toast message.

Dynamically showing "Folder" or "Request" in the toast provides clear feedback to the user.


487-490: Good accessibility additions matching the Collection component.

Consistent keyboard navigation support across collection items.

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

♻️ Duplicate comments (1)
packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js (1)

423-438: Extract Mac/modifier-key detection into a shared keyboard utility

The isMac + isModifierPressed logic in handleKeyDown is (or was) also present in Collection/index.js. To avoid duplication and keep platform detection consistent, it’s worth moving this into a small shared helper (e.g., utils/keyboard.js) and using it here:

// utils/keyboard.js
export const isMacPlatform = () =>
  navigator.userAgent?.includes('Mac') || navigator.platform?.startsWith('Mac');

export const isModifierKeyPressed = (e) =>
  isMacPlatform() ? e.metaKey : e.ctrlKey;

Then:

-  const handleKeyDown = (e) => {
-    const isMac = navigator.userAgent?.includes('Mac') || navigator.platform?.startsWith('Mac');
-    const isModifierPressed = isMac ? e.metaKey : e.ctrlKey;
+  const handleKeyDown = (e) => {
+    const isModifierPressed = isModifierKeyPressed(e);

This keeps the keyboard shortcut logic focused on intent (copy/paste) and centralises the platform heuristics.

🧹 Nitpick comments (1)
packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js (1)

398-421: Make dropdown ref usage defensive for keyboard-triggered copy/paste

handleCopyItem / handlePasteItem are now called both from the dropdown and via keyboard shortcuts, but they unconditionally call dropdownTippyRef.current.hide(). In keyboard flows, there’s a risk that the ref isn’t initialised yet, which would throw.

Consider guarding the ref and (optionally) aligning paste behavior with hasCopiedItems:

 const handleCopyItem = () => {
-  dropdownTippyRef.current.hide();
+  dropdownTippyRef.current?.hide();
   dispatch(copyRequest(item));
   const itemType = isFolder ? 'Folder' : 'Request';
   toast.success(`${itemType} copied to clipboard`);
 };

 const handlePasteItem = () => {
-  dropdownTippyRef.current.hide();
+  dropdownTippyRef.current?.hide();
 
   // Only allow paste into folders
   if (!isFolder) {
     toast.error('Paste is only available for folders');
     return;
   }

+  // Optional: mirror menu gating and avoid backend "No item in clipboard" errors
+  // if (!hasCopiedItems) {
+  //   return;
+  // }
+
   dispatch(pasteItem(collectionUid, item.uid))
     .then(() => {
       toast.success('Item pasted successfully');
     })
     .catch((err) => {
       toast.error(err ? err.message : 'An error occurred while pasting the item');
     });
 };

This keeps dropdown behavior unchanged while making the handlers safe for shortcut usage.

Also applies to: 610-618, 622-622

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b74e13a and 7d2752e.

📒 Files selected for processing (2)
  • packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js (5 hunks)
  • packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js
🧰 Additional context used
🧬 Code graph analysis (1)
packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js (2)
packages/bruno-app/src/providers/ReduxStore/slices/app.js (2)
  • copyRequest (191-195)
  • copyRequest (191-195)
packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js (2)
  • pasteItem (960-1044)
  • pasteItem (960-1044)
⏰ 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). (6)
  • GitHub Check: SSL Tests - Windows
  • GitHub Check: Playwright E2E Tests
  • GitHub Check: SSL Tests - Linux
  • GitHub Check: Unit Tests
  • GitHub Check: CLI Tests
  • GitHub Check: SSL Tests - macOS
🔇 Additional comments (1)
packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js (1)

80-80: Keyboard focus state and row wiring look solid

isKeyboardFocused plus the conditional item-focused-in-tab / item-keyboard-focused classes, together with tabIndex={0} and the focus/blur handlers, give a clear and predictable focus model for the row. This should work well with the new keyboard shortcuts and avoids double-highlighting when the tab is active.

Also applies to: 189-195, 440-446, 487-490

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants