mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-25 01:14:23 +01:00
feat: local filesystem collections (resolves #22)
This commit is contained in:
parent
91981a48e4
commit
44aa019754
@ -5,6 +5,9 @@ npm i
|
||||
|
||||
# run next app
|
||||
npm run dev --workspace=packages/bruno-app
|
||||
|
||||
# run electron app
|
||||
npm run dev --workspace=packages/bruno-electron
|
||||
```
|
||||
|
||||
# testing
|
||||
|
19449
package-lock.json
generated
19449
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",
|
||||
"lodash": "^4.17.21",
|
||||
"mousetrap": "^1.6.5",
|
||||
"nanoid": "^3.1.30",
|
||||
"nanoid": "3.3.4",
|
||||
"next": "^12.1.0",
|
||||
"path": "^0.12.7",
|
||||
"qs": "^6.11.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
@ -1,13 +1,14 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import useIdb from './useIdb';
|
||||
import useLocalCollectionTreeSync from './useLocalCollectionTreeSync';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { refreshScreenWidth } from 'providers/ReduxStore/slices/app';
|
||||
|
||||
export const AppContext = React.createContext();
|
||||
|
||||
export const AppProvider = props => {
|
||||
// boot idb
|
||||
useIdb();
|
||||
useLocalCollectionTreeSync();
|
||||
|
||||
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 each from 'lodash/each';
|
||||
import trim from 'lodash/trim';
|
||||
import toast from 'react-hot-toast';
|
||||
import { uuid } from 'utils/common';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
@ -8,11 +10,13 @@ import {
|
||||
findCollectionByUid,
|
||||
recursivelyGetAllItemUids,
|
||||
transformCollectionToSaveToIdb,
|
||||
transformRequestToSaveToFilesystem,
|
||||
deleteItemInCollection,
|
||||
findParentItemInCollection,
|
||||
isItemAFolder
|
||||
isItemAFolder,
|
||||
refreshUidsInItem
|
||||
} from 'utils/collections';
|
||||
import { collectionSchema } from '@usebruno/schema';
|
||||
import { collectionSchema, itemSchema } from '@usebruno/schema';
|
||||
import { waitForNextTick } from 'utils/common';
|
||||
import cancelTokens, { saveCancelToken, deleteCancelToken } from 'utils/network/cancelTokens';
|
||||
import { getCollectionsFromIdb, saveCollectionToIdb, deleteCollectionInIdb } from 'utils/idb';
|
||||
@ -35,6 +39,9 @@ import {
|
||||
|
||||
import { closeTabs, addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { addCollectionToWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { isLocalCollection, resolveRequestFilename } from 'utils/common/platform';
|
||||
|
||||
const PATH_SEPARATOR = path.sep;
|
||||
|
||||
export const loadCollectionsFromIdb = () => (dispatch) => {
|
||||
getCollectionsFromIdb(window.__idb)
|
||||
@ -44,6 +51,28 @@ export const loadCollectionsFromIdb = () => (dispatch) => {
|
||||
.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) => {
|
||||
const newCollection = {
|
||||
uid: uuid(),
|
||||
@ -148,6 +177,24 @@ export const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => {
|
||||
if(!collection) {
|
||||
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 collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
|
||||
|
||||
@ -208,6 +255,43 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS
|
||||
if(!collection) {
|
||||
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 item = {
|
||||
uid: uuid(),
|
||||
@ -250,8 +334,32 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
|
||||
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 item = findItemInCollection(collectionCopy, itemUid);
|
||||
if(item) {
|
||||
item.name = newName;
|
||||
}
|
||||
@ -280,18 +388,56 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if(!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
throw new Error('Collection not found');
|
||||
}
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const item = findItemInCollection(collectionCopy, itemUid);
|
||||
if(!item) {
|
||||
return;
|
||||
throw new Error('Unable to locate item');
|
||||
}
|
||||
|
||||
if(isItemAFolder(item)) {
|
||||
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
|
||||
const clonedItem = cloneDeep(item);
|
||||
clonedItem.name = newName;
|
||||
@ -328,23 +474,38 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if(collection) {
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
deleteItemInCollection(itemUid, collectionCopy);
|
||||
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
|
||||
|
||||
collectionSchema
|
||||
.validate(collectionToSave)
|
||||
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
|
||||
.then(() => {
|
||||
dispatch(_deleteItem({
|
||||
itemUid: itemUid,
|
||||
collectionUid: collectionUid
|
||||
}));
|
||||
})
|
||||
.then(() => resolve())
|
||||
.catch((error) => reject(error));
|
||||
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);
|
||||
deleteItemInCollection(itemUid, collectionCopy);
|
||||
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
|
||||
|
||||
collectionSchema
|
||||
.validate(collectionToSave)
|
||||
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
|
||||
.then(() => {
|
||||
dispatch(_deleteItem({
|
||||
itemUid: itemUid,
|
||||
collectionUid: collectionUid
|
||||
}));
|
||||
})
|
||||
.then(() => resolve())
|
||||
.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) {
|
||||
collectionCopy.items.push(item);
|
||||
} else {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import path from 'path';
|
||||
import { uuid } from 'utils/common';
|
||||
import find from 'lodash/find';
|
||||
import concat from 'lodash/concat';
|
||||
@ -9,14 +10,16 @@ import splitOnFirst from 'split-on-first';
|
||||
import {
|
||||
findCollectionByUid,
|
||||
findItemInCollection,
|
||||
findItemInCollectionByPathname,
|
||||
addDepth,
|
||||
collapseCollection,
|
||||
deleteItemInCollection,
|
||||
isItemARequest
|
||||
} from 'utils/collections';
|
||||
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 = {
|
||||
collections: []
|
||||
@ -538,6 +541,131 @@ export const collectionsSlice = createSlice({
|
||||
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,
|
||||
updateRequestBodyMode,
|
||||
updateRequestBody,
|
||||
updateRequestMethod
|
||||
updateRequestMethod,
|
||||
localCollectionAddFileEvent,
|
||||
localCollectionAddDirectoryEvent,
|
||||
localCollectionChangeFileEvent,
|
||||
localCollectionUnlinkFileEvent,
|
||||
localCollectionUnlinkDirectoryEvent
|
||||
} = collectionsSlice.actions;
|
||||
|
||||
export default collectionsSlice.reducer;
|
||||
|
@ -134,9 +134,9 @@ export const addCollectionToWorkspace = (workspaceUid, collectionUid) => (dispat
|
||||
const workspaceCopy = cloneDeep(workspace);
|
||||
if(workspaceCopy.collections && workspace.collections.length) {
|
||||
if(!findCollectionInWorkspace(workspace, collectionUid)) {
|
||||
workspaceCopy.collections.push([{
|
||||
workspaceCopy.collections.push({
|
||||
uid: collectionUid
|
||||
}]);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
workspaceCopy.collections = [{
|
||||
|
@ -46,9 +46,9 @@ export const workspacesSlice = createSlice({
|
||||
if(workspace) {
|
||||
if(workspace.collections && workspace.collections.length) {
|
||||
if(!findCollectionInWorkspace(workspace, collectionUid)) {
|
||||
workspace.collections.push([{
|
||||
workspace.collections.push({
|
||||
uid: collectionUid
|
||||
}]);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
workspace.collections = [{
|
||||
|
@ -48,10 +48,10 @@ const updateUidsInCollection = (collection) => {
|
||||
each(items, (item) => {
|
||||
item.uid = uuid();
|
||||
|
||||
each(get(item, 'headers'), (header) => header.uid = uuid());
|
||||
each(get(item, 'params'), (param) => param.uid = uuid());
|
||||
each(get(item, 'body.multipartForm'), (param) => param.uid = uuid());
|
||||
each(get(item, 'body.formUrlEncoded'), (param) => param.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());
|
||||
|
||||
if(item.items && item.items.length) {
|
||||
updateItemUids(item.items);
|
||||
|
@ -1,9 +1,11 @@
|
||||
import get from 'lodash/get';
|
||||
import each from 'lodash/each';
|
||||
import find from 'lodash/find';
|
||||
import isString from 'lodash/isString';
|
||||
import map from 'lodash/map';
|
||||
import filter from 'lodash/filter';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { uuid } from 'utils/common';
|
||||
|
||||
const replaceTabsWithSpaces = (str, numSpaces = 2) => {
|
||||
if(!str || !str.length || !isString(str)) {
|
||||
@ -77,11 +79,20 @@ export const findItem = (items = [], itemUid) => {
|
||||
return find(items, (i) => i.uid === itemUid);
|
||||
};
|
||||
|
||||
|
||||
export const findCollectionByUid = (collections, 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) => {
|
||||
let flattenedItems = flattenItems(collection.items);
|
||||
|
||||
@ -224,6 +235,48 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
||||
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
|
||||
export const deleteItemInCollection = (itemUid, collection) => {
|
||||
collection.items = filter(collection.items, (i) => i.uid !== itemUid);
|
||||
@ -274,3 +327,14 @@ export const humanizeRequestBodyMode = (mode) => {
|
||||
|
||||
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": {
|
||||
"axios": "^0.26.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron-is-dev": "^2.0.0",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-util": "^0.17.2",
|
||||
"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": {
|
||||
"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',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Open Collection',
|
||||
label: 'Open Local Collection',
|
||||
click () {
|
||||
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 { setContentSecurityPolicy } = require('electron-util');
|
||||
|
||||
const menuTemplate = require('./menu-template');
|
||||
const registerIpc = require('./ipc');
|
||||
const menuTemplate = require('./app/menu-template');
|
||||
const registerNetworkIpc = require('./ipc/network');
|
||||
const registerLocalCollectionsIpc = require('./ipc/local-collection');
|
||||
const Watcher = require('./app/watcher');
|
||||
|
||||
setContentSecurityPolicy(`
|
||||
default-src * 'unsafe-inline' 'unsafe-eval';
|
||||
@ -20,6 +22,7 @@ const menu = Menu.buildFromTemplate(menuTemplate);
|
||||
Menu.setApplicationMenu(menu);
|
||||
|
||||
let mainWindow;
|
||||
let watcher;
|
||||
|
||||
// Prepare the renderer once the app is ready
|
||||
app.on('ready', async () => {
|
||||
@ -42,9 +45,11 @@ app.on('ready', async () => {
|
||||
});
|
||||
|
||||
mainWindow.loadURL(url);
|
||||
watcher = new Watcher();
|
||||
|
||||
// register all ipc handlers
|
||||
registerIpc(mainWindow);
|
||||
registerNetworkIpc(mainWindow, watcher);
|
||||
registerLocalCollectionsIpc(mainWindow, watcher);
|
||||
});
|
||||
|
||||
// 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 { forOwn, extend } = require('lodash');
|
||||
|
||||
|
||||
const registerIpc = () => {
|
||||
const registerNetworkIpc = () => {
|
||||
// handler for sending http request
|
||||
ipcMain.handle('send-http-request', async (event, request) => {
|
||||
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),
|
||||
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();
|
||||
|
||||
const collectionSchema = Yup.object({
|
||||
@ -52,7 +54,8 @@ const collectionSchema = Yup.object({
|
||||
.min(1, 'name must be atleast 1 characters')
|
||||
.max(50, 'name must be 100 characters or less')
|
||||
.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();
|
||||
|
||||
module.exports = {
|
||||
|
@ -1,7 +1,8 @@
|
||||
const { workspaceSchema } = require("./workspaces");
|
||||
const { collectionSchema } = require("./collections");
|
||||
const { collectionSchema, itemSchema } = require("./collections");
|
||||
|
||||
module.exports = {
|
||||
itemSchema,
|
||||
collectionSchema,
|
||||
workspaceSchema
|
||||
};
|
Loading…
Reference in New Issue
Block a user