From 89a4cd62bc621c61ff2d2779749c229881ad2597 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <161328623+sanjaikumar-bruno@users.noreply.github.com> Date: Mon, 10 Feb 2025 20:46:42 +0530 Subject: [PATCH 01/11] Feat: Move-Collection with Drag-and-Drop (#3755) --------- Co-authored-by: sanjai0py Co-authored-by: ramki-bruno --- .../Collection/CollectionItem/index.js | 22 +++++---- .../Collections/Collection/StyledWrapper.js | 11 +++++ .../Sidebar/Collections/Collection/index.js | 48 +++++++++++++++---- .../components/Sidebar/Collections/index.js | 8 +--- packages/bruno-app/src/index.js | 6 ++- .../ReduxStore/slices/collections/actions.js | 17 +++++++ .../ReduxStore/slices/collections/index.js | 11 ++++- .../bruno-app/src/utils/collections/index.js | 11 +---- packages/bruno-electron/src/ipc/collection.js | 4 ++ .../src/store/last-opened-collections.js | 18 +++---- 10 files changed, 110 insertions(+), 46 deletions(-) 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 3e426eb7ff..a08fac3b7d 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 b8e0d21fd7..5c06cc42ab 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 3fe00c686e..a185474065 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 0e5187ebe3..36b1d0bc6e 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 7de849eea2..c2532b3fd3 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 3ae0fa4e5a..3fe805aed8 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 eb53cfb48e..e119553e46 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 89138f0906..27a34d8618 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 546b73b57e..72452eef34 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', []); } } From 8810b9e2918ed569da16fc6b84f7433e832b815a Mon Sep 17 00:00:00 2001 From: lohxt1 Date: Mon, 10 Feb 2025 21:03:51 +0530 Subject: [PATCH 02/11] fix graphql variables editor and tests editor height --- package-lock.json | 13 +++++++++- .../RequestPane/GraphQLRequestPane/index.js | 2 +- .../RequestPane/GraphQLVariables/index.js | 4 +-- .../RequestPane/Tests/StyledWrapper.js | 10 -------- .../src/components/RequestPane/Tests/index.js | 25 ++++++++----------- 5 files changed, 26 insertions(+), 28 deletions(-) delete mode 100644 packages/bruno-app/src/components/RequestPane/Tests/StyledWrapper.js diff --git a/package-lock.json b/package-lock.json index a3af1292fc..42cf6a3436 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -786,6 +787,7 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", @@ -816,6 +818,7 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -833,6 +836,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/@babel/generator": { @@ -1112,6 +1116,7 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.25.9", @@ -7078,6 +7083,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, "license": "MIT" }, "node_modules/@types/lodash": { @@ -7090,6 +7096,7 @@ "version": "12.2.3", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/linkify-it": "*", @@ -7100,6 +7107,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, "license": "MIT" }, "node_modules/@types/ms": { @@ -10329,6 +10337,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, "license": "MIT" }, "node_modules/cookie": { @@ -11930,6 +11939,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -12963,6 +12973,7 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -23534,7 +23545,7 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js index 187a91a682..07dcf14192 100644 --- a/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js +++ b/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js @@ -154,7 +154,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
-
{getTabPanel(focusedTab.requestPaneTab)}
+
{getTabPanel(focusedTab.requestPaneTab)}
); }; diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js b/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js index 91fea01344..eaac6f204f 100644 --- a/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js +++ b/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js @@ -49,7 +49,7 @@ const GraphQLVariables = ({ variables, item, collection }) => { const onSave = () => dispatch(saveRequest(item.uid, collection.uid)); return ( - + <>