feat: import collections

This commit is contained in:
Anoop M D 2022-10-15 04:04:45 +05:30
parent 6feca9937e
commit d546709b26
8 changed files with 133 additions and 23 deletions

View File

@ -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",

View File

@ -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>

View File

@ -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">

View File

@ -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';

View File

@ -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(),

View File

@ -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;

View 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;

View File

@ -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));