feat: add copy paste feature for folder#6097
Conversation
packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
Outdated
Show resolved
Hide resolved
WalkthroughAdds 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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (6)
tests/request/copy-request/copy-folder.spec.ts (2)
9-91: Consider usingtest.stepfor better test reporting.Breaking down each test into logical steps using
test.stepwould 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
beforeAllorbeforeEachhook, 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 withtest.stepfor improved report readability.Per coding guidelines, using
test.stepmakes 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 byfocus()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
📒 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 existingexpect()playwright callsAvoid using
page.pause()in codeUse locator variables for locators
Avoid using test.only
Use multiple assertions
Promote the use of
test.stepas much as possible so the generated reports are easier to readEnsure that the
fixtureslike the collections are nested inside thefixturesfolderFixture 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.tstests/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-focusedclass provides clear visual feedback for keyboard navigation, using the theme propertykeyboardFocusBgand ensuring consistent hover behavior.packages/bruno-app/src/themes/light.js (1)
121-121: LGTM! Theme property addition.The
keyboardFocusBgproperty 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
reqVarsandresVarsto arrays prevents potential runtime errors when these values are unexpectedly non-array types, and simplifies theisResVarlogic.packages/bruno-electron/src/ipc/collection.js (2)
776-784: LGTM! Collection context added to delete operation.The additional
collectionPathnameparameter 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
generateUniqueNamefunction 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.pathnameargument enables format-aware folder cloning operations.
998-1036: LGTM! Comprehensive folder paste support.The
pasteItemfunction now properly handles both folder and request pasting:
- Uses
generateUniqueNameto avoid naming conflicts- Sets appropriate sequence numbers with safe null handling
- Invokes the correct IPC methods for each item type
- Queues
OPEN_REQUESTtasks for pasted requestspackages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js (1)
98-105: LGTM! Collection-level keyboard focus styling.The
collection-keyboard-focusedclass 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
keyboardFocusBgaddition 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 withtabIndex={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.
packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js
Show resolved
Hide resolved
packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js
Show resolved
Hide resolved
There was a problem hiding this comment.
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 utilityThe
isMac+isModifierPressedlogic inhandleKeyDownis (or was) also present inCollection/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/handlePasteItemare now called both from the dropdown and via keyboard shortcuts, but they unconditionally calldropdownTippyRef.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
📒 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
isKeyboardFocusedplus the conditionalitem-focused-in-tab/item-keyboard-focusedclasses, together withtabIndex={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
Description
Features Added
Cmd+Cto copy,Cmd+Vto pasteCtrl+Cto copy,Ctrl+Vto pasteJIRA: BRU-1923, BRU-2164
Contribution Checklist:
Screen.Recording.2025-11-14.at.12.53.04.PM.mov
Summary by CodeRabbit
New Features
UX / Bug Fixes
Style
Tests
✏️ Tip: You can customize this high-level summary in your review settings.