mirror of
https://github.com/usebruno/bruno.git
synced 2025-06-25 14:31:44 +02:00
feat: drag and drop for files and folders
This commit is contained in:
parent
c4abe54c3f
commit
21c9c8b4fb
@ -1,9 +1,10 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
const RequestNotFound = ({ itemUid }) => {
|
const RequestNotFound = ({ itemUid }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const [showErrorMessage, setShowErrorMessage] = useState(false);
|
||||||
|
|
||||||
const closeTab = () => {
|
const closeTab = () => {
|
||||||
dispatch(
|
dispatch(
|
||||||
@ -13,6 +14,20 @@ const RequestNotFound = ({ itemUid }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowErrorMessage(true);
|
||||||
|
}, 300);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// add a delay component in react that shows a loading spinner
|
||||||
|
// and then shows the error message after a delay
|
||||||
|
// this will prevent the error message from flashing on the screen
|
||||||
|
|
||||||
|
if(!showErrorMessage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-6 px-6">
|
<div className="mt-6 px-6">
|
||||||
<div className="p-4 bg-orange-100 border-l-4 border-yellow-500 text-yellow-700 bg-yellow-100 p-4">
|
<div className="p-4 bg-orange-100 border-l-4 border-yellow-500 text-yellow-700 bg-yellow-100 p-4">
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { IconAlertTriangle } from '@tabler/icons';
|
||||||
|
|
||||||
|
const RequestTabNotFound = ({handleCloseClick}) => {
|
||||||
|
const [showErrorMessage, setShowErrorMessage] = useState(false);
|
||||||
|
|
||||||
|
// add a delay component in react that shows a loading spinner
|
||||||
|
// and then shows the error message after a delay
|
||||||
|
// this will prevent the error message from flashing on the screen
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowErrorMessage(true);
|
||||||
|
}, 300);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if(!showErrorMessage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center tab-label pl-2">
|
||||||
|
{showErrorMessage ? (
|
||||||
|
<>
|
||||||
|
<IconAlertTriangle size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||||
|
<span className="ml-1">Not Found</span>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
|
||||||
|
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RequestTabNotFound;
|
@ -4,7 +4,7 @@ import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
|||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { findItemInCollection } from 'utils/collections';
|
import { findItemInCollection } from 'utils/collections';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
import { IconAlertTriangle } from '@tabler/icons';
|
import RequestTabNotFound from './RequestTabNotFound';
|
||||||
|
|
||||||
const RequestTab = ({ tab, collection }) => {
|
const RequestTab = ({ tab, collection }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -61,18 +61,7 @@ const RequestTab = ({ tab, collection }) => {
|
|||||||
if (!item) {
|
if (!item) {
|
||||||
return (
|
return (
|
||||||
<StyledWrapper className="flex items-center justify-between tab-container px-1">
|
<StyledWrapper className="flex items-center justify-between tab-container px-1">
|
||||||
<div className="flex items-center tab-label pl-2">
|
<RequestTabNotFound handleCloseClick={handleCloseClick} />
|
||||||
<IconAlertTriangle size={18} strokeWidth={1.5} className="text-yellow-600" />
|
|
||||||
<span className="ml-1">Not Found</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
|
|
||||||
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -35,28 +35,28 @@ 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({
|
const [{ isDragging }, drag] = useDrag({
|
||||||
// type: 'COLLECTION_ITEM',
|
type: 'COLLECTION_ITEM',
|
||||||
// item: item,
|
item: item,
|
||||||
// collect: (monitor) => ({
|
collect: (monitor) => ({
|
||||||
// isDragging: monitor.isDragging()
|
isDragging: monitor.isDragging()
|
||||||
// })
|
})
|
||||||
// });
|
});
|
||||||
|
|
||||||
// const [{ isOver }, drop] = useDrop({
|
const [{ isOver }, drop] = useDrop({
|
||||||
// accept: 'COLLECTION_ITEM',
|
accept: 'COLLECTION_ITEM',
|
||||||
// drop: (draggedItem) => {
|
drop: (draggedItem) => {
|
||||||
// if (draggedItem.uid !== item.uid) {
|
if (draggedItem.uid !== item.uid) {
|
||||||
// dispatch(moveItem(collection.uid, draggedItem.uid, item.uid));
|
dispatch(moveItem(collection.uid, draggedItem.uid, item.uid));
|
||||||
// }
|
}
|
||||||
// },
|
},
|
||||||
// canDrop: (draggedItem) => {
|
canDrop: (draggedItem) => {
|
||||||
// return draggedItem.uid !== item.uid;
|
return draggedItem.uid !== item.uid;
|
||||||
// },
|
},
|
||||||
// collect: (monitor) => ({
|
collect: (monitor) => ({
|
||||||
// isOver: monitor.isOver()
|
isOver: monitor.isOver()
|
||||||
// })
|
})
|
||||||
// });
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchText && searchText.length) {
|
if (searchText && searchText.length) {
|
||||||
@ -131,8 +131,18 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestItems = filter(item.items, (i) => isItemARequest(i));
|
// we need to sort request items by seq property
|
||||||
const folderItems = filter(item.items, (i) => isItemAFolder(i));
|
const sortRequestItems = (items = []) => {
|
||||||
|
return items.sort((a, b) => a.seq - b.seq);
|
||||||
|
};
|
||||||
|
|
||||||
|
// we need to sort folder items by name alphabetically
|
||||||
|
const sortFolderItems = (items = []) => {
|
||||||
|
return items.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i)));
|
||||||
|
const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i)));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper className={className}>
|
<StyledWrapper className={className}>
|
||||||
@ -141,7 +151,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) => {
|
||||||
@ -239,13 +249,13 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
|||||||
|
|
||||||
{!itemIsCollapsed ? (
|
{!itemIsCollapsed ? (
|
||||||
<div>
|
<div>
|
||||||
{requestItems && requestItems.length
|
{folderItems && folderItems.length
|
||||||
? requestItems.map((i) => {
|
? folderItems.map((i) => {
|
||||||
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
||||||
})
|
})
|
||||||
: null}
|
: null}
|
||||||
{folderItems && folderItems.length
|
{requestItems && requestItems.length
|
||||||
? folderItems.map((i) => {
|
? requestItems.map((i) => {
|
||||||
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
||||||
})
|
})
|
||||||
: null}
|
: null}
|
||||||
|
@ -59,28 +59,37 @@ const Collection = ({ collection, searchText }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestItems = filter(collection.items, (i) => isItemARequest(i));
|
|
||||||
const folderItems = filter(collection.items, (i) => isItemAFolder(i));
|
|
||||||
|
|
||||||
const handleExportClick = () => {
|
const handleExportClick = () => {
|
||||||
const collectionCopy = cloneDeep(collection);
|
const collectionCopy = cloneDeep(collection);
|
||||||
exportCollection(transformCollectionToSaveToIdb(collectionCopy));
|
exportCollection(transformCollectionToSaveToIdb(collectionCopy));
|
||||||
};
|
};
|
||||||
|
|
||||||
// const [{ isOver }, drop] = useDrop({
|
const [{ isOver }, drop] = useDrop({
|
||||||
// accept: 'COLLECTION_ITEM',
|
accept: 'COLLECTION_ITEM',
|
||||||
// drop: (draggedItem) => {
|
drop: (draggedItem) => {
|
||||||
// console.log('drop', draggedItem);
|
dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid));
|
||||||
// dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid));
|
},
|
||||||
// },
|
canDrop: (draggedItem) => {
|
||||||
// canDrop: (draggedItem) => {
|
// todo need to make sure that draggedItem belongs to the collection
|
||||||
// // todo need to make sure that draggedItem belongs to the collection
|
return true;
|
||||||
// return true;
|
},
|
||||||
// },
|
collect: (monitor) => ({
|
||||||
// collect: (monitor) => ({
|
isOver: monitor.isOver()
|
||||||
// isOver: monitor.isOver()
|
})
|
||||||
// })
|
});
|
||||||
// });
|
|
||||||
|
// we need to sort request items by seq property
|
||||||
|
const sortRequestItems = (items = []) => {
|
||||||
|
return items.sort((a, b) => a.seq - b.seq);
|
||||||
|
};
|
||||||
|
|
||||||
|
// we need to sort folder items by name alphabetically
|
||||||
|
const sortFolderItems = (items = []) => {
|
||||||
|
return items.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestItems = sortRequestItems(filter(collection.items, (i) => isItemARequest(i)));
|
||||||
|
const folderItems = sortFolderItems(filter(collection.items, (i) => isItemAFolder(i)));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper className="flex flex-col">
|
<StyledWrapper className="flex flex-col">
|
||||||
@ -88,7 +97,7 @@ const Collection = ({ collection, searchText }) => {
|
|||||||
{showNewFolderModal && <NewFolder collection={collection} onClose={() => setShowNewFolderModal(false)} />}
|
{showNewFolderModal && <NewFolder collection={collection} onClose={() => setShowNewFolderModal(false)} />}
|
||||||
{showRenameCollectionModal && <RenameCollection collection={collection} onClose={() => setShowRenameCollectionModal(false)} />}
|
{showRenameCollectionModal && <RenameCollection collection={collection} onClose={() => setShowRenameCollectionModal(false)} />}
|
||||||
{showRemoveCollectionModal && <RemoveCollection collection={collection} onClose={() => setShowRemoveCollectionModal(false)} />}
|
{showRemoveCollectionModal && <RemoveCollection collection={collection} onClose={() => setShowRemoveCollectionModal(false)} />}
|
||||||
<div className="flex py-1 collection-name items-center">
|
<div className="flex py-1 collection-name items-center" ref={drop}>
|
||||||
<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>
|
||||||
@ -148,14 +157,13 @@ const Collection = ({ collection, searchText }) => {
|
|||||||
<div>
|
<div>
|
||||||
{!collectionIsCollapsed ? (
|
{!collectionIsCollapsed ? (
|
||||||
<div>
|
<div>
|
||||||
{requestItems && requestItems.length
|
{folderItems && folderItems.length
|
||||||
? requestItems.map((i) => {
|
? folderItems.map((i) => {
|
||||||
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
||||||
})
|
})
|
||||||
: null}
|
: null}
|
||||||
|
{requestItems && requestItems.length
|
||||||
{folderItems && folderItems.length
|
? requestItems.map((i) => {
|
||||||
? folderItems.map((i) => {
|
|
||||||
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
||||||
})
|
})
|
||||||
: null}
|
: null}
|
||||||
|
@ -6,6 +6,7 @@ import cloneDeep from 'lodash/cloneDeep';
|
|||||||
import {
|
import {
|
||||||
findItemInCollection,
|
findItemInCollection,
|
||||||
moveCollectionItem,
|
moveCollectionItem,
|
||||||
|
getItemsToResequence,
|
||||||
moveCollectionItemToRootOfCollection,
|
moveCollectionItemToRootOfCollection,
|
||||||
findCollectionByUid,
|
findCollectionByUid,
|
||||||
recursivelyGetAllItemUids,
|
recursivelyGetAllItemUids,
|
||||||
@ -13,6 +14,7 @@ import {
|
|||||||
transformRequestToSaveToFilesystem,
|
transformRequestToSaveToFilesystem,
|
||||||
findParentItemInCollection,
|
findParentItemInCollection,
|
||||||
findEnvironmentInCollection,
|
findEnvironmentInCollection,
|
||||||
|
isItemARequest,
|
||||||
isItemAFolder,
|
isItemAFolder,
|
||||||
refreshUidsInItem,
|
refreshUidsInItem,
|
||||||
interpolateEnvironmentVars
|
interpolateEnvironmentVars
|
||||||
@ -30,8 +32,6 @@ 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,
|
||||||
selectEnvironment as _selectEnvironment,
|
selectEnvironment as _selectEnvironment,
|
||||||
createCollection as _createCollection,
|
createCollection as _createCollection,
|
||||||
@ -330,27 +330,6 @@ export const moveItem = (collectionUid, draggedItemUid, targetItemUid) => (dispa
|
|||||||
return reject(new Error('Collection not found'));
|
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 collectionCopy = cloneDeep(collection);
|
||||||
const draggedItem = findItemInCollection(collectionCopy, draggedItemUid);
|
const draggedItem = findItemInCollection(collectionCopy, draggedItemUid);
|
||||||
const targetItem = findItemInCollection(collectionCopy, targetItemUid);
|
const targetItem = findItemInCollection(collectionCopy, targetItemUid);
|
||||||
@ -363,24 +342,112 @@ export const moveItem = (collectionUid, draggedItemUid, targetItemUid) => (dispa
|
|||||||
return reject(new Error('Target item not found'));
|
return reject(new Error('Target item not found'));
|
||||||
}
|
}
|
||||||
|
|
||||||
moveCollectionItem(collectionCopy, draggedItem, targetItem);
|
const draggedItemParent = findParentItemInCollection(collectionCopy, draggedItemUid);
|
||||||
|
const targetItemParent = findParentItemInCollection(collectionCopy, targetItemUid);
|
||||||
|
const sameParent = draggedItemParent === targetItemParent;
|
||||||
|
|
||||||
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
|
// file item dragged onto another file item and both are in the same folder
|
||||||
|
// this is also true when both items are at the root level
|
||||||
|
if (isItemARequest(draggedItem) && isItemARequest(targetItem) && sameParent) {
|
||||||
|
moveCollectionItem(collectionCopy, draggedItem, targetItem);
|
||||||
|
const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy);
|
||||||
|
|
||||||
collectionSchema
|
return ipcRenderer
|
||||||
.validate(collectionToSave)
|
.invoke('renderer:resequence-items', itemsToResequence)
|
||||||
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
|
.then(resolve)
|
||||||
.then(() => {
|
.catch((error) => reject(error));
|
||||||
dispatch(
|
}
|
||||||
_moveItem({
|
|
||||||
collectionUid: collectionUid,
|
// file item dragged onto another file item which is at the root level
|
||||||
draggedItemUid: draggedItemUid,
|
if (isItemARequest(draggedItem) && isItemARequest(targetItem) && !targetItemParent) {
|
||||||
targetItemUid: targetItemUid
|
const draggedItemPathname = draggedItem.pathname;
|
||||||
})
|
moveCollectionItem(collectionCopy, draggedItem, targetItem);
|
||||||
);
|
const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy);
|
||||||
})
|
const itemsToResequence2 = getItemsToResequence(targetItemParent, collectionCopy);
|
||||||
.then(() => resolve())
|
|
||||||
.catch((error) => reject(error));
|
return ipcRenderer
|
||||||
|
.invoke('renderer:move-file-item', draggedItemPathname, collectionCopy.pathname)
|
||||||
|
.then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence))
|
||||||
|
.then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence2))
|
||||||
|
.then(resolve)
|
||||||
|
.catch((error) => reject(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// file item dragged onto another file item and both are in different folders
|
||||||
|
if (isItemARequest(draggedItem) && isItemARequest(targetItem) && !sameParent) {
|
||||||
|
const draggedItemPathname = draggedItem.pathname;
|
||||||
|
moveCollectionItem(collectionCopy, draggedItem, targetItem);
|
||||||
|
const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy);
|
||||||
|
const itemsToResequence2 = getItemsToResequence(targetItemParent, collectionCopy);
|
||||||
|
console.log('itemsToResequence', itemsToResequence);
|
||||||
|
console.log('itemsToResequence2', itemsToResequence2);
|
||||||
|
|
||||||
|
return ipcRenderer
|
||||||
|
.invoke('renderer:move-file-item', draggedItemPathname, targetItemParent.pathname)
|
||||||
|
.then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence))
|
||||||
|
.then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence2))
|
||||||
|
.then(resolve)
|
||||||
|
.catch((error) => reject(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// file item dragged into its own folder
|
||||||
|
if (isItemARequest(draggedItem) && isItemAFolder(targetItem) && draggedItemParent === targetItem) {
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
// file item dragged into another folder
|
||||||
|
if (isItemARequest(draggedItem) && isItemAFolder(targetItem) && draggedItemParent !== targetItem) {
|
||||||
|
const draggedItemPathname = draggedItem.pathname;
|
||||||
|
moveCollectionItem(collectionCopy, draggedItem, targetItem);
|
||||||
|
const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy);
|
||||||
|
const itemsToResequence2 = getItemsToResequence(targetItem, collectionCopy);
|
||||||
|
|
||||||
|
return ipcRenderer
|
||||||
|
.invoke('renderer:move-file-item', draggedItemPathname, targetItem.pathname)
|
||||||
|
.then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence))
|
||||||
|
.then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence2))
|
||||||
|
.then(resolve)
|
||||||
|
.catch((error) => reject(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// end of the file drags, now let's handle folder drags
|
||||||
|
// folder drags are simpler since we don't allow ordering of folders
|
||||||
|
|
||||||
|
// folder dragged into its own folder
|
||||||
|
if (isItemAFolder(draggedItem) && isItemAFolder(targetItem) && draggedItemParent === targetItem) {
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
// folder dragged into a file which is at the same level
|
||||||
|
// this is also true when both items are at the root level
|
||||||
|
if (isItemAFolder(draggedItem) && isItemARequest(targetItem) && sameParent) {
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
// folder dragged into a file which is a child of the folder
|
||||||
|
if (isItemAFolder(draggedItem) && isItemARequest(targetItem) && draggedItem === targetItemParent) {
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
// folder dragged into a file which is at the root level
|
||||||
|
if (isItemAFolder(draggedItem) && isItemARequest(targetItem) && !targetItemParent) {
|
||||||
|
const draggedItemPathname = draggedItem.pathname;
|
||||||
|
|
||||||
|
return ipcRenderer
|
||||||
|
.invoke('renderer:move-folder-item', draggedItemPathname, collectionCopy.pathname)
|
||||||
|
.then(resolve)
|
||||||
|
.catch((error) => reject(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// folder dragged into another folder
|
||||||
|
if (isItemAFolder(draggedItem) && isItemAFolder(targetItem) && draggedItemParent !== targetItem) {
|
||||||
|
const draggedItemPathname = draggedItem.pathname;
|
||||||
|
|
||||||
|
return ipcRenderer
|
||||||
|
.invoke('renderer:move-folder-item', draggedItemPathname, targetItem.pathname)
|
||||||
|
.then(resolve)
|
||||||
|
.catch((error) => reject(error));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -393,45 +460,28 @@ export const moveItemToRootOfCollection = (collectionUid, draggedItemUid) => (di
|
|||||||
return reject(new Error('Collection not found'));
|
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 collectionCopy = cloneDeep(collection);
|
||||||
const draggedItem = findItemInCollection(collectionCopy, draggedItemUid);
|
const draggedItem = findItemInCollection(collectionCopy, draggedItemUid);
|
||||||
|
|
||||||
if (!draggedItem) {
|
if (!draggedItem) {
|
||||||
return reject(new Error('Dragged item not found'));
|
return reject(new Error('Dragged item not found'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const draggedItemParent = findParentItemInCollection(collectionCopy, draggedItemUid);
|
||||||
|
// file item is already at the root level
|
||||||
|
if (!draggedItemParent) {
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
const draggedItemPathname = draggedItem.pathname;
|
||||||
moveCollectionItemToRootOfCollection(collectionCopy, draggedItem);
|
moveCollectionItemToRootOfCollection(collectionCopy, draggedItem);
|
||||||
|
const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy);
|
||||||
|
const itemsToResequence2 = getItemsToResequence(collectionCopy, collectionCopy);
|
||||||
|
|
||||||
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
|
return ipcRenderer
|
||||||
|
.invoke('renderer:move-file-item', draggedItemPathname, collectionCopy.pathname)
|
||||||
collectionSchema
|
.then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence))
|
||||||
.validate(collectionToSave)
|
.then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence2))
|
||||||
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
|
.then(resolve)
|
||||||
.then(() => {
|
|
||||||
dispatch(
|
|
||||||
_moveItemToRootOfCollection({
|
|
||||||
collectionUid: collectionUid,
|
|
||||||
draggedItemUid: draggedItemUid
|
|
||||||
})
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch((error) => reject(error));
|
.catch((error) => reject(error));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -14,10 +14,11 @@ import {
|
|||||||
findEnvironmentInCollection,
|
findEnvironmentInCollection,
|
||||||
findItemInCollectionByPathname,
|
findItemInCollectionByPathname,
|
||||||
addDepth,
|
addDepth,
|
||||||
moveCollectionItem,
|
|
||||||
collapseCollection,
|
collapseCollection,
|
||||||
deleteItemInCollection,
|
deleteItemInCollection,
|
||||||
isItemARequest
|
deleteItemInCollectionByPathname,
|
||||||
|
isItemARequest,
|
||||||
|
areItemsTheSameExceptSeqUpdate
|
||||||
} from 'utils/collections';
|
} from 'utils/collections';
|
||||||
import { parseQueryParams, stringifyQueryParams } from 'utils/url';
|
import { parseQueryParams, stringifyQueryParams } from 'utils/url';
|
||||||
import { getSubdirectoriesFromRoot } from 'utils/common/platform';
|
import { getSubdirectoriesFromRoot } from 'utils/common/platform';
|
||||||
@ -146,38 +147,6 @@ 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, requestSent } = action.payload;
|
const { itemUid, collectionUid, cancelTokenUid, requestSent } = action.payload;
|
||||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||||
@ -650,7 +619,7 @@ export const collectionsSlice = createSlice({
|
|||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
pathname: `${currentPath}${PATH_SEPARATOR}${directoryName}`,
|
pathname: `${currentPath}${PATH_SEPARATOR}${directoryName}`,
|
||||||
name: directoryName,
|
name: directoryName,
|
||||||
collapsed: false,
|
collapsed: true,
|
||||||
type: 'folder',
|
type: 'folder',
|
||||||
items: []
|
items: []
|
||||||
};
|
};
|
||||||
@ -669,6 +638,7 @@ export const collectionsSlice = createSlice({
|
|||||||
if (currentItem) {
|
if (currentItem) {
|
||||||
currentItem.name = file.data.name;
|
currentItem.name = file.data.name;
|
||||||
currentItem.type = file.data.type;
|
currentItem.type = file.data.type;
|
||||||
|
currentItem.seq = file.data.seq;
|
||||||
currentItem.request = file.data.request;
|
currentItem.request = file.data.request;
|
||||||
currentItem.filename = file.meta.name;
|
currentItem.filename = file.meta.name;
|
||||||
currentItem.pathname = file.meta.pathname;
|
currentItem.pathname = file.meta.pathname;
|
||||||
@ -678,6 +648,7 @@ export const collectionsSlice = createSlice({
|
|||||||
uid: file.data.uid,
|
uid: file.data.uid,
|
||||||
name: file.data.name,
|
name: file.data.name,
|
||||||
type: file.data.type,
|
type: file.data.type,
|
||||||
|
seq: file.data.seq,
|
||||||
request: file.data.request,
|
request: file.data.request,
|
||||||
filename: file.meta.name,
|
filename: file.meta.name,
|
||||||
pathname: file.meta.pathname,
|
pathname: file.meta.pathname,
|
||||||
@ -703,7 +674,7 @@ export const collectionsSlice = createSlice({
|
|||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
pathname: `${currentPath}${PATH_SEPARATOR}${directoryName}`,
|
pathname: `${currentPath}${PATH_SEPARATOR}${directoryName}`,
|
||||||
name: directoryName,
|
name: directoryName,
|
||||||
collapsed: false,
|
collapsed: true,
|
||||||
type: 'folder',
|
type: 'folder',
|
||||||
items: []
|
items: []
|
||||||
};
|
};
|
||||||
@ -724,12 +695,20 @@ export const collectionsSlice = createSlice({
|
|||||||
const item = findItemInCollection(collection, file.data.uid);
|
const item = findItemInCollection(collection, file.data.uid);
|
||||||
|
|
||||||
if (item) {
|
if (item) {
|
||||||
item.name = file.data.name;
|
// whenever a user attempts to sort a req within the same folder
|
||||||
item.type = file.data.type;
|
// the seq is updated, but everything else remains the same
|
||||||
item.request = file.data.request;
|
// we don't want to lose the draft in this case
|
||||||
item.filename = file.meta.name;
|
if(areItemsTheSameExceptSeqUpdate(item, file.data)) {
|
||||||
item.pathname = file.meta.pathname;
|
item.seq = file.data.seq;
|
||||||
item.draft = null;
|
} else {
|
||||||
|
item.name = file.data.name;
|
||||||
|
item.type = file.data.type;
|
||||||
|
item.seq = file.data.seq;
|
||||||
|
item.request = file.data.request;
|
||||||
|
item.filename = file.meta.name;
|
||||||
|
item.pathname = file.meta.pathname;
|
||||||
|
item.draft = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -741,7 +720,7 @@ export const collectionsSlice = createSlice({
|
|||||||
const item = findItemInCollectionByPathname(collection, file.meta.pathname);
|
const item = findItemInCollectionByPathname(collection, file.meta.pathname);
|
||||||
|
|
||||||
if (item) {
|
if (item) {
|
||||||
deleteItemInCollection(item.uid, collection);
|
deleteItemInCollectionByPathname(file.meta.pathname, collection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -753,7 +732,7 @@ export const collectionsSlice = createSlice({
|
|||||||
const item = findItemInCollectionByPathname(collection, directory.meta.pathname);
|
const item = findItemInCollectionByPathname(collection, directory.meta.pathname);
|
||||||
|
|
||||||
if (item) {
|
if (item) {
|
||||||
deleteItemInCollection(item.uid, collection);
|
deleteItemInCollectionByPathname(directory.meta.pathname, collection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -788,8 +767,6 @@ export const {
|
|||||||
deleteItem,
|
deleteItem,
|
||||||
renameItem,
|
renameItem,
|
||||||
cloneItem,
|
cloneItem,
|
||||||
moveItem,
|
|
||||||
moveItemToRootOfCollection,
|
|
||||||
requestSent,
|
requestSent,
|
||||||
requestCancelled,
|
requestCancelled,
|
||||||
responseReceived,
|
responseReceived,
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
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';
|
||||||
@ -7,7 +6,14 @@ 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';
|
||||||
import sortBy from 'lodash/sortBy';
|
import sortBy from 'lodash/sortBy';
|
||||||
|
import isEqual from 'lodash/isEqual';
|
||||||
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
import { uuid } from 'utils/common';
|
import { uuid } from 'utils/common';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// although we are not using rekonjs directly
|
||||||
|
// its populating the global string prototype with .reckon method
|
||||||
|
import reckon from 'reckonjs';
|
||||||
|
|
||||||
const replaceTabsWithSpaces = (str, numSpaces = 2) => {
|
const replaceTabsWithSpaces = (str, numSpaces = 2) => {
|
||||||
if (!str || !str.length || !isString(str)) {
|
if (!str || !str.length || !isString(str)) {
|
||||||
@ -128,6 +134,7 @@ export const moveCollectionItem = (collection, draggedItem, targetItem) => {
|
|||||||
|
|
||||||
if (draggedItemParent) {
|
if (draggedItemParent) {
|
||||||
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
|
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
|
||||||
|
draggedItem.pathname = path.join(draggedItemParent.pathname, draggedItem.filename);
|
||||||
} else {
|
} else {
|
||||||
collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid);
|
collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid);
|
||||||
}
|
}
|
||||||
@ -135,15 +142,18 @@ export const moveCollectionItem = (collection, draggedItem, targetItem) => {
|
|||||||
if (targetItem.type === 'folder') {
|
if (targetItem.type === 'folder') {
|
||||||
targetItem.items = targetItem.items || [];
|
targetItem.items = targetItem.items || [];
|
||||||
targetItem.items.push(draggedItem);
|
targetItem.items.push(draggedItem);
|
||||||
|
draggedItem.pathname = path.join(targetItem.pathname, draggedItem.filename);
|
||||||
} else {
|
} else {
|
||||||
let targetItemParent = findParentItemInCollection(collection, targetItem.uid);
|
let targetItemParent = findParentItemInCollection(collection, targetItem.uid);
|
||||||
|
|
||||||
if (targetItemParent) {
|
if (targetItemParent) {
|
||||||
let targetItemIndex = findIndex(targetItemParent.items, (i) => i.uid === targetItem.uid);
|
let targetItemIndex = findIndex(targetItemParent.items, (i) => i.uid === targetItem.uid);
|
||||||
targetItemParent.items.splice(targetItemIndex + 1, 0, draggedItem);
|
targetItemParent.items.splice(targetItemIndex + 1, 0, draggedItem);
|
||||||
|
draggedItem.pathname = path.join(targetItemParent.pathname, draggedItem.filename);
|
||||||
} else {
|
} else {
|
||||||
let targetItemIndex = findIndex(collection.items, (i) => i.uid === targetItem.uid);
|
let targetItemIndex = findIndex(collection.items, (i) => i.uid === targetItem.uid);
|
||||||
collection.items.splice(targetItemIndex + 1, 0, draggedItem);
|
collection.items.splice(targetItemIndex + 1, 0, draggedItem);
|
||||||
|
draggedItem.pathname = path.join(collection.pathname, draggedItem.filename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -151,13 +161,46 @@ export const moveCollectionItem = (collection, draggedItem, targetItem) => {
|
|||||||
export const moveCollectionItemToRootOfCollection = (collection, draggedItem) => {
|
export const moveCollectionItemToRootOfCollection = (collection, draggedItem) => {
|
||||||
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
|
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
|
||||||
|
|
||||||
if (draggedItemParent) {
|
// If the dragged item is already at the root of the collection, do nothing
|
||||||
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
|
if(!draggedItemParent) {
|
||||||
} else {
|
return;
|
||||||
collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
|
||||||
collection.items.push(draggedItem);
|
collection.items.push(draggedItem);
|
||||||
|
draggedItem.pathname = path.join(collection.pathname, draggedItem.filename);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getItemsToResequence = (parent, collection) => {
|
||||||
|
let itemsToResequence = [];
|
||||||
|
|
||||||
|
if(!parent) {
|
||||||
|
let index = 1;
|
||||||
|
each(collection.items, (item) => {
|
||||||
|
if(isItemARequest(item)) {
|
||||||
|
itemsToResequence.push({
|
||||||
|
pathname: item.pathname,
|
||||||
|
seq: index++
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return itemsToResequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent.items && parent.items.length) {
|
||||||
|
let index = 1;
|
||||||
|
each(parent.items, (item) => {
|
||||||
|
if(isItemARequest(item)) {
|
||||||
|
itemsToResequence.push({
|
||||||
|
pathname: item.pathname,
|
||||||
|
seq: index++
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return itemsToResequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemsToResequence;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
||||||
@ -335,7 +378,6 @@ export const deleteItemInCollection = (itemUid, collection) => {
|
|||||||
collection.items = filter(collection.items, (i) => i.uid !== itemUid);
|
collection.items = filter(collection.items, (i) => i.uid !== itemUid);
|
||||||
|
|
||||||
let flattenedItems = flattenItems(collection.items);
|
let flattenedItems = flattenItems(collection.items);
|
||||||
|
|
||||||
each(flattenedItems, (i) => {
|
each(flattenedItems, (i) => {
|
||||||
if (i.items && i.items.length) {
|
if (i.items && i.items.length) {
|
||||||
i.items = filter(i.items, (i) => i.uid !== itemUid);
|
i.items = filter(i.items, (i) => i.uid !== itemUid);
|
||||||
@ -343,6 +385,17 @@ export const deleteItemInCollection = (itemUid, collection) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const deleteItemInCollectionByPathname = (pathname, collection) => {
|
||||||
|
collection.items = filter(collection.items, (i) => i.pathname !== pathname);
|
||||||
|
|
||||||
|
let flattenedItems = flattenItems(collection.items);
|
||||||
|
each(flattenedItems, (i) => {
|
||||||
|
if (i.items && i.items.length) {
|
||||||
|
i.items = filter(i.items, (i) => i.pathname !== pathname);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const isItemARequest = (item) => {
|
export const isItemARequest = (item) => {
|
||||||
return item.hasOwnProperty('request') && ['http-request', 'graphql-request'].includes(item.type) && !item.items;
|
return item.hasOwnProperty('request') && ['http-request', 'graphql-request'].includes(item.type) && !item.items;
|
||||||
};
|
};
|
||||||
@ -449,6 +502,44 @@ export const interpolateEnvironmentVars = (item, variables) => {
|
|||||||
return request;
|
return request;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const deleteUidsInItem = (item) => {
|
||||||
|
delete item.uid;
|
||||||
|
const params = get(item, 'request.params', []);
|
||||||
|
const headers = get(item, 'request.headers', []);
|
||||||
|
const bodyFormUrlEncoded = get(item, 'request.body.formUrlEncoded', []);
|
||||||
|
const bodyMultipartForm = get(item, 'request.body.multipartForm', []);
|
||||||
|
|
||||||
|
params.forEach((param) => delete param.uid);
|
||||||
|
headers.forEach((header) => delete header.uid);
|
||||||
|
bodyFormUrlEncoded.forEach((param) => delete param.uid);
|
||||||
|
bodyMultipartForm.forEach((param) => delete param.uid);
|
||||||
|
|
||||||
|
return item;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const areItemsTheSameExceptSeqUpdate = (_item1, _item2) => {
|
||||||
|
let item1 = cloneDeep(_item1);
|
||||||
|
let item2 = cloneDeep(_item2);
|
||||||
|
|
||||||
|
// remove seq from both items
|
||||||
|
delete item1.seq;
|
||||||
|
delete item2.seq;
|
||||||
|
|
||||||
|
// remove draft from both items
|
||||||
|
delete item1.draft;
|
||||||
|
delete item2.draft;
|
||||||
|
|
||||||
|
// get projection of both items
|
||||||
|
item1 = transformRequestToSaveToFilesystem(item1);
|
||||||
|
item2 = transformRequestToSaveToFilesystem(item2);
|
||||||
|
|
||||||
|
// delete uids from both items
|
||||||
|
deleteUidsInItem(item1);
|
||||||
|
deleteUidsInItem(item2);
|
||||||
|
|
||||||
|
return isEqual(item1, item2);
|
||||||
|
};
|
||||||
|
|
||||||
export const getDefaultRequestPaneTab = (item) => {
|
export const getDefaultRequestPaneTab = (item) => {
|
||||||
if(item.type === 'http-request') {
|
if(item.type === 'http-request') {
|
||||||
return 'params';
|
return 'params';
|
||||||
|
@ -10,7 +10,8 @@ const {
|
|||||||
envJsonToBru,
|
envJsonToBru,
|
||||||
} = require('@usebruno/bruno-lang');
|
} = require('@usebruno/bruno-lang');
|
||||||
const { itemSchema } = require('@usebruno/schema');
|
const { itemSchema } = require('@usebruno/schema');
|
||||||
const { generateUidBasedOnHash, uuid } = require('../utils/common');
|
const { uuid } = require('../utils/common');
|
||||||
|
const { getRequestUid } = require('../cache/requestUids');
|
||||||
|
|
||||||
const isJsonEnvironmentConfig = (pathname, collectionPath) => {
|
const isJsonEnvironmentConfig = (pathname, collectionPath) => {
|
||||||
const dirname = path.dirname(pathname);
|
const dirname = path.dirname(pathname);
|
||||||
@ -28,7 +29,7 @@ const isBruEnvironmentConfig = (pathname, collectionPath) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const hydrateRequestWithUuid = (request, pathname) => {
|
const hydrateRequestWithUuid = (request, pathname) => {
|
||||||
request.uid = generateUidBasedOnHash(pathname);
|
request.uid = getRequestUid(pathname);
|
||||||
|
|
||||||
const params = _.get(request, 'request.params', []);
|
const params = _.get(request, 'request.params', []);
|
||||||
const headers = _.get(request, 'request.headers', []);
|
const headers = _.get(request, 'request.headers', []);
|
||||||
@ -57,7 +58,7 @@ const addEnvironmentFile = async (win, pathname, collectionUid) => {
|
|||||||
const bruContent = fs.readFileSync(pathname, 'utf8');
|
const bruContent = fs.readFileSync(pathname, 'utf8');
|
||||||
file.data = bruToEnvJson(bruContent);
|
file.data = bruToEnvJson(bruContent);
|
||||||
file.data.name = basename.substring(0, basename.length - 4);
|
file.data.name = basename.substring(0, basename.length - 4);
|
||||||
file.data.uid = generateUidBasedOnHash(pathname);
|
file.data.uid = getRequestUid(pathname);
|
||||||
|
|
||||||
_.each(_.get(file, 'data.variables', []), (variable) => variable.uid = uuid());
|
_.each(_.get(file, 'data.variables', []), (variable) => variable.uid = uuid());
|
||||||
win.webContents.send('main:collection-tree-updated', 'addEnvironmentFile', file);
|
win.webContents.send('main:collection-tree-updated', 'addEnvironmentFile', file);
|
||||||
@ -80,7 +81,7 @@ const changeEnvironmentFile = async (win, pathname, collectionUid) => {
|
|||||||
const bruContent = fs.readFileSync(pathname, 'utf8');
|
const bruContent = fs.readFileSync(pathname, 'utf8');
|
||||||
file.data = bruToEnvJson(bruContent);
|
file.data = bruToEnvJson(bruContent);
|
||||||
file.data.name = basename.substring(0, basename.length - 4);
|
file.data.name = basename.substring(0, basename.length - 4);
|
||||||
file.data.uid = generateUidBasedOnHash(pathname);
|
file.data.uid = getRequestUid(pathname);
|
||||||
_.each(_.get(file, 'data.variables', []), (variable) => variable.uid = uuid());
|
_.each(_.get(file, 'data.variables', []), (variable) => variable.uid = uuid());
|
||||||
|
|
||||||
// we are reusing the addEnvironmentFile event itself
|
// we are reusing the addEnvironmentFile event itself
|
||||||
@ -101,7 +102,7 @@ const unlinkEnvironmentFile = async (win, pathname, collectionUid) => {
|
|||||||
name: path.basename(pathname),
|
name: path.basename(pathname),
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
uid: generateUidBasedOnHash(pathname),
|
uid: getRequestUid(pathname),
|
||||||
name: path.basename(pathname).substring(0, path.basename(pathname).length - 4),
|
name: path.basename(pathname).substring(0, path.basename(pathname).length - 4),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
44
packages/bruno-electron/src/cache/requestUids.js
vendored
Normal file
44
packages/bruno-electron/src/cache/requestUids.js
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* we maintain a cache of request uids to ensure that we
|
||||||
|
* preserve the same uid for a request even when the request
|
||||||
|
* moves to a different location
|
||||||
|
*
|
||||||
|
* In the past, we used to generate unique ids based on the
|
||||||
|
* pathname of the request, but we faced problems when implementing
|
||||||
|
* functionality where the user can move the request to a different
|
||||||
|
* location. In that case, the uid would change, and the we would
|
||||||
|
* lose the request's draft state if the user has made some changes
|
||||||
|
*/
|
||||||
|
|
||||||
|
const requestUids = new Map();
|
||||||
|
const { uuid } = require('../utils/common');
|
||||||
|
|
||||||
|
const getRequestUid = (pathname) => {
|
||||||
|
let uid = requestUids.get(pathname);
|
||||||
|
|
||||||
|
if (!uid) {
|
||||||
|
uid = uuid();
|
||||||
|
requestUids.set(pathname, uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return uid;
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveRequestUid = (oldPathname, newPathname) => {
|
||||||
|
const uid = requestUids.get(oldPathname);
|
||||||
|
|
||||||
|
if (uid) {
|
||||||
|
requestUids.delete(oldPathname);
|
||||||
|
requestUids.set(newPathname, uid);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteRequestUid = (pathname) => {
|
||||||
|
requestUids.delete(pathname);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getRequestUid,
|
||||||
|
moveRequestUid,
|
||||||
|
deleteRequestUid
|
||||||
|
};
|
@ -13,11 +13,13 @@ const {
|
|||||||
hasBruExtension,
|
hasBruExtension,
|
||||||
isDirectory,
|
isDirectory,
|
||||||
browseDirectory,
|
browseDirectory,
|
||||||
createDirectory
|
createDirectory,
|
||||||
|
searchForBruFiles
|
||||||
} = require('../utils/filesystem');
|
} = require('../utils/filesystem');
|
||||||
const { uuid, stringifyJson } = require('../utils/common');
|
const { stringifyJson } = require('../utils/common');
|
||||||
const { openCollectionDialog, openCollection } = require('../app/collections');
|
const { openCollectionDialog, openCollection } = require('../app/collections');
|
||||||
const { generateUidBasedOnHash } = require('../utils/common');
|
const { generateUidBasedOnHash } = require('../utils/common');
|
||||||
|
const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids');
|
||||||
|
|
||||||
const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
|
const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
|
||||||
// browse directory
|
// browse directory
|
||||||
@ -179,6 +181,12 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
|||||||
|
|
||||||
// if its directory, rename and return
|
// if its directory, rename and return
|
||||||
if(isDirectory(oldPath)) {
|
if(isDirectory(oldPath)) {
|
||||||
|
const bruFilesAtSource = await searchForBruFiles(oldPath);
|
||||||
|
|
||||||
|
for(let bruFile of bruFilesAtSource) {
|
||||||
|
const newBruFilePath = bruFile.replace(oldPath, newPath);
|
||||||
|
moveRequestUid(bruFile, newBruFilePath);
|
||||||
|
}
|
||||||
return fs.renameSync(oldPath, newPath);
|
return fs.renameSync(oldPath, newPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,6 +201,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
|||||||
|
|
||||||
jsonData.name = newName;
|
jsonData.name = newName;
|
||||||
|
|
||||||
|
moveRequestUid(oldPath, newPath);
|
||||||
|
|
||||||
const content = jsonToBru(jsonData);
|
const content = jsonToBru(jsonData);
|
||||||
await writeFile(newPath, content);
|
await writeFile(newPath, content);
|
||||||
await fs.unlinkSync(oldPath);
|
await fs.unlinkSync(oldPath);
|
||||||
@ -218,9 +228,25 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
|||||||
ipcMain.handle('renderer:delete-item', async (event, pathname, type) => {
|
ipcMain.handle('renderer:delete-item', async (event, pathname, type) => {
|
||||||
try {
|
try {
|
||||||
if(type === 'folder') {
|
if(type === 'folder') {
|
||||||
await fs.rmSync(pathname, { recursive: true, force: true});
|
if(!fs.existsSync(pathname)) {
|
||||||
|
return Promise.reject(new Error('The directory does not exist'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the request uid mappings
|
||||||
|
const bruFilesAtSource = await searchForBruFiles(pathname);
|
||||||
|
for(let bruFile of bruFilesAtSource) {
|
||||||
|
deleteRequestUid(bruFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.rmSync(pathname, { recursive: true, force: true});
|
||||||
} else if (['http-request', 'graphql-request'].includes(type)) {
|
} else if (['http-request', 'graphql-request'].includes(type)) {
|
||||||
await fs.unlinkSync(pathname);
|
if(!fs.existsSync(pathname)) {
|
||||||
|
return Promise.reject(new Error('The file does not exist'));
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteRequestUid(pathname);
|
||||||
|
|
||||||
|
fs.unlinkSync(pathname);
|
||||||
} else {
|
} else {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
@ -308,6 +334,63 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('renderer:resequence-items', async (event, itemsToResequence) => {
|
||||||
|
try {
|
||||||
|
for(let item of itemsToResequence) {
|
||||||
|
const bru = fs.readFileSync(item.pathname, 'utf8');
|
||||||
|
const jsonData = bruToJson(bru);
|
||||||
|
|
||||||
|
if(jsonData.seq !== item.seq) {
|
||||||
|
jsonData.seq = item.seq;
|
||||||
|
const content = jsonToBru(jsonData);
|
||||||
|
await writeFile(item.pathname, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('renderer:move-file-item', async (event, itemPath, destinationPath) => {
|
||||||
|
try {
|
||||||
|
const itemContent = fs.readFileSync(itemPath, 'utf8');
|
||||||
|
const newItemPath = path.join(destinationPath, path.basename(itemPath));
|
||||||
|
|
||||||
|
moveRequestUid(itemPath, newItemPath);
|
||||||
|
|
||||||
|
fs.unlinkSync(itemPath);
|
||||||
|
fs.writeFileSync(newItemPath, itemContent);
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('renderer:move-folder-item', async (event, folderPath, destinationPath) => {
|
||||||
|
try {
|
||||||
|
const folderName = path.basename(folderPath);
|
||||||
|
const newFolderPath = path.join(destinationPath, folderName);
|
||||||
|
|
||||||
|
if(!fs.existsSync(folderPath)) {
|
||||||
|
throw new Error(`folder: ${folderPath} does not exist`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(fs.existsSync(newFolderPath)) {
|
||||||
|
throw new Error(`folder: ${newFolderPath} already exists`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bruFilesAtSource = await searchForBruFiles(folderPath);
|
||||||
|
|
||||||
|
for(let bruFile of bruFilesAtSource) {
|
||||||
|
const newBruFilePath = bruFile.replace(folderPath, newFolderPath);
|
||||||
|
moveRequestUid(bruFile, newBruFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.renameSync(folderPath, newFolderPath);
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('renderer:ready', async (event) => {
|
ipcMain.handle('renderer:ready', async (event) => {
|
||||||
// reload last opened collections
|
// reload last opened collections
|
||||||
const lastOpened = lastOpenedCollections.getAll();
|
const lastOpened = lastOpenedCollections.getAll();
|
||||||
|
@ -95,6 +95,25 @@ const browseDirectory = async (win) => {
|
|||||||
return isDirectory(resolvedPath) ? resolvedPath : false;
|
return isDirectory(resolvedPath) ? resolvedPath : false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const searchForFiles = (dir, extension) => {
|
||||||
|
let results = [];
|
||||||
|
const files = fs.readdirSync(dir);
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(dir, file);
|
||||||
|
const stat = fs.statSync(filePath);
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
results = results.concat(searchForFiles(filePath, extension));
|
||||||
|
} else if (path.extname(file) === extension) {
|
||||||
|
results.push(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchForBruFiles = (dir) => {
|
||||||
|
return searchForFiles(dir, '.bru');
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
isValidPathname,
|
isValidPathname,
|
||||||
exists,
|
exists,
|
||||||
@ -106,5 +125,7 @@ module.exports = {
|
|||||||
hasJsonExtension,
|
hasJsonExtension,
|
||||||
hasBruExtension,
|
hasBruExtension,
|
||||||
createDirectory,
|
createDirectory,
|
||||||
browseDirectory
|
browseDirectory,
|
||||||
|
searchForFiles,
|
||||||
|
searchForBruFiles
|
||||||
};
|
};
|
||||||
|
@ -44,6 +44,7 @@ const bruToJson = (fileContents) => {
|
|||||||
const json = {
|
const json = {
|
||||||
type: parsed.type || '',
|
type: parsed.type || '',
|
||||||
name: parsed.name || '',
|
name: parsed.name || '',
|
||||||
|
seq: parsed.seq || 1,
|
||||||
request: {
|
request: {
|
||||||
method: parsed.method || '',
|
method: parsed.method || '',
|
||||||
url: parsed.url || '',
|
url: parsed.url || '',
|
||||||
@ -78,6 +79,7 @@ const jsonToBru = (json) => {
|
|||||||
const {
|
const {
|
||||||
type,
|
type,
|
||||||
name,
|
name,
|
||||||
|
seq,
|
||||||
request: {
|
request: {
|
||||||
method,
|
method,
|
||||||
url,
|
url,
|
||||||
@ -92,6 +94,7 @@ method ${method}
|
|||||||
url ${url}
|
url ${url}
|
||||||
type ${type}
|
type ${type}
|
||||||
body-mode ${body ? body.mode : 'none'}
|
body-mode ${body ? body.mode : 'none'}
|
||||||
|
seq ${seq ? seq : 1}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if(params && params.length) {
|
if(params && params.length) {
|
||||||
|
@ -17,6 +17,7 @@ const inlineTag = sequenceOf([
|
|||||||
str('name'),
|
str('name'),
|
||||||
str('method'),
|
str('method'),
|
||||||
str('url'),
|
str('url'),
|
||||||
|
str('seq'),
|
||||||
str('body-mode')
|
str('body-mode')
|
||||||
]),
|
]),
|
||||||
whitespace,
|
whitespace,
|
||||||
|
@ -58,6 +58,7 @@ const requestSchema = Yup.object({
|
|||||||
const itemSchema = Yup.object({
|
const itemSchema = Yup.object({
|
||||||
uid: uidSchema,
|
uid: uidSchema,
|
||||||
type: Yup.string().oneOf(['http-request', 'graphql-request', 'folder']).required('type is required'),
|
type: Yup.string().oneOf(['http-request', 'graphql-request', 'folder']).required('type is required'),
|
||||||
|
seq: Yup.number().min(1),
|
||||||
name: Yup.string()
|
name: Yup.string()
|
||||||
.min(1, 'name must be atleast 1 characters')
|
.min(1, 'name must be atleast 1 characters')
|
||||||
.max(50, 'name must be 100 characters or less')
|
.max(50, 'name must be 100 characters or less')
|
||||||
@ -69,7 +70,7 @@ const itemSchema = Yup.object({
|
|||||||
items: Yup.lazy(() => Yup.array().of(itemSchema)),
|
items: Yup.lazy(() => Yup.array().of(itemSchema)),
|
||||||
filename: Yup.string().max(1024, 'filename cannot be more than 1024 characters').nullable(),
|
filename: Yup.string().max(1024, 'filename cannot be more than 1024 characters').nullable(),
|
||||||
pathname: Yup.string().max(1024, 'pathname cannot be more than 1024 characters').nullable()
|
pathname: Yup.string().max(1024, 'pathname cannot be more than 1024 characters').nullable()
|
||||||
}).noUnknown(true).strict();
|
}).noUnknown(true);
|
||||||
|
|
||||||
const collectionSchema = Yup.object({
|
const collectionSchema = Yup.object({
|
||||||
version: Yup.string().oneOf(['1']).required('version is required'),
|
version: Yup.string().oneOf(['1']).required('version is required'),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user