mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-25 09:23:17 +01:00
feat: import collections
This commit is contained in:
parent
6feca9937e
commit
d546709b26
@ -19,6 +19,7 @@
|
|||||||
"codemirror": "^5.65.2",
|
"codemirror": "^5.65.2",
|
||||||
"codemirror-graphql": "^1.2.5",
|
"codemirror-graphql": "^1.2.5",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
|
"file-dialog": "^0.0.8",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
"graphiql": "^1.5.9",
|
"graphiql": "^1.5.9",
|
||||||
|
@ -3,12 +3,14 @@ import toast from 'react-hot-toast';
|
|||||||
import Dropdown from 'components/Dropdown';
|
import Dropdown from 'components/Dropdown';
|
||||||
import Bruno from 'components/Bruno';
|
import Bruno from 'components/Bruno';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import { collectionImported } from 'providers/ReduxStore/slices/collections';
|
||||||
import { createCollection } from 'providers/ReduxStore/slices/collections/actions';
|
import { createCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
import { addCollectionToWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
import { addCollectionToWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||||
import { showHomePage } from 'providers/ReduxStore/slices/app';
|
import { showHomePage } from 'providers/ReduxStore/slices/app';
|
||||||
import { IconDots } from '@tabler/icons';
|
import { IconDots } from '@tabler/icons';
|
||||||
import CreateCollection from '../CreateCollection';
|
import CreateCollection from '../CreateCollection';
|
||||||
import SelectCollection from 'components/Sidebar/Collections/SelectCollection';
|
import SelectCollection from 'components/Sidebar/Collections/SelectCollection';
|
||||||
|
import importCollection from 'utils/collections/import';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
const TitleBar = () => {
|
const TitleBar = () => {
|
||||||
@ -47,6 +49,15 @@ const TitleBar = () => {
|
|||||||
.catch(() => toast.error("An error occured while adding collection to workspace"));
|
.catch(() => toast.error("An error occured while adding collection to workspace"));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleImportCollection = () => {
|
||||||
|
importCollection()
|
||||||
|
.then((collection) => {
|
||||||
|
dispatch(collectionImported({collection: collection}));
|
||||||
|
dispatch(addCollectionToWorkspace(activeWorkspaceUid, collection.uid));
|
||||||
|
})
|
||||||
|
.catch((err) => console.log(err));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper className="px-2 py-2">
|
<StyledWrapper className="px-2 py-2">
|
||||||
{createCollectionModalOpen ? (
|
{createCollectionModalOpen ? (
|
||||||
@ -85,6 +96,7 @@ const TitleBar = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="dropdown-item" onClick={(e) => {
|
<div className="dropdown-item" onClick={(e) => {
|
||||||
menuDropdownTippyRef.current.hide();
|
menuDropdownTippyRef.current.hide();
|
||||||
|
handleImportCollection();
|
||||||
}}>
|
}}>
|
||||||
Import Collection
|
Import Collection
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,11 +11,13 @@ import {
|
|||||||
IconDeviceDesktop
|
IconDeviceDesktop
|
||||||
} from '@tabler/icons';
|
} from '@tabler/icons';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import { collectionImported } from 'providers/ReduxStore/slices/collections';
|
||||||
import { createCollection } from 'providers/ReduxStore/slices/collections/actions';
|
import { createCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
import { addCollectionToWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
import { addCollectionToWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||||
import Bruno from 'components/Bruno';
|
import Bruno from 'components/Bruno';
|
||||||
import CreateCollection from 'components/Sidebar/CreateCollection';
|
import CreateCollection from 'components/Sidebar/CreateCollection';
|
||||||
import SelectCollection from 'components/Sidebar/Collections/SelectCollection';
|
import SelectCollection from 'components/Sidebar/Collections/SelectCollection';
|
||||||
|
import importCollection from 'utils/collections/import';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
const Welcome = () => {
|
const Welcome = () => {
|
||||||
@ -42,6 +44,15 @@ const Welcome = () => {
|
|||||||
.catch(() => toast.error("An error occured while adding collection to workspace"));
|
.catch(() => toast.error("An error occured while adding collection to workspace"));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleImportCollection = () => {
|
||||||
|
importCollection()
|
||||||
|
.then((collection) => {
|
||||||
|
dispatch(collectionImported({collection: collection}));
|
||||||
|
dispatch(addCollectionToWorkspace(activeWorkspaceUid, collection.uid));
|
||||||
|
})
|
||||||
|
.catch((err) => console.log(err));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper className="pb-4 px-6 mt-6">
|
<StyledWrapper className="pb-4 px-6 mt-6">
|
||||||
{createCollectionModalOpen ? (
|
{createCollectionModalOpen ? (
|
||||||
@ -73,7 +84,7 @@ const Welcome = () => {
|
|||||||
<div className="flex items-center ml-6">
|
<div className="flex items-center ml-6">
|
||||||
<IconFiles size={18} strokeWidth={2}/><span className="label ml-2" onClick={() => setAddCollectionToWSModalOpen(true)}>Add Collection to Workspace</span>
|
<IconFiles size={18} strokeWidth={2}/><span className="label ml-2" onClick={() => setAddCollectionToWSModalOpen(true)}>Add Collection to Workspace</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center ml-6">
|
<div className="flex items-center ml-6" onClick={handleImportCollection}>
|
||||||
<IconUpload size={18} strokeWidth={2}/><span className="label ml-2">Import Collection</span>
|
<IconUpload size={18} strokeWidth={2}/><span className="label ml-2">Import Collection</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center ml-6">
|
<div className="flex items-center ml-6">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { openDB } from 'idb';
|
import { openDB } from 'idb';
|
||||||
import { idbConnectionReady } from 'providers/ReduxStore/slices/app'
|
import { idbConnectionReady } from 'providers/ReduxStore/slices/app'
|
||||||
import { loadCollectionsFromIdb } from 'providers/ReduxStore/slices/collections'
|
import { loadCollectionsFromIdb } from 'providers/ReduxStore/slices/collections/actions'
|
||||||
import { loadWorkspacesFromIdb } from 'providers/ReduxStore/slices/workspaces/actions'
|
import { loadWorkspacesFromIdb } from 'providers/ReduxStore/slices/workspaces/actions'
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
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';
|
||||||
import {
|
import {
|
||||||
@ -13,10 +14,11 @@ import {
|
|||||||
import { collectionSchema } from '@usebruno/schema';
|
import { collectionSchema } 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 { saveCollectionToIdb, deleteCollectionInIdb } from 'utils/idb';
|
import { getCollectionsFromIdb, saveCollectionToIdb, deleteCollectionInIdb } from 'utils/idb';
|
||||||
import { sendNetworkRequest } from 'utils/network';
|
import { sendNetworkRequest } from 'utils/network';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
loadCollections,
|
||||||
requestSent,
|
requestSent,
|
||||||
requestCancelled,
|
requestCancelled,
|
||||||
responseReceived,
|
responseReceived,
|
||||||
@ -33,6 +35,14 @@ 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';
|
||||||
|
|
||||||
|
export const loadCollectionsFromIdb = () => (dispatch) => {
|
||||||
|
getCollectionsFromIdb(window.__idb)
|
||||||
|
.then((collections) => dispatch(loadCollections({
|
||||||
|
collections: collections
|
||||||
|
})))
|
||||||
|
.catch(() => toast.error("Error occured while loading collections from IndexedDB"));
|
||||||
|
};
|
||||||
|
|
||||||
export const createCollection = (collectionName) => (dispatch, getState) => {
|
export const createCollection = (collectionName) => (dispatch, getState) => {
|
||||||
const newCollection = {
|
const newCollection = {
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
|
@ -15,7 +15,6 @@ import {
|
|||||||
isItemARequest
|
isItemARequest
|
||||||
} from 'utils/collections';
|
} from 'utils/collections';
|
||||||
import { parseQueryParams, stringifyQueryParams } from 'utils/url';
|
import { parseQueryParams, stringifyQueryParams } from 'utils/url';
|
||||||
import { getCollectionsFromIdb } from 'utils/idb';
|
|
||||||
|
|
||||||
// todo: errors should be tracked in each slice and displayed as toasts
|
// todo: errors should be tracked in each slice and displayed as toasts
|
||||||
|
|
||||||
@ -32,6 +31,12 @@ export const collectionsSlice = createSlice({
|
|||||||
each(action.payload.collections, (c) => addDepth(c.items));
|
each(action.payload.collections, (c) => addDepth(c.items));
|
||||||
state.collections = action.payload.collections;
|
state.collections = action.payload.collections;
|
||||||
},
|
},
|
||||||
|
collectionImported: (state, action) => {
|
||||||
|
const { collection } = action.payload;
|
||||||
|
collapseCollection(collection);
|
||||||
|
addDepth(collection.items);
|
||||||
|
state.collections.push(collection);
|
||||||
|
},
|
||||||
createCollection: (state, action) => {
|
createCollection: (state, action) => {
|
||||||
state.collections.push(action.payload);
|
state.collections.push(action.payload);
|
||||||
},
|
},
|
||||||
@ -538,6 +543,7 @@ export const collectionsSlice = createSlice({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
|
collectionImported,
|
||||||
createCollection,
|
createCollection,
|
||||||
renameCollection,
|
renameCollection,
|
||||||
deleteCollection,
|
deleteCollection,
|
||||||
@ -571,12 +577,4 @@ export const {
|
|||||||
updateRequestMethod
|
updateRequestMethod
|
||||||
} = collectionsSlice.actions;
|
} = collectionsSlice.actions;
|
||||||
|
|
||||||
export const loadCollectionsFromIdb = () => (dispatch) => {
|
|
||||||
getCollectionsFromIdb(window.__idb)
|
|
||||||
.then((collections) => dispatch(loadCollections({
|
|
||||||
collections: collections
|
|
||||||
})))
|
|
||||||
.catch((err) => console.log(err));
|
|
||||||
};
|
|
||||||
|
|
||||||
export default collectionsSlice.reducer;
|
export default collectionsSlice.reducer;
|
||||||
|
86
packages/bruno-app/src/utils/collections/import.js
Normal file
86
packages/bruno-app/src/utils/collections/import.js
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import each from 'lodash/each';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import fileDialog from 'file-dialog';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { uuid } from 'utils/common';
|
||||||
|
import { collectionSchema } from '@usebruno/schema';
|
||||||
|
import { saveCollectionToIdb } from 'utils/idb';
|
||||||
|
|
||||||
|
const readFile = (files) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
fileReader.onload = (e) => resolve(e.target.result);
|
||||||
|
fileReader.onerror = (err) => reject(err);
|
||||||
|
fileReader.readAsText(files[0]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseJsonCollection = (str) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
let parsed = JSON.parse(str);
|
||||||
|
return resolve(parsed);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error("Unable to parse the collection json file");
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateSchema = (collection = {}) => {
|
||||||
|
collection.uid = uuid();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
collectionSchema
|
||||||
|
.validate(collection)
|
||||||
|
.then(() => resolve(collection))
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error("The Collection file is corrupted");
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const updateUidsInCollection = (collection) => {
|
||||||
|
collection.uid = uuid();
|
||||||
|
|
||||||
|
const updateItemUids = (items = []) => {
|
||||||
|
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());
|
||||||
|
|
||||||
|
if(item.items && item.items.length) {
|
||||||
|
updateItemUids(item.items);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
updateItemUids(collection.items);
|
||||||
|
|
||||||
|
return collection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const importCollection = () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fileDialog({accept: 'application/json'})
|
||||||
|
.then(readFile)
|
||||||
|
.then(parseJsonCollection)
|
||||||
|
.then(validateSchema)
|
||||||
|
.then(updateUidsInCollection)
|
||||||
|
.then(validateSchema)
|
||||||
|
.then((collection) => saveCollectionToIdb(window.__idb, collection))
|
||||||
|
.then((collection) => {
|
||||||
|
toast.success("Collection imported successfully");
|
||||||
|
resolve(collection);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error("Import collection failed");
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default importCollection;
|
@ -1,5 +1,3 @@
|
|||||||
import isArray from 'lodash/isArray';
|
|
||||||
|
|
||||||
export const saveCollectionToIdb = (connection, collection) => {
|
export const saveCollectionToIdb = (connection, collection) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
connection
|
connection
|
||||||
@ -7,15 +5,9 @@ export const saveCollectionToIdb = (connection, collection) => {
|
|||||||
let tx = db.transaction(`collection`, 'readwrite');
|
let tx = db.transaction(`collection`, 'readwrite');
|
||||||
let collectionStore = tx.objectStore('collection');
|
let collectionStore = tx.objectStore('collection');
|
||||||
|
|
||||||
if(isArray(collection)) {
|
|
||||||
for(let c of collection) {
|
|
||||||
collectionStore.put(c);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
collectionStore.put(collection);
|
collectionStore.put(collection);
|
||||||
}
|
|
||||||
|
|
||||||
resolve();
|
resolve(collection);
|
||||||
})
|
})
|
||||||
.catch((err) => reject(err));
|
.catch((err) => reject(err));
|
||||||
});
|
});
|
||||||
@ -28,7 +20,7 @@ export const deleteCollectionInIdb = (connection, collectionUid) => {
|
|||||||
let tx = db.transaction(`collection`, 'readwrite');
|
let tx = db.transaction(`collection`, 'readwrite');
|
||||||
tx.objectStore('collection').delete(collectionUid);
|
tx.objectStore('collection').delete(collectionUid);
|
||||||
|
|
||||||
tx.oncomplete = () => resolve();
|
tx.oncomplete = () => resolve(collectionUid);
|
||||||
tx.onerror = () => reject(tx.error);
|
tx.onerror = () => reject(tx.error);
|
||||||
})
|
})
|
||||||
.catch((err) => reject(err));
|
.catch((err) => reject(err));
|
||||||
|
Loading…
Reference in New Issue
Block a user