diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js index 3e426eb7f..a08fac3b7 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js @@ -32,6 +32,7 @@ const CollectionItem = ({ item, collection, searchText }) => { const activeTabUid = useSelector((state) => state.tabs.activeTabUid); const isSidebarDragging = useSelector((state) => state.app.isDragging); const dispatch = useDispatch(); + const collectionItemRef = useRef(null); const [renameItemModalOpen, setRenameItemModalOpen] = useState(false); const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false); @@ -45,28 +46,31 @@ const CollectionItem = ({ item, collection, searchText }) => { const itemIsCollapsed = hasSearchText ? false : item.collapsed; const [{ isDragging }, drag] = useDrag({ - type: `COLLECTION_ITEM_${collection.uid}`, + type: `collection-item-${collection.uid}`, item: item, collect: (monitor) => ({ isDragging: monitor.isDragging() - }) + }), + options: { + dropEffect: "move" + } }); const [{ isOver }, drop] = useDrop({ - accept: `COLLECTION_ITEM_${collection.uid}`, + accept: `collection-item-${collection.uid}`, drop: (draggedItem) => { - if (draggedItem.uid !== item.uid) { - dispatch(moveItem(collection.uid, draggedItem.uid, item.uid)); - } + dispatch(moveItem(collection.uid, draggedItem.uid, item.uid)); }, canDrop: (draggedItem) => { return draggedItem.uid !== item.uid; }, collect: (monitor) => ({ - isOver: monitor.isOver() - }) + isOver: monitor.isOver(), + }), }); + drag(drop(collectionItemRef)); + const dropdownTippyRef = useRef(); const MenuIcon = forwardRef((props, ref) => { return ( @@ -255,7 +259,7 @@ const CollectionItem = ({ item, collection, searchText }) => { {generateCodeItemModalOpen && ( setGenerateCodeItemModalOpen(false)} /> )} -
drag(drop(node))}> +
{indents && indents.length ? indents.map((i) => { diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js index b8e0d21fd..5c06cc42a 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js @@ -12,6 +12,17 @@ const Wrapper = styled.div` transform: rotateZ(90deg); } + &.item-hovered { + background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; + .collection-actions { + .dropdown { + div[aria-expanded='false'] { + visibility: visible; + } + } + } + } + .collection-actions { .dropdown { div[aria-expanded='true'] { diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js index 3fe00c686..a18547406 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -2,11 +2,11 @@ import React, { useState, forwardRef, useRef, useEffect } from 'react'; import classnames from 'classnames'; import { uuid } from 'utils/common'; import filter from 'lodash/filter'; -import { useDrop } from 'react-dnd'; +import { useDrop, useDrag } from 'react-dnd'; import { IconChevronRight, IconDots, IconLoader2 } from '@tabler/icons'; import Dropdown from 'components/Dropdown'; import { collapseCollection } from 'providers/ReduxStore/slices/collections'; -import { mountCollection, moveItemToRootOfCollection } from 'providers/ReduxStore/slices/collections/actions'; +import { mountCollection, moveItemToRootOfCollection, moveCollectionAndPersist } from 'providers/ReduxStore/slices/collections/actions'; import { useDispatch, useSelector } from 'react-redux'; import { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs'; import NewRequest from 'components/Sidebar/NewRequest'; @@ -33,6 +33,7 @@ const Collection = ({ collection, searchText }) => { const tabs = useSelector((state) => state.tabs.tabs); const dispatch = useDispatch(); const isLoading = areItemsLoading(collection); + const collectionRef = useRef(null); const menuDropdownTippyRef = useRef(); const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref); @@ -124,26 +125,51 @@ const Collection = ({ collection, searchText }) => { ); }; + const isCollectionItem = (itemType) => { + return itemType.startsWith('collection-item'); + }; + + const [{ isDragging }, drag] = useDrag({ + type: "collection", + item: collection, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + options: { + dropEffect: "move" + } + }); + const [{ isOver }, drop] = useDrop({ - accept: `COLLECTION_ITEM_${collection.uid}`, - drop: (draggedItem) => { - dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid)); + accept: ["collection", `collection-item-${collection.uid}`], + drop: (draggedItem, monitor) => { + const itemType = monitor.getItemType(); + if (isCollectionItem(itemType)) { + dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid)) + } else { + dispatch(moveCollectionAndPersist({draggedItem, targetItem: collection})); + } }, canDrop: (draggedItem) => { - // todo need to make sure that draggedItem belongs to the collection - return true; + return draggedItem.uid !== collection.uid; }, collect: (monitor) => ({ - isOver: monitor.isOver() - }) + isOver: monitor.isOver(), + }), }); + drag(drop(collectionRef)); + if (searchText && searchText.length) { if (!doesCollectionHaveItemsMatchingSearchText(collection, searchText)) { return null; } } + const collectionRowClassName = classnames('flex py-1 collection-name items-center', { + 'item-hovered': isOver + }); + // we need to sort request items by seq property const sortRequestItems = (items = []) => { return items.sort((a, b) => a.seq - b.seq); @@ -173,7 +199,9 @@ const Collection = ({ collection, searchText }) => { {showCloneCollectionModalOpen && ( setShowCloneCollectionModalOpen(false)} /> )} -
+
{ {collections && collections.length ? collections.map((c) => { return ( - - - + ); }) : null} diff --git a/packages/bruno-app/src/index.js b/packages/bruno-app/src/index.js index 0e5187ebe..36b1d0bc6 100644 --- a/packages/bruno-app/src/index.js +++ b/packages/bruno-app/src/index.js @@ -1,6 +1,8 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './pages/index'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; const rootElement = document.getElementById('root'); @@ -8,7 +10,9 @@ if (rootElement) { const root = ReactDOM.createRoot(rootElement); root.render( - + + + ); } diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index 7de849eea..c2532b3fd 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -32,6 +32,7 @@ import { selectEnvironment as _selectEnvironment, sortCollections as _sortCollections, updateCollectionMountStatus, + moveCollection, requestCancelled, resetRunResults, responseReceived, @@ -1151,6 +1152,22 @@ export const importCollection = (collection, collectionLocation) => (dispatch, g }); }; +export const moveCollectionAndPersist = ({ draggedItem, targetItem }) => (dispatch, getState) => { + dispatch(moveCollection({ draggedItem, targetItem })); + + return new Promise((resolve, reject) => { + const { ipcRenderer } = window; + const state = getState(); + + const collectionPaths = state.collections.collections.map((collection) => collection.pathname); + + ipcRenderer + .invoke('renderer:update-collection-paths', collectionPaths) + .then(resolve) + .catch(reject); + }); +}; + export const saveCollectionSecurityConfig = (collectionUid, securityConfig) => (dispatch, getState) => { return new Promise((resolve, reject) => { const { ipcRenderer } = window; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 3ae0fa4e5..3fe805aed 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -1,5 +1,5 @@ import { uuid } from 'utils/common'; -import { find, map, forOwn, concat, filter, each, cloneDeep, get, set } from 'lodash'; +import { find, map, forOwn, concat, filter, each, cloneDeep, get, set, findIndex } from 'lodash'; import { createSlice } from '@reduxjs/toolkit'; import { addDepth, @@ -100,6 +100,12 @@ export const collectionsSlice = createSlice({ break; } }, + moveCollection: (state, action) => { + const { draggedItem, targetItem } = action.payload; + state.collections = state.collections.filter((i) => i.uid !== draggedItem.uid); // Remove dragged item + const targetItemIndex = state.collections.findIndex((i) => i.uid === targetItem.uid); // Find target item + state.collections.splice(targetItemIndex, 0, draggedItem); // Insert dragged-item above target-item + }, updateLastAction: (state, action) => { const { collectionUid, lastAction } = action.payload; const collection = findCollectionByUid(state.collections, collectionUid); @@ -2082,7 +2088,8 @@ export const { runFolderEvent, resetCollectionRunner, updateRequestDocs, - updateFolderDocs + updateFolderDocs, + moveCollection } = collectionsSlice.actions; export default collectionsSlice.reducer; diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index eb53cfb48..e119553e4 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -1,13 +1,4 @@ -import get from 'lodash/get'; -import each from 'lodash/each'; -import find from 'lodash/find'; -import findIndex from 'lodash/findIndex'; -import isString from 'lodash/isString'; -import map from 'lodash/map'; -import filter from 'lodash/filter'; -import sortBy from 'lodash/sortBy'; -import isEqual from 'lodash/isEqual'; -import cloneDeep from 'lodash/cloneDeep'; +import {cloneDeep, isEqual, sortBy, filter, map, isString, findIndex, find, each, get } from 'lodash'; import { uuid } from 'utils/common'; import path from 'path'; import slash from 'utils/common/slash'; diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 89138f090..27a34d861 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -516,6 +516,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } }); + ipcMain.handle('renderer:update-collection-paths', async (_, collectionPaths) => { + lastOpenedCollections.update(collectionPaths); + }) + ipcMain.handle('renderer:import-collection', async (event, collection, collectionLocation) => { try { let collectionName = sanitizeDirectoryName(collection.name); diff --git a/packages/bruno-electron/src/store/last-opened-collections.js b/packages/bruno-electron/src/store/last-opened-collections.js index 546b73b57..72452eef3 100644 --- a/packages/bruno-electron/src/store/last-opened-collections.js +++ b/packages/bruno-electron/src/store/last-opened-collections.js @@ -16,18 +16,20 @@ class LastOpenedCollections { } add(collectionPath) { - const collections = this.store.get('lastOpenedCollections') || []; + const collections = this.getAll(); - if (isDirectory(collectionPath)) { - if (!collections.includes(collectionPath)) { - collections.push(collectionPath); - this.store.set('lastOpenedCollections', collections); - } + if (isDirectory(collectionPath) && !collections.includes(collectionPath)) { + collections.push(collectionPath); + this.store.set('lastOpenedCollections', collections); } } + update(collectionPaths) { + this.store.set('lastOpenedCollections', collectionPaths); + } + remove(collectionPath) { - let collections = this.store.get('lastOpenedCollections') || []; + let collections = this.getAll(); if (collections.includes(collectionPath)) { collections = _.filter(collections, (c) => c !== collectionPath); @@ -36,7 +38,7 @@ class LastOpenedCollections { } removeAll() { - return this.store.set('lastOpenedCollections', []); + this.store.set('lastOpenedCollections', []); } }