Merge branch 'feature/sort-requests'

This commit is contained in:
Anoop M D 2023-01-10 09:39:13 +05:30
commit 87f6000b85
11 changed files with 267 additions and 28 deletions

View File

@ -1,5 +1,6 @@
{ {
"name": "@usebruno/app", "name": "@usebruno/app",
"version": "0.3.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
@ -15,8 +16,8 @@
"@reduxjs/toolkit": "^1.8.0", "@reduxjs/toolkit": "^1.8.0",
"@tabler/icons": "^1.46.0", "@tabler/icons": "^1.46.0",
"@tippyjs/react": "^4.2.6", "@tippyjs/react": "^4.2.6",
"@usebruno/schema": "0.2.0",
"@usebruno/graphql-docs": "0.1.0", "@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.2.0",
"axios": "^0.26.0", "axios": "^0.26.0",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"codemirror": "^5.65.2", "codemirror": "^5.65.2",
@ -34,16 +35,17 @@
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"next": "12.3.1", "next": "12.3.3",
"path": "^0.12.7", "path": "^0.12.7",
"platform": "^1.3.6", "platform": "^1.3.6",
"posthog-node": "^2.1.0", "posthog-node": "^2.1.0",
"qs": "^6.11.0", "qs": "^6.11.0",
"react": "^17.0.2", "react": "18.2.0",
"react-dom": "^17.0.2", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "18.2.0",
"react-hot-toast": "^2.4.0", "react-hot-toast": "^2.4.0",
"react-redux": "^7.2.6", "react-redux": "^7.2.6",
"react-tabs": "^3.2.3",
"reckonjs": "^0.1.2", "reckonjs": "^0.1.2",
"sass": "^1.46.0", "sass": "^1.46.0",
"split-on-first": "^3.0.0", "split-on-first": "^3.0.0",

View File

@ -70,7 +70,7 @@ const Wrapper = styled.div`
} }
} }
&.is-dragging .collection-item-name { &.is-sidebar-dragging .collection-item-name {
cursor: inherit; cursor: inherit;
} }
`; `;

View File

@ -2,10 +2,12 @@ import React, { useState, useRef, forwardRef, useEffect } from 'react';
import range from 'lodash/range'; import range from 'lodash/range';
import filter from 'lodash/filter'; import filter from 'lodash/filter';
import classnames from 'classnames'; import classnames from 'classnames';
import { useDrag, useDrop } from 'react-dnd';
import { IconChevronRight, IconDots } from '@tabler/icons'; import { IconChevronRight, IconDots } from '@tabler/icons';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs'; import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections'; import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections';
import { moveItem } from 'providers/ReduxStore/slices/collections/actions';
import Dropdown from 'components/Dropdown'; import Dropdown from 'components/Dropdown';
import NewRequest from 'components/Sidebar/NewRequest'; import NewRequest from 'components/Sidebar/NewRequest';
import NewFolder from 'components/Sidebar/NewFolder'; import NewFolder from 'components/Sidebar/NewFolder';
@ -23,7 +25,7 @@ import StyledWrapper from './StyledWrapper';
const CollectionItem = ({ item, collection, searchText }) => { const CollectionItem = ({ item, collection, searchText }) => {
const tabs = useSelector((state) => state.tabs.tabs); const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid); const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const isDragging = useSelector((state) => state.app.isDragging); const isSidebarDragging = useSelector((state) => state.app.isDragging);
const dispatch = useDispatch(); const dispatch = useDispatch();
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false); const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
@ -33,6 +35,29 @@ const CollectionItem = ({ item, collection, searchText }) => {
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false); const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
const [itemIsCollapsed, setItemisCollapsed] = useState(item.collapsed); const [itemIsCollapsed, setItemisCollapsed] = useState(item.collapsed);
const [{ isDragging }, drag] = useDrag({
type: 'COLLECTION_ITEM',
item: item,
collect: (monitor) => ({
isDragging: monitor.isDragging()
})
});
const [{ isOver }, drop] = useDrop({
accept: 'COLLECTION_ITEM',
drop: (draggedItem) => {
if (draggedItem.uid !== item.uid) {
dispatch(moveItem(collection.uid, draggedItem.uid, item.uid));
}
},
canDrop: (draggedItem) => {
return draggedItem.uid !== item.uid;
},
collect: (monitor) => ({
isOver: monitor.isOver()
})
});
useEffect(() => { useEffect(() => {
if (searchText && searchText.length) { if (searchText && searchText.length) {
setItemisCollapsed(false); setItemisCollapsed(false);
@ -91,7 +116,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
const isFolder = isItemAFolder(item); const isFolder = isItemAFolder(item);
const className = classnames('flex flex-col w-full', { const className = classnames('flex flex-col w-full', {
'is-dragging': isDragging 'is-sidebar-dragging': isSidebarDragging
}); });
if (searchText && searchText.length) { if (searchText && searchText.length) {
@ -116,7 +141,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
{deleteItemModalOpen && <DeleteCollectionItem item={item} collection={collection} onClose={() => setDeleteItemModalOpen(false)} />} {deleteItemModalOpen && <DeleteCollectionItem item={item} collection={collection} onClose={() => setDeleteItemModalOpen(false)} />}
{newRequestModalOpen && <NewRequest item={item} collection={collection} onClose={() => setNewRequestModalOpen(false)} />} {newRequestModalOpen && <NewRequest item={item} collection={collection} onClose={() => setNewRequestModalOpen(false)} />}
{newFolderModalOpen && <NewFolder item={item} collection={collection} onClose={() => setNewFolderModalOpen(false)} />} {newFolderModalOpen && <NewFolder item={item} collection={collection} onClose={() => setNewFolderModalOpen(false)} />}
<div className={itemRowClassName}> <div className={itemRowClassName} ref={(node) => drag(drop(node))}>
<div className="flex items-center h-full w-full"> <div className="flex items-center h-full w-full">
{indents && indents.length {indents && indents.length
? indents.map((i) => { ? indents.map((i) => {

View File

@ -2,9 +2,11 @@ import React, { useState, forwardRef, useRef, useEffect } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import filter from 'lodash/filter'; import filter from 'lodash/filter';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import { useDrop } from 'react-dnd';
import { IconChevronRight, IconDots } from '@tabler/icons'; import { IconChevronRight, IconDots } from '@tabler/icons';
import Dropdown from 'components/Dropdown'; import Dropdown from 'components/Dropdown';
import { collectionClicked } from 'providers/ReduxStore/slices/collections'; import { collectionClicked } from 'providers/ReduxStore/slices/collections';
import { moveItemToRootOfCollection } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import NewRequest from 'components/Sidebar/NewRequest'; import NewRequest from 'components/Sidebar/NewRequest';
import NewFolder from 'components/Sidebar/NewFolder'; import NewFolder from 'components/Sidebar/NewFolder';
@ -71,6 +73,21 @@ const Collection = ({ collection, searchText }) => {
const isLocal = isLocalCollection(collection); const isLocal = isLocalCollection(collection);
const [{ isOver }, drop] = useDrop({
accept: 'COLLECTION_ITEM',
drop: (draggedItem) => {
console.log('drop', draggedItem);
dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid));
},
canDrop: (draggedItem) => {
// todo need to make sure that draggedItem belongs to the collection
return true;
},
collect: (monitor) => ({
isOver: monitor.isOver()
})
});
return ( return (
<StyledWrapper className="flex flex-col"> <StyledWrapper className="flex flex-col">
{showNewRequestModal && <NewRequest collection={collection} onClose={() => setShowNewRequestModal(false)} />} {showNewRequestModal && <NewRequest collection={collection} onClose={() => setShowNewRequestModal(false)} />}
@ -79,7 +96,7 @@ const Collection = ({ collection, searchText }) => {
{showRemoveCollectionFromWSModal && <RemoveCollectionFromWorkspace collection={collection} onClose={() => setShowRemoveCollectionFromWSModal(false)} />} {showRemoveCollectionFromWSModal && <RemoveCollectionFromWorkspace collection={collection} onClose={() => setShowRemoveCollectionFromWSModal(false)} />}
{showDeleteCollectionModal && <DeleteCollection collection={collection} onClose={() => setShowDeleteCollectionModal(false)} />} {showDeleteCollectionModal && <DeleteCollection collection={collection} onClose={() => setShowDeleteCollectionModal(false)} />}
{showRemoveLocalCollectionModal && <RemoveLocalCollection collection={collection} onClose={() => setShowRemoveLocalCollectionModal(false)} />} {showRemoveLocalCollectionModal && <RemoveLocalCollection collection={collection} onClose={() => setShowRemoveLocalCollectionModal(false)} />}
<div className="flex py-1 collection-name items-center"> <div className="flex py-1 collection-name items-center" ref={(node) => drop(node)}>
<div className="flex flex-grow items-center" onClick={handleClick}> <div className="flex flex-grow items-center" onClick={handleClick}>
<IconChevronRight size={16} strokeWidth={2} className={iconClassName} style={{ width: 16, color: 'rgb(160 160 160)' }} /> <IconChevronRight size={16} strokeWidth={2} className={iconClassName} style={{ width: 16, color: 'rgb(160 160 160)' }} />
<div className="ml-1" id="sidebar-collection-name">{collection.name}</div> <div className="ml-1" id="sidebar-collection-name">{collection.name}</div>

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import find from 'lodash/find'; import find from 'lodash/find';
import filter from 'lodash/filter'; import filter from 'lodash/filter';
import Collection from './Collection'; import Collection from './Collection';
@ -26,7 +28,11 @@ const Collections = ({ searchText }) => {
<div className="mt-4 flex flex-col"> <div className="mt-4 flex flex-col">
{collectionToDisplay && collectionToDisplay.length {collectionToDisplay && collectionToDisplay.length
? collectionToDisplay.map((c) => { ? collectionToDisplay.map((c) => {
return <Collection searchText={searchText} collection={c} key={c.uid} />; return (
<DndProvider backend={HTML5Backend} key={c.uid}>
<Collection searchText={searchText} collection={c} />;
</DndProvider>
);
}) })
: null} : null}
</div> </div>

View File

@ -21,17 +21,6 @@ const Wrapper = styled.div`
.fw-600 { .fw-600 {
font-weight: 600; font-weight: 600;
} }
.react-tabs {
.react-tabs__tab-list {
padding-left: 1rem;
border-bottom: 1px solid #cfcfcf;
.react-tabs__tab--selected {
border-color: #cfcfcf;
}
}
}
`; `;
export default Wrapper; export default Wrapper;

View File

@ -1,3 +1,4 @@
import { useState, useEffect } from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { AppProvider } from 'providers/App'; import { AppProvider } from 'providers/App';
import { ToastProvider } from 'providers/Toaster'; import { ToastProvider } from 'providers/Toaster';
@ -9,7 +10,6 @@ import ThemeProvider from 'providers/Theme/index';
import '../styles/app.scss'; import '../styles/app.scss';
import '../styles/globals.css'; import '../styles/globals.css';
import 'tailwindcss/dist/tailwind.min.css'; import 'tailwindcss/dist/tailwind.min.css';
import 'react-tabs/style/react-tabs.css';
import 'codemirror/lib/codemirror.css'; import 'codemirror/lib/codemirror.css';
import 'graphiql/graphiql.min.css'; import 'graphiql/graphiql.min.css';
@ -28,6 +28,16 @@ function NoSsr({ children }) {
} }
function MyApp({ Component, pageProps }) { function MyApp({ Component, pageProps }) {
const [domLoaded, setDomLoaded] = useState(false);
useEffect(() => {
setDomLoaded(true);
}, []);
if(!domLoaded) {
return null;
}
return ( return (
<SafeHydrate> <SafeHydrate>
<NoSsr> <NoSsr>

View File

@ -7,6 +7,8 @@ import { uuid } from 'utils/common';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import { import {
findItemInCollection, findItemInCollection,
moveCollectionItem,
moveCollectionItemToRootOfCollection,
findCollectionByUid, findCollectionByUid,
recursivelyGetAllItemUids, recursivelyGetAllItemUids,
transformCollectionToSaveToIdb, transformCollectionToSaveToIdb,
@ -33,6 +35,8 @@ import {
renameItem as _renameItem, renameItem as _renameItem,
cloneItem as _cloneItem, cloneItem as _cloneItem,
deleteItem as _deleteItem, deleteItem as _deleteItem,
moveItem as _moveItem,
moveItemToRootOfCollection as _moveItemToRootOfCollection,
saveRequest as _saveRequest, saveRequest as _saveRequest,
addEnvironment as _addEnvironment, addEnvironment as _addEnvironment,
renameEnvironment as _renameEnvironment, renameEnvironment as _renameEnvironment,
@ -550,6 +554,121 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
}); });
}; };
export const moveItem = (collectionUid, draggedItemUid, targetItemUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
return new Promise((resolve, reject) => {
if (!collection) {
return reject(new Error('Collection not found'));
}
if (isLocalCollection(collection)) {
const draggedItem = findItemInCollection(collection, draggedItemUid);
const targetItem = findItemInCollection(collection, targetItemUid);
if (!draggedItem) {
return reject(new Error('Dragged item not found'));
}
if (!targetItem) {
return reject(new Error('Target item not found'));
}
const { ipcRenderer } = window;
ipcRenderer
.invoke('renderer:move-item', draggedItem.pathname, targetItem.pathname)
.then(() => resolve())
.catch((error) => reject(error));
return;
}
const collectionCopy = cloneDeep(collection);
const draggedItem = findItemInCollection(collectionCopy, draggedItemUid);
const targetItem = findItemInCollection(collectionCopy, targetItemUid);
if (!draggedItem) {
return reject(new Error('Dragged item not found'));
}
if (!targetItem) {
return reject(new Error('Target item not found'));
}
moveCollectionItem(collectionCopy, draggedItem, targetItem);
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => {
dispatch(
_moveItem({
collectionUid: collectionUid,
draggedItemUid: draggedItemUid,
targetItemUid: targetItemUid
})
);
})
.then(() => resolve())
.catch((error) => reject(error));
});
};
export const moveItemToRootOfCollection = (collectionUid, draggedItemUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
return new Promise((resolve, reject) => {
if (!collection) {
return reject(new Error('Collection not found'));
}
if (isLocalCollection(collection)) {
const draggedItem = findItemInCollection(collection, draggedItemUid);
if (!draggedItem) {
return reject(new Error('Dragged item not found'));
}
const { ipcRenderer } = window;
ipcRenderer
.invoke('renderer:move-item-to-root-of-collection', draggedItem.pathname)
.then(() => resolve())
.catch((error) => reject(error));
return;
}
const collectionCopy = cloneDeep(collection);
const draggedItem = findItemInCollection(collectionCopy, draggedItemUid);
if (!draggedItem) {
return reject(new Error('Dragged item not found'));
}
moveCollectionItemToRootOfCollection(collectionCopy, draggedItem);
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => {
dispatch(
_moveItemToRootOfCollection({
collectionUid: collectionUid,
draggedItemUid: draggedItemUid
})
);
})
.then(() => resolve())
.catch((error) => reject(error));
});
};
export const newHttpRequest = (params) => (dispatch, getState) => { export const newHttpRequest = (params) => (dispatch, getState) => {
const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid } = params; const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid } = params;

View File

@ -14,6 +14,7 @@ import {
findEnvironmentInCollection, findEnvironmentInCollection,
findItemInCollectionByPathname, findItemInCollectionByPathname,
addDepth, addDepth,
moveCollectionItem,
collapseCollection, collapseCollection,
deleteItemInCollection, deleteItemInCollection,
isItemARequest isItemARequest
@ -181,6 +182,38 @@ export const collectionsSlice = createSlice({
} }
} }
}, },
moveItem: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const draggedItemUid = action.payload.draggedItemUid;
const targetItemUid = action.payload.targetItemUid;
if (collection) {
const draggedItem = findItemInCollection(collection, draggedItemUid);
const targetItem = findItemInCollection(collection, targetItemUid);
if (!draggedItem || !targetItem) {
return;
}
moveCollectionItem(collection, draggedItem, targetItem);
addDepth(collection.items);
}
},
moveItemToRootOfCollection: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const draggedItemUid = action.payload.draggedItemUid;
if (collection) {
const draggedItem = findItemInCollection(collection, draggedItemUid);
if (!draggedItem) {
return;
}
moveCollectionItemToRootOfCollection(collection, draggedItem);
addDepth(collection.items);
}
},
requestSent: (state, action) => { requestSent: (state, action) => {
const { itemUid, collectionUid, cancelTokenUid } = action.payload; const { itemUid, collectionUid, cancelTokenUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid); const collection = findCollectionByUid(state.collections, collectionUid);
@ -785,6 +818,8 @@ export const {
deleteItem, deleteItem,
renameItem, renameItem,
cloneItem, cloneItem,
moveItem,
moveItemToRootOfCollection,
requestSent, requestSent,
requestCancelled, requestCancelled,
responseReceived, responseReceived,

View File

@ -1,7 +1,7 @@
import reckon from 'reckonjs';
import get from 'lodash/get'; import get from 'lodash/get';
import each from 'lodash/each'; import each from 'lodash/each';
import find from 'lodash/find'; import find from 'lodash/find';
import findIndex from 'lodash/findIndex';
import isString from 'lodash/isString'; import isString from 'lodash/isString';
import map from 'lodash/map'; import map from 'lodash/map';
import filter from 'lodash/filter'; import filter from 'lodash/filter';
@ -122,6 +122,43 @@ export const findEnvironmentInCollection = (collection, envUid) => {
return find(collection.environments, (e) => e.uid === envUid); return find(collection.environments, (e) => e.uid === envUid);
}; };
export const moveCollectionItem = (collection, draggedItem, targetItem) => {
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
if (draggedItemParent) {
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
} else {
collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid);
}
if (targetItem.type === 'folder') {
targetItem.items = targetItem.items || [];
targetItem.items.push(draggedItem);
} else {
let targetItemParent = findParentItemInCollection(collection, targetItem.uid);
if (targetItemParent) {
let targetItemIndex = findIndex(targetItemParent.items, (i) => i.uid === targetItem.uid);
targetItemParent.items.splice(targetItemIndex + 1, 0, draggedItem);
} else {
let targetItemIndex = findIndex(collection.items, (i) => i.uid === targetItem.uid);
collection.items.splice(targetItemIndex + 1, 0, draggedItem);
}
}
};
export const moveCollectionItemToRootOfCollection = (collection, draggedItem) => {
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
if (draggedItemParent) {
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
} else {
collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid);
}
collection.items.push(draggedItem);
};
export const transformCollectionToSaveToIdb = (collection, options = {}) => { export const transformCollectionToSaveToIdb = (collection, options = {}) => {
const copyHeaders = (headers) => { const copyHeaders = (headers) => {
return map(headers, (header) => { return map(headers, (header) => {

View File

@ -19,8 +19,8 @@
"graphql": "^16.6.0", "graphql": "^16.6.0",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"postcss": "^8.4.18", "postcss": "^8.4.18",
"react": "^17.0.2", "react": "18.2.0",
"react-dom": "^17.0.2", "react-dom": "18.2.0",
"rollup": "3.2.5", "rollup": "3.2.5",
"rollup-plugin-dts": "^5.0.0", "rollup-plugin-dts": "^5.0.0",
"rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-peer-deps-external": "^2.2.4",
@ -30,8 +30,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"graphql": "^16.6.0", "graphql": "^16.6.0",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1"
"react": "^17.0.2"
}, },
"overrides": { "overrides": {
"rollup": "3.2.5" "rollup": "3.2.5"