@@ -148,14 +157,13 @@ const Collection = ({ collection, searchText }) => {
{!collectionIsCollapsed ? (
- {requestItems && requestItems.length
- ? requestItems.map((i) => {
+ {folderItems && folderItems.length
+ ? folderItems.map((i) => {
return ;
})
: null}
-
- {folderItems && folderItems.length
- ? folderItems.map((i) => {
+ {requestItems && requestItems.length
+ ? requestItems.map((i) => {
return ;
})
: null}
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 4fb58d0e4..d09b562f1 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -6,6 +6,7 @@ import cloneDeep from 'lodash/cloneDeep';
import {
findItemInCollection,
moveCollectionItem,
+ getItemsToResequence,
moveCollectionItemToRootOfCollection,
findCollectionByUid,
recursivelyGetAllItemUids,
@@ -13,6 +14,7 @@ import {
transformRequestToSaveToFilesystem,
findParentItemInCollection,
findEnvironmentInCollection,
+ isItemARequest,
isItemAFolder,
refreshUidsInItem,
interpolateEnvironmentVars
@@ -30,8 +32,6 @@ import {
renameItem as _renameItem,
cloneItem as _cloneItem,
deleteItem as _deleteItem,
- moveItem as _moveItem,
- moveItemToRootOfCollection as _moveItemToRootOfCollection,
saveRequest as _saveRequest,
selectEnvironment as _selectEnvironment,
createCollection as _createCollection,
@@ -330,27 +330,6 @@ export const moveItem = (collectionUid, draggedItemUid, targetItemUid) => (dispa
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);
@@ -363,24 +342,112 @@ export const moveItem = (collectionUid, draggedItemUid, targetItemUid) => (dispa
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
- .validate(collectionToSave)
- .then(() => saveCollectionToIdb(window.__idb, collectionToSave))
- .then(() => {
- dispatch(
- _moveItem({
- collectionUid: collectionUid,
- draggedItemUid: draggedItemUid,
- targetItemUid: targetItemUid
- })
- );
- })
- .then(() => resolve())
- .catch((error) => reject(error));
+ return ipcRenderer
+ .invoke('renderer:resequence-items', itemsToResequence)
+ .then(resolve)
+ .catch((error) => reject(error));
+ }
+
+ // file item dragged onto another file item which is at the root level
+ if (isItemARequest(draggedItem) && isItemARequest(targetItem) && !targetItemParent) {
+ const draggedItemPathname = draggedItem.pathname;
+ moveCollectionItem(collectionCopy, draggedItem, targetItem);
+ const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy);
+ const itemsToResequence2 = getItemsToResequence(targetItemParent, collectionCopy);
+
+ 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'));
}
- 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'));
}
+ const draggedItemParent = findParentItemInCollection(collectionCopy, draggedItemUid);
+ // file item is already at the root level
+ if (!draggedItemParent) {
+ return resolve();
+ }
+
+ const draggedItemPathname = draggedItem.pathname;
moveCollectionItemToRootOfCollection(collectionCopy, draggedItem);
+ const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy);
+ const itemsToResequence2 = getItemsToResequence(collectionCopy, collectionCopy);
- const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
-
- collectionSchema
- .validate(collectionToSave)
- .then(() => saveCollectionToIdb(window.__idb, collectionToSave))
- .then(() => {
- dispatch(
- _moveItemToRootOfCollection({
- collectionUid: collectionUid,
- draggedItemUid: draggedItemUid
- })
- );
- })
- .then(() => resolve())
+ 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));
});
};
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 7ac998102..1809ba68f 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -14,10 +14,11 @@ import {
findEnvironmentInCollection,
findItemInCollectionByPathname,
addDepth,
- moveCollectionItem,
collapseCollection,
deleteItemInCollection,
- isItemARequest
+ deleteItemInCollectionByPathname,
+ isItemARequest,
+ areItemsTheSameExceptSeqUpdate
} from 'utils/collections';
import { parseQueryParams, stringifyQueryParams } from 'utils/url';
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) => {
const { itemUid, collectionUid, cancelTokenUid, requestSent } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
@@ -650,7 +619,7 @@ export const collectionsSlice = createSlice({
uid: uuid(),
pathname: `${currentPath}${PATH_SEPARATOR}${directoryName}`,
name: directoryName,
- collapsed: false,
+ collapsed: true,
type: 'folder',
items: []
};
@@ -669,6 +638,7 @@ export const collectionsSlice = createSlice({
if (currentItem) {
currentItem.name = file.data.name;
currentItem.type = file.data.type;
+ currentItem.seq = file.data.seq;
currentItem.request = file.data.request;
currentItem.filename = file.meta.name;
currentItem.pathname = file.meta.pathname;
@@ -678,6 +648,7 @@ export const collectionsSlice = createSlice({
uid: file.data.uid,
name: file.data.name,
type: file.data.type,
+ seq: file.data.seq,
request: file.data.request,
filename: file.meta.name,
pathname: file.meta.pathname,
@@ -703,7 +674,7 @@ export const collectionsSlice = createSlice({
uid: uuid(),
pathname: `${currentPath}${PATH_SEPARATOR}${directoryName}`,
name: directoryName,
- collapsed: false,
+ collapsed: true,
type: 'folder',
items: []
};
@@ -724,12 +695,20 @@ export const collectionsSlice = createSlice({
const item = findItemInCollection(collection, file.data.uid);
if (item) {
- item.name = file.data.name;
- item.type = file.data.type;
- item.request = file.data.request;
- item.filename = file.meta.name;
- item.pathname = file.meta.pathname;
- item.draft = null;
+ // whenever a user attempts to sort a req within the same folder
+ // the seq is updated, but everything else remains the same
+ // we don't want to lose the draft in this case
+ if(areItemsTheSameExceptSeqUpdate(item, file.data)) {
+ item.seq = file.data.seq;
+ } 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);
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);
if (item) {
- deleteItemInCollection(item.uid, collection);
+ deleteItemInCollectionByPathname(directory.meta.pathname, collection);
}
}
},
@@ -788,8 +767,6 @@ export const {
deleteItem,
renameItem,
cloneItem,
- moveItem,
- moveItemToRootOfCollection,
requestSent,
requestCancelled,
responseReceived,
diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js
index 44cd2ed3f..d9923e827 100644
--- a/packages/bruno-app/src/utils/collections/index.js
+++ b/packages/bruno-app/src/utils/collections/index.js
@@ -1,4 +1,3 @@
-import reckon from 'reckonjs';
import get from 'lodash/get';
import each from 'lodash/each';
import find from 'lodash/find';
@@ -7,7 +6,14 @@ 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 { 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) => {
if (!str || !str.length || !isString(str)) {
@@ -128,6 +134,7 @@ export const moveCollectionItem = (collection, draggedItem, targetItem) => {
if (draggedItemParent) {
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
+ draggedItem.pathname = path.join(draggedItemParent.pathname, draggedItem.filename);
} else {
collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid);
}
@@ -135,15 +142,18 @@ export const moveCollectionItem = (collection, draggedItem, targetItem) => {
if (targetItem.type === 'folder') {
targetItem.items = targetItem.items || [];
targetItem.items.push(draggedItem);
+ draggedItem.pathname = path.join(targetItem.pathname, draggedItem.filename);
} 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);
+ draggedItem.pathname = path.join(targetItemParent.pathname, draggedItem.filename);
} else {
let targetItemIndex = findIndex(collection.items, (i) => i.uid === targetItem.uid);
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) => {
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 the dragged item is already at the root of the collection, do nothing
+ if(!draggedItemParent) {
+ return;
}
+ draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
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 = {}) => {
@@ -335,7 +378,6 @@ export const deleteItemInCollection = (itemUid, collection) => {
collection.items = filter(collection.items, (i) => i.uid !== itemUid);
let flattenedItems = flattenItems(collection.items);
-
each(flattenedItems, (i) => {
if (i.items && i.items.length) {
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) => {
return item.hasOwnProperty('request') && ['http-request', 'graphql-request'].includes(item.type) && !item.items;
};
@@ -449,6 +502,44 @@ export const interpolateEnvironmentVars = (item, variables) => {
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) => {
if(item.type === 'http-request') {
return 'params';
diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js
index 068441001..1217fce25 100644
--- a/packages/bruno-electron/src/app/watcher.js
+++ b/packages/bruno-electron/src/app/watcher.js
@@ -10,7 +10,8 @@ const {
envJsonToBru,
} = require('@usebruno/bruno-lang');
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 dirname = path.dirname(pathname);
@@ -28,7 +29,7 @@ const isBruEnvironmentConfig = (pathname, collectionPath) => {
};
const hydrateRequestWithUuid = (request, pathname) => {
- request.uid = generateUidBasedOnHash(pathname);
+ request.uid = getRequestUid(pathname);
const params = _.get(request, 'request.params', []);
const headers = _.get(request, 'request.headers', []);
@@ -57,7 +58,7 @@ const addEnvironmentFile = async (win, pathname, collectionUid) => {
const bruContent = fs.readFileSync(pathname, 'utf8');
file.data = bruToEnvJson(bruContent);
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());
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');
file.data = bruToEnvJson(bruContent);
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());
// we are reusing the addEnvironmentFile event itself
@@ -101,7 +102,7 @@ const unlinkEnvironmentFile = async (win, pathname, collectionUid) => {
name: path.basename(pathname),
},
data: {
- uid: generateUidBasedOnHash(pathname),
+ uid: getRequestUid(pathname),
name: path.basename(pathname).substring(0, path.basename(pathname).length - 4),
}
};
diff --git a/packages/bruno-electron/src/cache/requestUids.js b/packages/bruno-electron/src/cache/requestUids.js
new file mode 100644
index 000000000..0a166b41a
--- /dev/null
+++ b/packages/bruno-electron/src/cache/requestUids.js
@@ -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
+};
diff --git a/packages/bruno-electron/src/ipc/local-collection.js b/packages/bruno-electron/src/ipc/local-collection.js
index 918d7aa0c..2bc9304f4 100644
--- a/packages/bruno-electron/src/ipc/local-collection.js
+++ b/packages/bruno-electron/src/ipc/local-collection.js
@@ -13,11 +13,13 @@ const {
hasBruExtension,
isDirectory,
browseDirectory,
- createDirectory
+ createDirectory,
+ searchForBruFiles
} = require('../utils/filesystem');
-const { uuid, stringifyJson } = require('../utils/common');
+const { stringifyJson } = require('../utils/common');
const { openCollectionDialog, openCollection } = require('../app/collections');
const { generateUidBasedOnHash } = require('../utils/common');
+const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids');
const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
// browse directory
@@ -179,6 +181,12 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// if its directory, rename and return
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);
}
@@ -193,6 +201,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
jsonData.name = newName;
+ moveRequestUid(oldPath, newPath);
+
const content = jsonToBru(jsonData);
await writeFile(newPath, content);
await fs.unlinkSync(oldPath);
@@ -218,9 +228,25 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:delete-item', async (event, pathname, type) => {
try {
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)) {
- await fs.unlinkSync(pathname);
+ if(!fs.existsSync(pathname)) {
+ return Promise.reject(new Error('The file does not exist'));
+ }
+
+ deleteRequestUid(pathname);
+
+ fs.unlinkSync(pathname);
} else {
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) => {
// reload last opened collections
const lastOpened = lastOpenedCollections.getAll();
diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js
index 322ff2e00..33e64ee31 100644
--- a/packages/bruno-electron/src/utils/filesystem.js
+++ b/packages/bruno-electron/src/utils/filesystem.js
@@ -95,6 +95,25 @@ const browseDirectory = async (win) => {
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 = {
isValidPathname,
exists,
@@ -106,5 +125,7 @@ module.exports = {
hasJsonExtension,
hasBruExtension,
createDirectory,
- browseDirectory
+ browseDirectory,
+ searchForFiles,
+ searchForBruFiles
};
diff --git a/packages/bruno-lang/src/index.js b/packages/bruno-lang/src/index.js
index 2258262e7..12217d30f 100644
--- a/packages/bruno-lang/src/index.js
+++ b/packages/bruno-lang/src/index.js
@@ -44,6 +44,7 @@ const bruToJson = (fileContents) => {
const json = {
type: parsed.type || '',
name: parsed.name || '',
+ seq: parsed.seq || 1,
request: {
method: parsed.method || '',
url: parsed.url || '',
@@ -78,6 +79,7 @@ const jsonToBru = (json) => {
const {
type,
name,
+ seq,
request: {
method,
url,
@@ -92,6 +94,7 @@ method ${method}
url ${url}
type ${type}
body-mode ${body ? body.mode : 'none'}
+seq ${seq ? seq : 1}
`;
if(params && params.length) {
diff --git a/packages/bruno-lang/src/inline-tag.js b/packages/bruno-lang/src/inline-tag.js
index a16f3338d..03391e4c9 100644
--- a/packages/bruno-lang/src/inline-tag.js
+++ b/packages/bruno-lang/src/inline-tag.js
@@ -17,6 +17,7 @@ const inlineTag = sequenceOf([
str('name'),
str('method'),
str('url'),
+ str('seq'),
str('body-mode')
]),
whitespace,
diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js
index 22494d52b..2b3eb9986 100644
--- a/packages/bruno-schema/src/collections/index.js
+++ b/packages/bruno-schema/src/collections/index.js
@@ -58,6 +58,7 @@ const requestSchema = Yup.object({
const itemSchema = Yup.object({
uid: uidSchema,
type: Yup.string().oneOf(['http-request', 'graphql-request', 'folder']).required('type is required'),
+ seq: Yup.number().min(1),
name: Yup.string()
.min(1, 'name must be atleast 1 characters')
.max(50, 'name must be 100 characters or less')
@@ -69,7 +70,7 @@ const itemSchema = Yup.object({
items: Yup.lazy(() => Yup.array().of(itemSchema)),
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()
-}).noUnknown(true).strict();
+}).noUnknown(true);
const collectionSchema = Yup.object({
version: Yup.string().oneOf(['1']).required('version is required'),