forked from extern/bruno
feat: local filesystem collections (resolves #22)
This commit is contained in:
parent
91981a48e4
commit
44aa019754
@ -5,6 +5,9 @@ npm i
|
|||||||
|
|
||||||
# run next app
|
# run next app
|
||||||
npm run dev --workspace=packages/bruno-app
|
npm run dev --workspace=packages/bruno-app
|
||||||
|
|
||||||
|
# run electron app
|
||||||
|
npm run dev --workspace=packages/bruno-electron
|
||||||
```
|
```
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
|
19459
package-lock.json
generated
19459
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
4652
packages/bruno-app/package-lock.json
generated
4652
packages/bruno-app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -29,8 +29,9 @@
|
|||||||
"immer": "^9.0.12",
|
"immer": "^9.0.12",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mousetrap": "^1.6.5",
|
"mousetrap": "^1.6.5",
|
||||||
"nanoid": "^3.1.30",
|
"nanoid": "3.3.4",
|
||||||
"next": "^12.1.0",
|
"next": "^12.1.0",
|
||||||
|
"path": "^0.12.7",
|
||||||
"qs": "^6.11.0",
|
"qs": "^6.11.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import useIdb from './useIdb';
|
import useIdb from './useIdb';
|
||||||
|
import useLocalCollectionTreeSync from './useLocalCollectionTreeSync';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { refreshScreenWidth } from 'providers/ReduxStore/slices/app';
|
import { refreshScreenWidth } from 'providers/ReduxStore/slices/app';
|
||||||
|
|
||||||
export const AppContext = React.createContext();
|
export const AppContext = React.createContext();
|
||||||
|
|
||||||
export const AppProvider = props => {
|
export const AppProvider = props => {
|
||||||
// boot idb
|
|
||||||
useIdb();
|
useIdb();
|
||||||
|
useLocalCollectionTreeSync();
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import {
|
||||||
|
removeCollectionEvent,
|
||||||
|
localCollectionAddDirectoryEvent,
|
||||||
|
localCollectionAddFileEvent,
|
||||||
|
localCollectionChangeFileEvent,
|
||||||
|
localCollectionUnlinkFileEvent,
|
||||||
|
localCollectionUnlinkDirectoryEvent
|
||||||
|
} from 'providers/ReduxStore/slices/collections';
|
||||||
|
import {
|
||||||
|
openLocalCollectionEvent
|
||||||
|
} from 'providers/ReduxStore/slices/collections/actions';
|
||||||
|
import { isElectron } from 'utils/common/platform';
|
||||||
|
|
||||||
|
const useLocalCollectionTreeSync = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(!isElectron()) {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ipcRenderer } = window;
|
||||||
|
|
||||||
|
const _openCollection = (pathname, uid) => {
|
||||||
|
console.log(`collection uid: ${uid}, pathname: ${pathname}`);
|
||||||
|
dispatch(openLocalCollectionEvent(uid, pathname));
|
||||||
|
};
|
||||||
|
|
||||||
|
const _collectionTreeUpdated = (type, val) => {
|
||||||
|
if(type === 'addDir') {
|
||||||
|
dispatch(localCollectionAddDirectoryEvent({
|
||||||
|
dir: val
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if(type === 'addFile') {
|
||||||
|
dispatch(localCollectionAddFileEvent({
|
||||||
|
file: val
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if(type === 'change') {
|
||||||
|
dispatch(localCollectionChangeFileEvent({
|
||||||
|
file: val
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if(type === 'unlink') {
|
||||||
|
setTimeout(() => {
|
||||||
|
dispatch(localCollectionUnlinkFileEvent({
|
||||||
|
file: val
|
||||||
|
}));
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
if(type === 'unlinkDir') {
|
||||||
|
dispatch(localCollectionUnlinkDirectoryEvent({
|
||||||
|
directory: val
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const _collectionRemoved = (pathname) => {
|
||||||
|
// dispatch(removeCollectionEvent({
|
||||||
|
// pathname
|
||||||
|
// }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeListener1 = ipcRenderer.on('main:collection-opened', _openCollection);
|
||||||
|
const removeListener2 = ipcRenderer.on('main:collection-tree-updated', _collectionTreeUpdated);
|
||||||
|
const removeListener3 = ipcRenderer.on('main:collection-removed', _collectionRemoved);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
removeListener1();
|
||||||
|
removeListener2();
|
||||||
|
removeListener3();
|
||||||
|
};
|
||||||
|
}, [isElectron]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useLocalCollectionTreeSync;
|
@ -1,5 +1,7 @@
|
|||||||
|
import path from 'path';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import each from 'lodash/each';
|
import each from 'lodash/each';
|
||||||
|
import trim from 'lodash/trim';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { uuid } from 'utils/common';
|
import { uuid } from 'utils/common';
|
||||||
import cloneDeep from 'lodash/cloneDeep';
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
@ -8,11 +10,13 @@ import {
|
|||||||
findCollectionByUid,
|
findCollectionByUid,
|
||||||
recursivelyGetAllItemUids,
|
recursivelyGetAllItemUids,
|
||||||
transformCollectionToSaveToIdb,
|
transformCollectionToSaveToIdb,
|
||||||
|
transformRequestToSaveToFilesystem,
|
||||||
deleteItemInCollection,
|
deleteItemInCollection,
|
||||||
findParentItemInCollection,
|
findParentItemInCollection,
|
||||||
isItemAFolder
|
isItemAFolder,
|
||||||
|
refreshUidsInItem
|
||||||
} from 'utils/collections';
|
} from 'utils/collections';
|
||||||
import { collectionSchema } from '@usebruno/schema';
|
import { collectionSchema, itemSchema } from '@usebruno/schema';
|
||||||
import { waitForNextTick } from 'utils/common';
|
import { waitForNextTick } from 'utils/common';
|
||||||
import cancelTokens, { saveCancelToken, deleteCancelToken } from 'utils/network/cancelTokens';
|
import cancelTokens, { saveCancelToken, deleteCancelToken } from 'utils/network/cancelTokens';
|
||||||
import { getCollectionsFromIdb, saveCollectionToIdb, deleteCollectionInIdb } from 'utils/idb';
|
import { getCollectionsFromIdb, saveCollectionToIdb, deleteCollectionInIdb } from 'utils/idb';
|
||||||
@ -35,6 +39,9 @@ import {
|
|||||||
|
|
||||||
import { closeTabs, addTab } from 'providers/ReduxStore/slices/tabs';
|
import { closeTabs, addTab } from 'providers/ReduxStore/slices/tabs';
|
||||||
import { addCollectionToWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
import { addCollectionToWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||||
|
import { isLocalCollection, resolveRequestFilename } from 'utils/common/platform';
|
||||||
|
|
||||||
|
const PATH_SEPARATOR = path.sep;
|
||||||
|
|
||||||
export const loadCollectionsFromIdb = () => (dispatch) => {
|
export const loadCollectionsFromIdb = () => (dispatch) => {
|
||||||
getCollectionsFromIdb(window.__idb)
|
getCollectionsFromIdb(window.__idb)
|
||||||
@ -44,6 +51,28 @@ export const loadCollectionsFromIdb = () => (dispatch) => {
|
|||||||
.catch(() => toast.error("Error occured while loading collections from IndexedDB"));
|
.catch(() => toast.error("Error occured while loading collections from IndexedDB"));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const openLocalCollectionEvent = (uid, pathname) => (dispatch, getState) => {
|
||||||
|
const localCollection = {
|
||||||
|
uid: uid,
|
||||||
|
name: path.basename(pathname),
|
||||||
|
pathname: pathname,
|
||||||
|
items: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = getState();
|
||||||
|
const { activeWorkspaceUid } = state.workspaces;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
collectionSchema
|
||||||
|
.validate(localCollection)
|
||||||
|
.then(() => dispatch(_createCollection(localCollection)))
|
||||||
|
.then(waitForNextTick)
|
||||||
|
.then(() => dispatch(addCollectionToWorkspace(activeWorkspaceUid, localCollection.uid)))
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const createCollection = (collectionName) => (dispatch, getState) => {
|
export const createCollection = (collectionName) => (dispatch, getState) => {
|
||||||
const newCollection = {
|
const newCollection = {
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
@ -148,6 +177,24 @@ export const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => {
|
|||||||
if(!collection) {
|
if(!collection) {
|
||||||
return reject(new Error('Collection not found'));
|
return reject(new Error('Collection not found'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(isLocalCollection(collection)) {
|
||||||
|
const item = findItemInCollection(collection, itemUid);
|
||||||
|
if(item) {
|
||||||
|
const itemToSave = transformRequestToSaveToFilesystem(item);
|
||||||
|
const { ipcRenderer } = window;
|
||||||
|
|
||||||
|
itemSchema
|
||||||
|
.validate(itemToSave)
|
||||||
|
.then(() => ipcRenderer.invoke('renderer:save-request', item.pathname, itemToSave))
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
} else {
|
||||||
|
reject(new Error("Not able to locate item"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const collectionCopy = cloneDeep(collection);
|
const collectionCopy = cloneDeep(collection);
|
||||||
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
|
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
|
||||||
|
|
||||||
@ -208,6 +255,43 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS
|
|||||||
if(!collection) {
|
if(!collection) {
|
||||||
return reject(new Error('Collection not found'));
|
return reject(new Error('Collection not found'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(isLocalCollection(collection)) {
|
||||||
|
if(!itemUid) {
|
||||||
|
const folderWithSameNameExists = find(collection.items, (i) => i.type === 'folder' && trim(i.name) === trim(folderName));
|
||||||
|
if(!folderWithSameNameExists) {
|
||||||
|
const fullName = `${collection.pathname}${PATH_SEPARATOR}${folderName}`;
|
||||||
|
const { ipcRenderer } = window;
|
||||||
|
|
||||||
|
ipcRenderer
|
||||||
|
.invoke('renderer:new-folder', fullName)
|
||||||
|
.then(() => resolve())
|
||||||
|
.catch((error) => reject(error));
|
||||||
|
} else {
|
||||||
|
return reject(new Error("folder with same name already exists"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const currentItem = findItemInCollection(collection, itemUid);
|
||||||
|
if(currentItem) {
|
||||||
|
const folderWithSameNameExists = find(currentItem.items, (i) => i.type === 'folder' && trim(i.name) === trim(folderName));
|
||||||
|
if(!folderWithSameNameExists) {
|
||||||
|
const fullName = `${currentItem.pathname}${PATH_SEPARATOR}${folderName}`;
|
||||||
|
const { ipcRenderer } = window;
|
||||||
|
|
||||||
|
ipcRenderer
|
||||||
|
.invoke('renderer:new-folder', fullName)
|
||||||
|
.then(() => resolve())
|
||||||
|
.catch((error) => reject(error));
|
||||||
|
} else {
|
||||||
|
return reject(new Error("folder with same name already exists"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return reject(new Error("unable to find parent folder"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const collectionCopy = cloneDeep(collection);
|
const collectionCopy = cloneDeep(collection);
|
||||||
const item = {
|
const item = {
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
@ -250,8 +334,32 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
|
|||||||
return reject(new Error('Collection not found'));
|
return reject(new Error('Collection not found'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const item = findItemInCollection(collection, itemUid);
|
||||||
|
if(!item) {
|
||||||
|
return reject(new Error("Unable to locate item"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isLocalCollection(collection)) {
|
||||||
|
const dirname = path.dirname(item.pathname);
|
||||||
|
|
||||||
|
let newPathname = '';
|
||||||
|
if(item.type === 'folder') {
|
||||||
|
newPathname = `${dirname}${PATH_SEPARATOR}${trim(newName)}`;
|
||||||
|
} else {
|
||||||
|
const filename = resolveRequestFilename(newName);
|
||||||
|
newPathname = `${dirname}${PATH_SEPARATOR}${filename}`;
|
||||||
|
}
|
||||||
|
const { ipcRenderer } = window;
|
||||||
|
|
||||||
|
ipcRenderer
|
||||||
|
.invoke('renderer:rename-item', item.pathname, newPathname, newName)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const collectionCopy = cloneDeep(collection);
|
const collectionCopy = cloneDeep(collection);
|
||||||
const item = findItemInCollection(collectionCopy, itemUid);
|
|
||||||
if(item) {
|
if(item) {
|
||||||
item.name = newName;
|
item.name = newName;
|
||||||
}
|
}
|
||||||
@ -280,18 +388,56 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
|
|||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if(!collection) {
|
if(!collection) {
|
||||||
return reject(new Error('Collection not found'));
|
throw new Error('Collection not found');
|
||||||
}
|
}
|
||||||
const collectionCopy = cloneDeep(collection);
|
const collectionCopy = cloneDeep(collection);
|
||||||
const item = findItemInCollection(collectionCopy, itemUid);
|
const item = findItemInCollection(collectionCopy, itemUid);
|
||||||
if(!item) {
|
if(!item) {
|
||||||
return;
|
throw new Error('Unable to locate item');
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isItemAFolder(item)) {
|
if(isItemAFolder(item)) {
|
||||||
throw new Error('Cloning folders is not supported yet');
|
throw new Error('Cloning folders is not supported yet');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(isLocalCollection(collection)) {
|
||||||
|
const parentItem = findParentItemInCollection(collectionCopy, itemUid);
|
||||||
|
const filename = resolveRequestFilename(newName);
|
||||||
|
const itemToSave = refreshUidsInItem(transformRequestToSaveToFilesystem(item));
|
||||||
|
itemToSave.name = trim(newName);
|
||||||
|
if(!parentItem) {
|
||||||
|
const reqWithSameNameExists = find(collection.items, (i) => i.type !== 'folder' && trim(i.filename) === trim(filename));
|
||||||
|
if(!reqWithSameNameExists) {
|
||||||
|
const fullName = `${collection.pathname}${PATH_SEPARATOR}${filename}`;
|
||||||
|
const { ipcRenderer } = window;
|
||||||
|
|
||||||
|
itemSchema
|
||||||
|
.validate(itemToSave)
|
||||||
|
.then(() => ipcRenderer.invoke('renderer:new-request', fullName, itemToSave))
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
} else {
|
||||||
|
return reject(new Error(`${requestName} already exists in collection`));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const reqWithSameNameExists = find(parentItem.items, (i) => i.type !== 'folder' && trim(i.filename) === trim(filename));
|
||||||
|
if(!reqWithSameNameExists) {
|
||||||
|
const dirname = path.dirname(item.pathname);
|
||||||
|
const fullName = `${dirname}${PATH_SEPARATOR}${filename}`;
|
||||||
|
const { ipcRenderer } = window;
|
||||||
|
|
||||||
|
itemSchema
|
||||||
|
.validate(itemToSave)
|
||||||
|
.then(() => ipcRenderer.invoke('renderer:new-request', fullName, itemToSave))
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
} else {
|
||||||
|
return reject(new Error(`${requestName} already exists in the folder`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
// todo: clone query params
|
// todo: clone query params
|
||||||
const clonedItem = cloneDeep(item);
|
const clonedItem = cloneDeep(item);
|
||||||
clonedItem.name = newName;
|
clonedItem.name = newName;
|
||||||
@ -328,7 +474,23 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
|
|||||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if(collection) {
|
if(!collection) {
|
||||||
|
return reject(new Error('Collection not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isLocalCollection(collection)) {
|
||||||
|
const item = findItemInCollection(collection, itemUid);
|
||||||
|
if(item) {
|
||||||
|
const { ipcRenderer } = window;
|
||||||
|
|
||||||
|
ipcRenderer
|
||||||
|
.invoke('renderer:delete-item', item.pathname, item.type)
|
||||||
|
.then(() => resolve())
|
||||||
|
.catch((error) => reject(error));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const collectionCopy = cloneDeep(collection);
|
const collectionCopy = cloneDeep(collection);
|
||||||
deleteItemInCollection(itemUid, collectionCopy);
|
deleteItemInCollection(itemUid, collectionCopy);
|
||||||
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
|
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
|
||||||
@ -344,7 +506,6 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
|
|||||||
})
|
})
|
||||||
.then(() => resolve())
|
.then(() => resolve())
|
||||||
.catch((error) => reject(error));
|
.catch((error) => reject(error));
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -384,6 +545,42 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if(isLocalCollection(collection)) {
|
||||||
|
const filename = resolveRequestFilename(requestName);
|
||||||
|
if(!itemUid) {
|
||||||
|
const reqWithSameNameExists = find(collection.items, (i) => i.type !== 'folder' && trim(i.filename) === trim(filename));
|
||||||
|
if(!reqWithSameNameExists) {
|
||||||
|
const fullName = `${collection.pathname}${PATH_SEPARATOR}${filename}`;
|
||||||
|
const { ipcRenderer } = window;
|
||||||
|
|
||||||
|
ipcRenderer
|
||||||
|
.invoke('renderer:new-request', fullName, item)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
} else {
|
||||||
|
return reject(new Error(`${requestName} already exists in collection`));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const currentItem = findItemInCollection(collection, itemUid);
|
||||||
|
if(currentItem) {
|
||||||
|
const reqWithSameNameExists = find(currentItem.items, (i) => i.type !== 'folder' && trim(i.filename) === trim(filename));
|
||||||
|
if(!reqWithSameNameExists) {
|
||||||
|
const fullName = `${currentItem.pathname}${PATH_SEPARATOR}${filename}`;
|
||||||
|
const { ipcRenderer } = window;
|
||||||
|
|
||||||
|
ipcRenderer
|
||||||
|
.invoke('renderer:new-request', fullName, item)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
} else {
|
||||||
|
return reject(new Error(`${requestName} already exists in the folder`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
if(!itemUid) {
|
if(!itemUid) {
|
||||||
collectionCopy.items.push(item);
|
collectionCopy.items.push(item);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import path from 'path';
|
||||||
import { uuid } from 'utils/common';
|
import { uuid } from 'utils/common';
|
||||||
import find from 'lodash/find';
|
import find from 'lodash/find';
|
||||||
import concat from 'lodash/concat';
|
import concat from 'lodash/concat';
|
||||||
@ -9,14 +10,16 @@ import splitOnFirst from 'split-on-first';
|
|||||||
import {
|
import {
|
||||||
findCollectionByUid,
|
findCollectionByUid,
|
||||||
findItemInCollection,
|
findItemInCollection,
|
||||||
|
findItemInCollectionByPathname,
|
||||||
addDepth,
|
addDepth,
|
||||||
collapseCollection,
|
collapseCollection,
|
||||||
deleteItemInCollection,
|
deleteItemInCollection,
|
||||||
isItemARequest
|
isItemARequest
|
||||||
} from 'utils/collections';
|
} from 'utils/collections';
|
||||||
import { parseQueryParams, stringifyQueryParams } from 'utils/url';
|
import { parseQueryParams, stringifyQueryParams } from 'utils/url';
|
||||||
|
import { getSubdirectoriesFromRoot } from 'utils/common/platform';
|
||||||
|
|
||||||
// todo: errors should be tracked in each slice and displayed as toasts
|
const PATH_SEPARATOR = path.sep;
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
collections: []
|
collections: []
|
||||||
@ -538,6 +541,131 @@ export const collectionsSlice = createSlice({
|
|||||||
item.draft.request.method = action.payload.method;
|
item.draft.request.method = action.payload.method;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
localCollectionAddFileEvent: (state, action) => {
|
||||||
|
const file = action.payload.file;
|
||||||
|
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
|
||||||
|
|
||||||
|
if(collection) {
|
||||||
|
const dirname = path.dirname(file.meta.pathname);
|
||||||
|
const subDirectories = getSubdirectoriesFromRoot(collection.pathname, dirname);
|
||||||
|
let currentPath = collection.pathname;
|
||||||
|
let currentSubItems = collection.items;
|
||||||
|
for (const directoryName of subDirectories) {
|
||||||
|
let childItem = currentSubItems.find(f => f.type === 'folder' && f.name === directoryName)
|
||||||
|
if (!childItem) {
|
||||||
|
childItem = {
|
||||||
|
uid: uuid(),
|
||||||
|
pathname: `${currentPath}${PATH_SEPARATOR}${directoryName}`,
|
||||||
|
name: directoryName,
|
||||||
|
collapsed: false,
|
||||||
|
type: 'folder',
|
||||||
|
items: []
|
||||||
|
};
|
||||||
|
currentSubItems.push(childItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPath = `${currentPath}${PATH_SEPARATOR}${directoryName}`;
|
||||||
|
currentSubItems = childItem.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSubItems.find(f => f.name === file.meta.name)) {
|
||||||
|
// this happens when you rename a file
|
||||||
|
// the add event might get triggered first, before the unlink event
|
||||||
|
// this results in duplicate uids causing react renderer to go mad
|
||||||
|
const currentItem = find(currentSubItems, (i) => i.uid === file.data.uid);
|
||||||
|
if(currentItem) {
|
||||||
|
currentItem.name = file.data.name;
|
||||||
|
currentItem.type = file.data.type;
|
||||||
|
currentItem.request = file.data.request;
|
||||||
|
currentItem.filename = file.meta.name;
|
||||||
|
currentItem.pathname = file.meta.pathname;
|
||||||
|
currentItem.draft = null;
|
||||||
|
} else {
|
||||||
|
currentSubItems.push({
|
||||||
|
uid: file.data.uid,
|
||||||
|
name: file.data.name,
|
||||||
|
type: file.data.type,
|
||||||
|
request: file.data.request,
|
||||||
|
filename: file.meta.name,
|
||||||
|
pathname: file.meta.pathname,
|
||||||
|
draft: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addDepth(collection.items);
|
||||||
|
// sortItems(collection);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
localCollectionAddDirectoryEvent: (state, action) => {
|
||||||
|
const { dir } = action.payload;
|
||||||
|
const collection = findCollectionByUid(state.collections, dir.meta.collectionUid);
|
||||||
|
|
||||||
|
if(collection) {
|
||||||
|
const subDirectories = getSubdirectoriesFromRoot(collection.pathname, dir.meta.pathname);
|
||||||
|
let currentPath = collection.pathname;
|
||||||
|
let currentSubItems = collection.items;
|
||||||
|
for (const directoryName of subDirectories) {
|
||||||
|
let childItem = currentSubItems.find(f => f.type === 'folder' && f.name === directoryName);
|
||||||
|
if (!childItem) {
|
||||||
|
childItem = {
|
||||||
|
uid: uuid(),
|
||||||
|
pathname: `${currentPath}${PATH_SEPARATOR}${directoryName}`,
|
||||||
|
name: directoryName,
|
||||||
|
collapsed: false,
|
||||||
|
type: 'folder',
|
||||||
|
items: []
|
||||||
|
};
|
||||||
|
currentSubItems.push(childItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPath = `${currentPath}${PATH_SEPARATOR}${directoryName}`;
|
||||||
|
currentSubItems = childItem.items;
|
||||||
|
}
|
||||||
|
addDepth(collection.items);
|
||||||
|
// sortItems(collection);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
localCollectionChangeFileEvent: (state, action) => {
|
||||||
|
const { file } = action.payload;
|
||||||
|
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
|
||||||
|
|
||||||
|
if(collection) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
localCollectionUnlinkFileEvent: (state, action) => {
|
||||||
|
const { file } = action.payload;
|
||||||
|
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
|
||||||
|
|
||||||
|
if(collection) {
|
||||||
|
const item = findItemInCollectionByPathname(collection, file.meta.pathname);
|
||||||
|
|
||||||
|
if(item) {
|
||||||
|
deleteItemInCollection(item.uid, collection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
localCollectionUnlinkDirectoryEvent: (state, action) => {
|
||||||
|
const { directory } = action.payload;
|
||||||
|
const collection = findCollectionByUid(state.collections, directory.meta.collectionUid);
|
||||||
|
|
||||||
|
if(collection) {
|
||||||
|
const item = findItemInCollectionByPathname(collection, directory.meta.pathname);
|
||||||
|
|
||||||
|
if(item) {
|
||||||
|
deleteItemInCollection(item.uid, collection);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -574,7 +702,12 @@ export const {
|
|||||||
deleteMultipartFormParam,
|
deleteMultipartFormParam,
|
||||||
updateRequestBodyMode,
|
updateRequestBodyMode,
|
||||||
updateRequestBody,
|
updateRequestBody,
|
||||||
updateRequestMethod
|
updateRequestMethod,
|
||||||
|
localCollectionAddFileEvent,
|
||||||
|
localCollectionAddDirectoryEvent,
|
||||||
|
localCollectionChangeFileEvent,
|
||||||
|
localCollectionUnlinkFileEvent,
|
||||||
|
localCollectionUnlinkDirectoryEvent
|
||||||
} = collectionsSlice.actions;
|
} = collectionsSlice.actions;
|
||||||
|
|
||||||
export default collectionsSlice.reducer;
|
export default collectionsSlice.reducer;
|
||||||
|
@ -134,9 +134,9 @@ export const addCollectionToWorkspace = (workspaceUid, collectionUid) => (dispat
|
|||||||
const workspaceCopy = cloneDeep(workspace);
|
const workspaceCopy = cloneDeep(workspace);
|
||||||
if(workspaceCopy.collections && workspace.collections.length) {
|
if(workspaceCopy.collections && workspace.collections.length) {
|
||||||
if(!findCollectionInWorkspace(workspace, collectionUid)) {
|
if(!findCollectionInWorkspace(workspace, collectionUid)) {
|
||||||
workspaceCopy.collections.push([{
|
workspaceCopy.collections.push({
|
||||||
uid: collectionUid
|
uid: collectionUid
|
||||||
}]);
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
workspaceCopy.collections = [{
|
workspaceCopy.collections = [{
|
||||||
|
@ -46,9 +46,9 @@ export const workspacesSlice = createSlice({
|
|||||||
if(workspace) {
|
if(workspace) {
|
||||||
if(workspace.collections && workspace.collections.length) {
|
if(workspace.collections && workspace.collections.length) {
|
||||||
if(!findCollectionInWorkspace(workspace, collectionUid)) {
|
if(!findCollectionInWorkspace(workspace, collectionUid)) {
|
||||||
workspace.collections.push([{
|
workspace.collections.push({
|
||||||
uid: collectionUid
|
uid: collectionUid
|
||||||
}]);
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
workspace.collections = [{
|
workspace.collections = [{
|
||||||
|
@ -48,10 +48,10 @@ const updateUidsInCollection = (collection) => {
|
|||||||
each(items, (item) => {
|
each(items, (item) => {
|
||||||
item.uid = uuid();
|
item.uid = uuid();
|
||||||
|
|
||||||
each(get(item, 'headers'), (header) => header.uid = uuid());
|
each(get(item, 'request.headers'), (header) => header.uid = uuid());
|
||||||
each(get(item, 'params'), (param) => param.uid = uuid());
|
each(get(item, 'request.params'), (param) => param.uid = uuid());
|
||||||
each(get(item, 'body.multipartForm'), (param) => param.uid = uuid());
|
each(get(item, 'request.body.multipartForm'), (param) => param.uid = uuid());
|
||||||
each(get(item, 'body.formUrlEncoded'), (param) => param.uid = uuid());
|
each(get(item, 'request.body.formUrlEncoded'), (param) => param.uid = uuid());
|
||||||
|
|
||||||
if(item.items && item.items.length) {
|
if(item.items && item.items.length) {
|
||||||
updateItemUids(item.items);
|
updateItemUids(item.items);
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
|
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 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';
|
||||||
import sortBy from 'lodash/sortBy';
|
import sortBy from 'lodash/sortBy';
|
||||||
|
import { uuid } from 'utils/common';
|
||||||
|
|
||||||
const replaceTabsWithSpaces = (str, numSpaces = 2) => {
|
const replaceTabsWithSpaces = (str, numSpaces = 2) => {
|
||||||
if(!str || !str.length || !isString(str)) {
|
if(!str || !str.length || !isString(str)) {
|
||||||
@ -77,11 +79,20 @@ export const findItem = (items = [], itemUid) => {
|
|||||||
return find(items, (i) => i.uid === itemUid);
|
return find(items, (i) => i.uid === itemUid);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const findCollectionByUid = (collections, collectionUid) => {
|
export const findCollectionByUid = (collections, collectionUid) => {
|
||||||
return find(collections, (c) => c.uid === collectionUid);
|
return find(collections, (c) => c.uid === collectionUid);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const findItemByPathname = (items = [], pathname) => {
|
||||||
|
return find(items, (i) => i.pathname === pathname);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findItemInCollectionByPathname = (collection, pathname) => {
|
||||||
|
let flattenedItems = flattenItems(collection.items);
|
||||||
|
|
||||||
|
return findItemByPathname(flattenedItems, pathname);
|
||||||
|
}
|
||||||
|
|
||||||
export const findItemInCollection = (collection, itemUid) => {
|
export const findItemInCollection = (collection, itemUid) => {
|
||||||
let flattenedItems = flattenItems(collection.items);
|
let flattenedItems = flattenItems(collection.items);
|
||||||
|
|
||||||
@ -224,6 +235,48 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
|||||||
return collectionToSave;
|
return collectionToSave;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const transformRequestToSaveToFilesystem = (item) => {
|
||||||
|
const _item = item.draft ? item.draft : item;
|
||||||
|
const itemToSave = {
|
||||||
|
uid: _item.uid,
|
||||||
|
type: _item.type,
|
||||||
|
name: _item.name,
|
||||||
|
request: {
|
||||||
|
method: _item.request.method,
|
||||||
|
url: _item.request.url,
|
||||||
|
params: [],
|
||||||
|
headers: [],
|
||||||
|
body: _item.request.body
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
each(_item.request.params, (param) => {
|
||||||
|
itemToSave.request.params.push({
|
||||||
|
uid: param.uid,
|
||||||
|
name: param.name,
|
||||||
|
value: param.value,
|
||||||
|
description: param.description,
|
||||||
|
enabled: param.enabled
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
each(_item.request.headers, (header) => {
|
||||||
|
itemToSave.request.headers.push({
|
||||||
|
uid: header.uid,
|
||||||
|
name: header.name,
|
||||||
|
value: header.value,
|
||||||
|
description: header.description,
|
||||||
|
enabled: header.enabled,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if(itemToSave.request.body.mode === 'json') {
|
||||||
|
itemToSave.request.body.json = replaceTabsWithSpaces(itemToSave.request.body.json);
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemToSave;
|
||||||
|
};
|
||||||
|
|
||||||
// todo: optimize this
|
// todo: optimize this
|
||||||
export const deleteItemInCollection = (itemUid, collection) => {
|
export const deleteItemInCollection = (itemUid, collection) => {
|
||||||
collection.items = filter(collection.items, (i) => i.uid !== itemUid);
|
collection.items = filter(collection.items, (i) => i.uid !== itemUid);
|
||||||
@ -274,3 +327,14 @@ export const humanizeRequestBodyMode = (mode) => {
|
|||||||
|
|
||||||
return label;
|
return label;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const refreshUidsInItem = (item) => {
|
||||||
|
item.uid = uuid();
|
||||||
|
|
||||||
|
each(get(item, 'request.headers'), (header) => header.uid = uuid());
|
||||||
|
each(get(item, 'request.params'), (param) => param.uid = uuid());
|
||||||
|
each(get(item, 'request.body.multipartForm'), (param) => param.uid = uuid());
|
||||||
|
each(get(item, 'request.body.formUrlEncoded'), (param) => param.uid = uuid());
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
27
packages/bruno-app/src/utils/common/platform.js
Normal file
27
packages/bruno-app/src/utils/common/platform.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import trim from 'lodash/trim';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export const isElectron = () => {
|
||||||
|
if(!window) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.ipcRenderer ? true : false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isLocalCollection = (collection) => {
|
||||||
|
return collection.pathname ? true : false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveRequestFilename = (name) => {
|
||||||
|
return `${trim(name)}.json`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSubdirectoriesFromRoot = (rootPath, pathname) => {
|
||||||
|
if (!path.isAbsolute(pathname)) {
|
||||||
|
throw new Error('Invalid path!');
|
||||||
|
}
|
||||||
|
const relativePath = path.relative(rootPath, pathname);
|
||||||
|
return relativePath ? relativePath.split(path.sep) : [];
|
||||||
|
};
|
||||||
|
|
6006
packages/bruno-electron/package-lock.json
generated
6006
packages/bruno-electron/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -18,10 +18,15 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.26.0",
|
"axios": "^0.26.0",
|
||||||
|
"chokidar": "^3.5.3",
|
||||||
"electron-is-dev": "^2.0.0",
|
"electron-is-dev": "^2.0.0",
|
||||||
|
"electron-store": "^8.1.0",
|
||||||
"electron-util": "^0.17.2",
|
"electron-util": "^0.17.2",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"lodash": "^4.17.21"
|
"fs-extra": "^10.1.0",
|
||||||
|
"is-valid-path": "^0.1.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"nanoid": "3.3.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "^17.1.0",
|
"electron": "^17.1.0",
|
||||||
|
26
packages/bruno-electron/src/app/collections.js
Normal file
26
packages/bruno-electron/src/app/collections.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
const { uuid } = require('../utils/common');
|
||||||
|
const { dialog } = require('electron');
|
||||||
|
const { isDirectory, normalizeAndResolvePath } = require('../utils/filesystem');
|
||||||
|
|
||||||
|
const openCollection = async (win, watcher) => {
|
||||||
|
const { filePaths } = await dialog.showOpenDialog(win, {
|
||||||
|
properties: ['openDirectory', 'createDirectory']
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filePaths && filePaths[0]) {
|
||||||
|
const resolvedPath = normalizeAndResolvePath(filePaths[0]);
|
||||||
|
if (isDirectory(resolvedPath)) {
|
||||||
|
if(!watcher.hasWatcher(resolvedPath)) {
|
||||||
|
const uid = uuid();
|
||||||
|
win.webContents.send('main:collection-opened', resolvedPath, uid);
|
||||||
|
watcher.addWatcher(win, resolvedPath, uid);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`[ERROR] Cannot open unknown folder: "${resolvedPath}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
openCollection
|
||||||
|
};
|
@ -5,7 +5,7 @@ const template = [
|
|||||||
label: 'Collection',
|
label: 'Collection',
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Open Collection',
|
label: 'Open Local Collection',
|
||||||
click () {
|
click () {
|
||||||
ipcMain.emit('main:open-collection');
|
ipcMain.emit('main:open-collection');
|
||||||
}
|
}
|
134
packages/bruno-electron/src/app/watcher.js
Normal file
134
packages/bruno-electron/src/app/watcher.js
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
const chokidar = require('chokidar');
|
||||||
|
const { hasJsonExtension } = require('../utils/filesystem');
|
||||||
|
|
||||||
|
const add = async (win, pathname, collectionUid) => {
|
||||||
|
const isJson = hasJsonExtension(pathname);
|
||||||
|
console.log(`watcher add: ${pathname}`);
|
||||||
|
|
||||||
|
if(isJson) {
|
||||||
|
const file = {
|
||||||
|
meta: {
|
||||||
|
collectionUid,
|
||||||
|
pathname,
|
||||||
|
name: path.basename(pathname),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jsonData = fs.readFileSync(pathname, 'utf8');
|
||||||
|
file.data = JSON.parse(jsonData);
|
||||||
|
win.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addDirectory = (win, pathname, collectionUid) => {
|
||||||
|
console.log(`watcher addDirectory: ${pathname}`);
|
||||||
|
const directory = {
|
||||||
|
meta: {
|
||||||
|
collectionUid,
|
||||||
|
pathname,
|
||||||
|
name: path.basename(pathname),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
win.webContents.send('main:collection-tree-updated', 'addDir', directory);
|
||||||
|
};
|
||||||
|
|
||||||
|
const change = async (win, pathname, collectionUid) => {
|
||||||
|
console.log(`watcher change: ${pathname}`);
|
||||||
|
const file = {
|
||||||
|
meta: {
|
||||||
|
collectionUid,
|
||||||
|
pathname,
|
||||||
|
name: path.basename(pathname),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jsonData = fs.readFileSync(pathname, 'utf8');
|
||||||
|
file.data = await JSON.parse(jsonData);
|
||||||
|
win.webContents.send('main:collection-tree-updated', 'change', file);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const unlink = (win, pathname, collectionUid) => {
|
||||||
|
console.log(`watcher unlink: ${pathname}`);
|
||||||
|
const file = {
|
||||||
|
meta: {
|
||||||
|
collectionUid,
|
||||||
|
pathname,
|
||||||
|
name: path.basename(pathname)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
win.webContents.send('main:collection-tree-updated', 'unlink', file);
|
||||||
|
}
|
||||||
|
|
||||||
|
const unlinkDir = (win, pathname, collectionUid) => {
|
||||||
|
console.log(`watcher unlinkDir: ${pathname}`);
|
||||||
|
const directory = {
|
||||||
|
meta: {
|
||||||
|
collectionUid,
|
||||||
|
pathname,
|
||||||
|
name: path.basename(pathname)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
win.webContents.send('main:collection-tree-updated', 'unlinkDir', directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Watcher {
|
||||||
|
constructor () {
|
||||||
|
this.watchers = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
addWatcher (win, watchPath, collectionUid) {
|
||||||
|
if(this.watchers[watchPath]) {
|
||||||
|
this.watchers[watchPath].close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
setTimeout(() => {
|
||||||
|
const watcher = chokidar.watch(watchPath, {
|
||||||
|
ignoreInitial: false,
|
||||||
|
usePolling: false,
|
||||||
|
ignored: path => ["node_modules", ".git", "bruno.json"].some(s => path.includes(s)),
|
||||||
|
persistent: true,
|
||||||
|
ignorePermissionErrors: true,
|
||||||
|
awaitWriteFinish: {
|
||||||
|
stabilityThreshold: 80,
|
||||||
|
pollInterval: 10
|
||||||
|
},
|
||||||
|
depth: 20
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher
|
||||||
|
.on('add', pathname => add(win, pathname, collectionUid))
|
||||||
|
.on('addDir', pathname => addDirectory(win, pathname, collectionUid))
|
||||||
|
.on('change', pathname => change(win, pathname, collectionUid))
|
||||||
|
.on('unlink', pathname => unlink(win, pathname, collectionUid))
|
||||||
|
.on('unlinkDir', pathname => unlinkDir(win, pathname, collectionUid))
|
||||||
|
|
||||||
|
self.watchers[watchPath] = watcher;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasWatcher (watchPath) {
|
||||||
|
return this.watchers[watchPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
removeWatcher (watchPath, win) {
|
||||||
|
if(this.watchers[watchPath]) {
|
||||||
|
this.watchers[watchPath].close();
|
||||||
|
this.watchers[watchPath] = null;
|
||||||
|
win.webContents.send('main:collection-removed', watchPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = Watcher;
|
@ -4,8 +4,10 @@ const { format } = require('url');
|
|||||||
const { BrowserWindow, app, Menu } = require('electron');
|
const { BrowserWindow, app, Menu } = require('electron');
|
||||||
const { setContentSecurityPolicy } = require('electron-util');
|
const { setContentSecurityPolicy } = require('electron-util');
|
||||||
|
|
||||||
const menuTemplate = require('./menu-template');
|
const menuTemplate = require('./app/menu-template');
|
||||||
const registerIpc = require('./ipc');
|
const registerNetworkIpc = require('./ipc/network');
|
||||||
|
const registerLocalCollectionsIpc = require('./ipc/local-collection');
|
||||||
|
const Watcher = require('./app/watcher');
|
||||||
|
|
||||||
setContentSecurityPolicy(`
|
setContentSecurityPolicy(`
|
||||||
default-src * 'unsafe-inline' 'unsafe-eval';
|
default-src * 'unsafe-inline' 'unsafe-eval';
|
||||||
@ -20,6 +22,7 @@ const menu = Menu.buildFromTemplate(menuTemplate);
|
|||||||
Menu.setApplicationMenu(menu);
|
Menu.setApplicationMenu(menu);
|
||||||
|
|
||||||
let mainWindow;
|
let mainWindow;
|
||||||
|
let watcher;
|
||||||
|
|
||||||
// Prepare the renderer once the app is ready
|
// Prepare the renderer once the app is ready
|
||||||
app.on('ready', async () => {
|
app.on('ready', async () => {
|
||||||
@ -42,9 +45,11 @@ app.on('ready', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.loadURL(url);
|
mainWindow.loadURL(url);
|
||||||
|
watcher = new Watcher();
|
||||||
|
|
||||||
// register all ipc handlers
|
// register all ipc handlers
|
||||||
registerIpc(mainWindow);
|
registerNetworkIpc(mainWindow, watcher);
|
||||||
|
registerLocalCollectionsIpc(mainWindow, watcher);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Quit the app once all windows are closed
|
// Quit the app once all windows are closed
|
||||||
|
174
packages/bruno-electron/src/ipc/local-collection.js
Normal file
174
packages/bruno-electron/src/ipc/local-collection.js
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
const _ = require('lodash');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { ipcMain } = require('electron');
|
||||||
|
const {
|
||||||
|
isValidPathname,
|
||||||
|
writeFile,
|
||||||
|
hasJsonExtension,
|
||||||
|
isDirectory,
|
||||||
|
browseDirectory,
|
||||||
|
createDirectory
|
||||||
|
} = require('../utils/filesystem');
|
||||||
|
const { uuid, stringifyJson, parseJson } = require('../utils/common');
|
||||||
|
const { openCollection } = require('../app/collections');
|
||||||
|
|
||||||
|
const registerRendererEventHandlers = (mainWindow, watcher) => {
|
||||||
|
// browse directory
|
||||||
|
ipcMain.handle('renderer:browse-directory', async (event, pathname, request) => {
|
||||||
|
try {
|
||||||
|
const dirPath = await browseDirectory(mainWindow);
|
||||||
|
|
||||||
|
return dirPath;
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// create collection
|
||||||
|
ipcMain.handle('renderer:create-collection', async (event, collectionName, collectionLocation) => {
|
||||||
|
try {
|
||||||
|
const dirPath = path.join(collectionLocation, collectionName);
|
||||||
|
if (fs.existsSync(dirPath)){
|
||||||
|
throw new Error(`collection: ${dir} already exists`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isValidPathname(dirPath)) {
|
||||||
|
throw new Error(`collection: invaid pathname - ${dir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await createDirectory(dirPath);
|
||||||
|
|
||||||
|
const content = await stringifyJson({
|
||||||
|
version: '1.0',
|
||||||
|
type: 'collection'
|
||||||
|
});
|
||||||
|
await writeFile(path.join(dirPath, 'bruno.json'), content);
|
||||||
|
|
||||||
|
const uid = uuid();
|
||||||
|
mainWindow.webContents.send('main:collection-opened', dirPath, uid);
|
||||||
|
watcher.addWatcher(mainWindow, dirPath, uid);
|
||||||
|
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// new request
|
||||||
|
ipcMain.handle('renderer:new-request', async (event, pathname, request) => {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(pathname)){
|
||||||
|
throw new Error(`path: ${pathname} already exists`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await stringifyJson(request);
|
||||||
|
await writeFile(pathname, content);
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// save request
|
||||||
|
ipcMain.handle('renderer:save-request', async (event, pathname, request) => {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(pathname)){
|
||||||
|
throw new Error(`path: ${pathname} does not exist`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await stringifyJson(request);
|
||||||
|
await writeFile(pathname, content);
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// rename item
|
||||||
|
ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(oldPath)){
|
||||||
|
throw new Error(`path: ${oldPath} does not exist`);
|
||||||
|
}
|
||||||
|
if (fs.existsSync(newPath)){
|
||||||
|
throw new Error(`path: ${oldPath} already exists`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if its directory, rename and return
|
||||||
|
if(isDirectory(oldPath)) {
|
||||||
|
return fs.renameSync(oldPath, newPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isJson = hasJsonExtension(oldPath);
|
||||||
|
if(!isJson) {
|
||||||
|
throw new Error(`path: ${oldPath} is not a json file`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update name in file and save new copy, then delete old copy
|
||||||
|
const data = fs.readFileSync(oldPath, 'utf8');
|
||||||
|
const jsonData = await parseJson(data);
|
||||||
|
|
||||||
|
jsonData.name = newName;
|
||||||
|
|
||||||
|
const content = await stringifyJson(jsonData);
|
||||||
|
await writeFile(newPath, content);
|
||||||
|
await fs.unlinkSync(oldPath);
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// new folder
|
||||||
|
ipcMain.handle('renderer:new-folder', async (event, pathname) => {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(pathname)){
|
||||||
|
fs.mkdirSync(pathname);
|
||||||
|
} else {
|
||||||
|
return Promise.reject(new Error('The directory already exists'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// delete file/folder
|
||||||
|
ipcMain.handle('renderer:delete-item', async (event, pathname, type) => {
|
||||||
|
try {
|
||||||
|
if(type === 'folder') {
|
||||||
|
await fs.rmSync(pathname, { recursive: true, force: true});
|
||||||
|
} else if (['http-request', 'graphql-request'].includes(type)) {
|
||||||
|
await fs.unlinkSync(pathname);
|
||||||
|
} else {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('renderer:open-collection', () => {
|
||||||
|
if(watcher && mainWindow) {
|
||||||
|
openCollection(mainWindow, watcher);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('renderer:remove-collection', async (event, collectionPath) => {
|
||||||
|
if(watcher && mainWindow) {
|
||||||
|
watcher.removeWatcher(collectionPath, mainWindow);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerMainEventHandlers = (mainWindow, watcher) => {
|
||||||
|
ipcMain.on('main:open-collection', () => {
|
||||||
|
if(watcher && mainWindow) {
|
||||||
|
openCollection(mainWindow, watcher);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const registerLocalCollectionsIpc = (mainWindow, watcher) => {
|
||||||
|
registerRendererEventHandlers(mainWindow, watcher);
|
||||||
|
registerMainEventHandlers(mainWindow, watcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerLocalCollectionsIpc;
|
@ -3,8 +3,7 @@ const FormData = require('form-data');
|
|||||||
const { ipcMain } = require('electron');
|
const { ipcMain } = require('electron');
|
||||||
const { forOwn, extend } = require('lodash');
|
const { forOwn, extend } = require('lodash');
|
||||||
|
|
||||||
|
const registerNetworkIpc = () => {
|
||||||
const registerIpc = () => {
|
|
||||||
// handler for sending http request
|
// handler for sending http request
|
||||||
ipcMain.handle('send-http-request', async (event, request) => {
|
ipcMain.handle('send-http-request', async (event, request) => {
|
||||||
try {
|
try {
|
||||||
@ -44,4 +43,4 @@ const registerIpc = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = registerIpc;
|
module.exports = registerNetworkIpc;
|
32
packages/bruno-electron/src/utils/common.js
Normal file
32
packages/bruno-electron/src/utils/common.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
const { customAlphabet } = require('nanoid');
|
||||||
|
|
||||||
|
// a customized version of nanoid without using _ and -
|
||||||
|
const uuid = () => {
|
||||||
|
// https://github.com/ai/nanoid/blob/main/url-alphabet/index.js
|
||||||
|
const urlAlphabet = 'useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict';
|
||||||
|
const customNanoId = customAlphabet (urlAlphabet, 21);
|
||||||
|
|
||||||
|
return customNanoId();
|
||||||
|
};
|
||||||
|
|
||||||
|
const stringifyJson = async (str) => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(str, null, 2);
|
||||||
|
} catch(err) {
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseJson = async (obj) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(obj);
|
||||||
|
} catch(err) {
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
uuid,
|
||||||
|
stringifyJson,
|
||||||
|
parseJson
|
||||||
|
};
|
104
packages/bruno-electron/src/utils/filesystem.js
Normal file
104
packages/bruno-electron/src/utils/filesystem.js
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const fsPromises = require('fs/promises');
|
||||||
|
const { dialog } = require('electron');
|
||||||
|
const isValidPathname = require('is-valid-path');
|
||||||
|
|
||||||
|
const exists = async p => {
|
||||||
|
try {
|
||||||
|
await fsPromises.access(p);
|
||||||
|
return true;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSymbolicLink = filepath => {
|
||||||
|
try {
|
||||||
|
return fs.existsSync(filepath) && fs.lstatSync(filepath).isSymbolicLink();
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFile = filepath => {
|
||||||
|
try {
|
||||||
|
return fs.existsSync(filepath) && fs.lstatSync(filepath).isFile();
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDirectory = dirPath => {
|
||||||
|
try {
|
||||||
|
return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory();
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeAndResolvePath = pathname => {
|
||||||
|
if (isSymbolicLink(pathname)) {
|
||||||
|
const absPath = path.dirname(pathname);
|
||||||
|
const targetPath = path.resolve(absPath, fs.readlinkSync(pathname));
|
||||||
|
if (isFile(targetPath) || isDirectory(targetPath)) {
|
||||||
|
return path.resolve(targetPath);
|
||||||
|
}
|
||||||
|
console.error(`Cannot resolve link target "${pathname}" (${targetPath}).`)
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return path.resolve(pathname);
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeFile = async (pathname, content) => {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(pathname, content, {
|
||||||
|
encoding: "utf8"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasJsonExtension = filename => {
|
||||||
|
if (!filename || typeof filename !== 'string') return false
|
||||||
|
return ['json'].some(ext => filename.toLowerCase().endsWith(`.${ext}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
const createDirectory = async (dir) => {
|
||||||
|
if(!dir) {
|
||||||
|
throw new Error(`directory: path is null`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(dir)){
|
||||||
|
throw new Error(`directory: ${dir} already exists`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs.mkdirSync(dir);
|
||||||
|
};
|
||||||
|
|
||||||
|
const browseDirectory = async (win) => {
|
||||||
|
const { filePaths } = await dialog.showOpenDialog(win, {
|
||||||
|
properties: ['openDirectory', 'createDirectory']
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!filePaths || !filePaths[0]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedPath = normalizeAndResolvePath(filePaths[0]);
|
||||||
|
return isDirectory(resolvedPath) ? resolvedPath : false;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isValidPathname,
|
||||||
|
exists,
|
||||||
|
isSymbolicLink,
|
||||||
|
isFile,
|
||||||
|
isDirectory,
|
||||||
|
normalizeAndResolvePath,
|
||||||
|
writeFile,
|
||||||
|
hasJsonExtension,
|
||||||
|
createDirectory,
|
||||||
|
browseDirectory
|
||||||
|
};
|
@ -43,7 +43,9 @@ const itemSchema = Yup.object({
|
|||||||
is: (type) => ['http-request', 'graphql-request'].includes(type),
|
is: (type) => ['http-request', 'graphql-request'].includes(type),
|
||||||
then: (schema) => schema.required('request is required when item-type is request')
|
then: (schema) => schema.required('request is required when item-type is request')
|
||||||
}),
|
}),
|
||||||
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(),
|
||||||
|
pathname: Yup.string().max(1024, 'pathname cannot be more than 1024 characters').nullable()
|
||||||
}).noUnknown(true).strict();
|
}).noUnknown(true).strict();
|
||||||
|
|
||||||
const collectionSchema = Yup.object({
|
const collectionSchema = Yup.object({
|
||||||
@ -52,7 +54,8 @@ const collectionSchema = Yup.object({
|
|||||||
.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')
|
||||||
.required('name is required'),
|
.required('name is required'),
|
||||||
items: Yup.array().of(itemSchema)
|
items: Yup.array().of(itemSchema),
|
||||||
|
pathname: Yup.string().max(1024, 'pathname cannot be more than 1024 characters').nullable()
|
||||||
}).noUnknown(true).strict();
|
}).noUnknown(true).strict();
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
const { workspaceSchema } = require("./workspaces");
|
const { workspaceSchema } = require("./workspaces");
|
||||||
const { collectionSchema } = require("./collections");
|
const { collectionSchema, itemSchema } = require("./collections");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
itemSchema,
|
||||||
collectionSchema,
|
collectionSchema,
|
||||||
workspaceSchema
|
workspaceSchema
|
||||||
};
|
};
|
Loading…
Reference in New Issue
Block a user