refactor: redux migration - save request

This commit is contained in:
Anoop M D 2022-03-18 18:38:49 +05:30
parent 038eb47dda
commit 7faff0339b
11 changed files with 160 additions and 166 deletions

View File

@ -9,7 +9,8 @@ import HttpRequestPane from 'components/HttpRequestPane';
import ResponsePane from 'components/ResponsePane';
import Welcome from 'components/Welcome';
import { findItemInCollection } from 'utils/collections';
import { sendRequest } from 'providers/ReduxStore/slices/collections';
import { sendRequest, requestUrlChanged } from 'providers/ReduxStore/slices/collections';
import { requestChanged } from 'providers/ReduxStore/slices/tabs';
import useGraphqlSchema from '../../hooks/useGraphqlSchema';
import StyledWrapper from './StyledWrapper';
@ -92,12 +93,15 @@ const RequestTabPanel = () => {
const item = findItemInCollection(collection, activeTabUid);
const onUrlChange = (value) => {
storeDispatch({
type: actions.REQUEST_URL_CHANGED,
url: value,
dispatch(requestChanged({
itemUid: item.uid,
collectionUid: collection ? collection.uid : null
});
collectionUid: collection.uid
}));
dispatch(requestUrlChanged({
itemUid: item.uid,
collectionUid: collection.uid,
url: value
}));
};
const runQuery = async () => {

View File

@ -1,20 +1,31 @@
import React, { useEffect } from 'react';
import find from 'lodash/find';
import Mousetrap from 'mousetrap';
import { useStore } from 'providers/Store';
import actions from 'providers/Store/actions';
import { saveRequest } from 'providers/ReduxStore/slices/collections';
import { requestSaved } from 'providers/ReduxStore/slices/tabs';
import { useSelector, useDispatch } from 'react-redux';
export const HotkeysContext = React.createContext();
export const HotkeysProvider = props => {
const [store, storeDispatch] = useStore();
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
useEffect(() => {
Mousetrap.bind(['command+s', 'ctrl+s'], (e) => {
console.log("Save hotkey");
storeDispatch({
type: actions.HOTKEY_SAVE
});
if(activeTabUid) {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
if(activeTab) {
// todo: these dispatches need to be chained and errors need to be handled
dispatch(saveRequest(activeTab.uid, activeTab.collectionUid));
dispatch(requestSaved({
itemUid: activeTab.uid
}))
}
}
return false; // this stops the event bubbling
});
@ -22,7 +33,7 @@ export const HotkeysProvider = props => {
return () => {
Mousetrap.unbind(['command+s', 'ctrl+s']);
};
}, []);
}, [activeTabUid, tabs, saveRequest, requestSaved]);
return (
<HotkeysContext.Provider {...props} value='hotkey'>

View File

@ -1,8 +1,9 @@
import { nanoid } from 'nanoid';
import cloneDeep from 'lodash/cloneDeep';
import { createSlice } from '@reduxjs/toolkit'
import { getCollectionsFromIdb, saveCollectionToIdb } from 'utils/idb';
import { sendNetworkRequest } from 'utils/network';
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
import { findCollectionByUid, findItemInCollection, cloneItem, transformCollectionToSaveToIdb } from 'utils/collections';
// todo: errors should be tracked in each slice and displayed as toasts
@ -41,12 +42,39 @@ export const collectionsSlice = createSlice({
}
}
},
_saveRequest: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if(collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if(item && item.draft) {
item.name = item.draft.name;
item.request = item.draft.request;
item.draft = null;
}
}
},
collectionClicked: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload);
if(collection) {
collection.collapsed = !collection.collapsed;
}
},
requestUrlChanged: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if(collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if(item) {
if(!item.draft) {
item.draft = cloneItem(item);
}
item.draft.request.url = action.payload.url;
}
}
}
}
});
@ -56,7 +84,9 @@ export const {
_createCollection,
_requestSent,
_responseReceived,
collectionClicked
_saveRequest,
collectionClicked,
requestUrlChanged,
} = collectionsSlice.actions;
export const loadCollectionsFromIdb = () => (dispatch) => {
@ -93,4 +123,23 @@ export const sendRequest = (item, collectionUid) => (dispatch) => {
.catch((err) => console.log(err));
};
export const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
if(collection) {
const collectionCopy = cloneDeep(collection);
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
saveCollectionToIdb(window.__idb, collectionToSave)
.then(() => {
dispatch(_saveRequest({
itemUid: itemUid,
collectionUid: collectionUid
}));
})
.catch((err) => console.log(err));
}
};
export default collectionsSlice.reducer;

View File

@ -1,3 +1,4 @@
import find from 'lodash/find';
import filter from 'lodash/filter';
import last from 'lodash/last';
import { createSlice } from '@reduxjs/toolkit'
@ -6,7 +7,8 @@ import { createSlice } from '@reduxjs/toolkit'
const initialState = {
tabs: [],
activeTabUid: null
activeTabUid: null,
hasChanges: false
};
export const tabsSlice = createSlice({
@ -32,14 +34,28 @@ export const tabsSlice = createSlice({
} else {
state.activeTabUid = null;
}
}
},
requestChanged: (state, action) => {
const tab = find(state.tabs, (t) => t.uid == action.payload.itemUid);
if(tab) {
tab.hasChanges = true;
}
},
requestSaved: (state, action) => {
const tab = find(state.tabs, (t) => t.uid == action.payload.itemUid);
if(tab) {
tab.hasChanges = false;
}
},
}
});
export const {
addTab,
focusTab,
closeTab
closeTab,
requestChanged,
requestSaved
} = tabsSlice.actions;
export default tabsSlice.reducer;

View File

@ -1,23 +1,15 @@
const SIDEBAR_COLLECTION_NEW_FOLDER = "SIDEBAR_COLLECTION_NEW_FOLDER";
const SIDEBAR_COLLECTION_NEW_REQUEST = "SIDEBAR_COLLECTION_NEW_REQUEST";
const LOAD_COLLECTIONS_FROM_IDB = "LOAD_COLLECTIONS_FROM_IDB";
const REQUEST_URL_CHANGED = "REQUEST_URL_CHANGED";
const REQUEST_GQL_QUERY_CHANGED = "REQUEST_GQL_QUERY_CHANGED";
const ADD_NEW_GQL_REQUEST = "ADD_NEW_GQL_REQUEST";
const IDB_CONNECTION_READY = "IDB_CONNECTION_READY";
const IDB_COLLECTIONS_SYNC_STARTED = "IDB_COLLECTIONS_SYNC_STARTED";
const IDB_COLLECTIONS_SYNC_ERROR = "IDB_COLLECTIONS_SYNC_ERROR";
const HOTKEY_SAVE = "HOTKEY_SAVE";
export default {
SIDEBAR_COLLECTION_NEW_FOLDER,
SIDEBAR_COLLECTION_NEW_REQUEST,
LOAD_COLLECTIONS_FROM_IDB,
REQUEST_URL_CHANGED,
REQUEST_GQL_QUERY_CHANGED,
ADD_NEW_GQL_REQUEST,
IDB_CONNECTION_READY,
IDB_COLLECTIONS_SYNC_STARTED,
IDB_COLLECTIONS_SYNC_ERROR,
HOTKEY_SAVE
};

View File

@ -1,9 +1,7 @@
import React, { useState, useEffect, useContext, useReducer, createContext } from 'react';
import React, {useContext, useReducer, createContext } from 'react';
import { nanoid } from 'nanoid';
import reducer from './reducer';
import useIdb from './useIdb';
import useLoadCollectionsFromIdb from './useLoadCollectionsFromIdb';
import useSyncCollectionsToIdb from './useSyncCollectionsToIdb';
export const StoreContext = createContext();
const collection = {
@ -121,15 +119,7 @@ const initialState = {
export const StoreProvider = props => {
const [state, dispatch] = useReducer(reducer, initialState);
const {
collections,
idbConnection,
collectionsToSyncToIdb
} = state;
useIdb(dispatch);
useLoadCollectionsFromIdb(idbConnection, dispatch);
useSyncCollectionsToIdb(collectionsToSyncToIdb, collections, idbConnection, dispatch);
return <StoreContext.Provider value={[state, dispatch]} {...props} />;
};

View File

@ -19,24 +19,6 @@ const reducer = (state, action) => {
});
}
case actions.IDB_COLLECTIONS_SYNC_STARTED: {
return produce(state, (draft) => {
draft.collectionsToSyncToIdb = [];
});
}
case actions.IDB_COLLECTIONS_SYNC_ERROR: {
return produce(state, (draft) => {
draft.collectionsToSyncToIdb = union(draft.collectionsToSyncToIdb, action.collectionUids);
});
}
case actions.LOAD_COLLECTIONS_FROM_IDB: {
return produce(state, (draft) => {
draft.collections = action.collections;
});
}
case actions.SIDEBAR_COLLECTION_NEW_REQUEST: {
return produce(state, (draft) => {
const collection = findCollectionByUid(draft.collections, action.collectionUid);
@ -88,25 +70,6 @@ const reducer = (state, action) => {
});
}
case actions.REQUEST_URL_CHANGED: {
return produce(state, (draft) => {
const collection = findCollectionByUid(draft.collections, action.collectionUid);
if(collection) {
let flattenedItems = flattenItems(collection.items);
let item = findItem(flattenedItems, action.itemUid);
if(item) {
if(!item.draft) {
item.draft = cloneItem(item);
}
item.draft.request.url = action.url;
updateRequestTabAsChanged(draft.requestTabs, item.uid);
}
}
});
}
case actions.REQUEST_GQL_QUERY_CHANGED: {
return produce(state, (draft) => {
const collection = findCollectionByUid(draft.collections, action.collectionUid);
@ -144,35 +107,6 @@ const reducer = (state, action) => {
});
}
case actions.HOTKEY_SAVE: {
return produce(state, (draft) => {
if(!draft.activeRequestTabUid) {
return;
}
// find request tab
const activeRequestTab = find(draft.requestTabs, (t) => t.uid === draft.activeRequestTabUid);
// resolve item, save and delete draft
if(activeRequestTab) {
const collection = findCollectionByUid(draft.collections, activeRequestTab.collectionUid);
if(collection) {
let flattenedItems = flattenItems(collection.items);
let item = findItem(flattenedItems, activeRequestTab.uid);
if(item && item.draft) {
item.name = item.draft.name;
item.request = item.draft.request;
item.draft = null;
activeRequestTab.hasChanges = false;
draft.collectionsToSyncToIdb.push(collection.uid);
}
}
}
});
}
default: {
return state;
}

View File

@ -1,20 +0,0 @@
import { useEffect } from 'react';
import { getCollectionsFromIdb } from './idb';
import actions from './actions';
const useLoadCollectionsFromIdb = (idbConnection, dispatch) => {
useEffect(() => {
if(idbConnection) {
getCollectionsFromIdb(idbConnection)
.then((collections) => {
dispatch({
type: actions.LOAD_COLLECTIONS_FROM_IDB,
collections: collections
});
})
.catch((err) => console.log(err));
}
}, [idbConnection, dispatch]);
};
export default useLoadCollectionsFromIdb;

View File

@ -1,41 +0,0 @@
import { useState, useEffect } from 'react';
import map from 'lodash/map';
import filter from 'lodash/filter';
import actions from './actions';
import { saveCollectionToIdb } from './idb';
// This hook listens to changes in 'collectionsToSyncToIdb' and syncs them to idb
// The app uses this when collections are created as well as when collections get updated
const useSyncCollectionsToIdb = (collectionsToSyncToIdb, collections, idbConnection, dispatch) => {
const [collectionsSyncingToIdb, setCollectionsSyncingToIdb] = useState(false);
useEffect(() => {
if(collectionsSyncingToIdb) {
return;
}
if(collectionsToSyncToIdb && collectionsToSyncToIdb.length) {
setCollectionsSyncingToIdb(true);
const _collections = filter(collections, (c) => {
return collectionsToSyncToIdb.indexOf(c.uid) > -1;
});
dispatch({
type: actions.IDB_COLLECTIONS_SYNC_STARTED
});
saveCollectionToIdb(idbConnection, _collections)
.then(() => {
setCollectionsSyncingToIdb(false);
})
.catch((err) => {
setCollectionsSyncingToIdb(false);
dispatch({
type: actions.IDB_COLLECTIONS_SYNC_ERROR,
collectionUids: map(collections, (c) => c.uid)
});
console.log(err);
});
}
}, [collectionsToSyncToIdb]);
};
export default useSyncCollectionsToIdb;

View File

@ -1,5 +1,6 @@
import each from 'lodash/each';
import find from 'lodash/find';
import cloneDeep from 'lodash/cloneDeep';
export const flattenItems = (items = []) => {
const flattenedItems = [];
@ -31,4 +32,62 @@ export const findItemInCollection = (collection, itemUid) => {
let flattenedItems = flattenItems(collection.items);
return findItem(flattenedItems, itemUid);
}
}
export const cloneItem = (item) => {
return cloneDeep(item);
};
export const transformCollectionToSaveToIdb = (collection) => {
const copyItems = (sourceItems, destItems) => {
each(sourceItems, (si) => {
const di = {
uid: si.uid,
type: si.type
};
// if items is draft, then take data from draft to save
if(si.draft) {
di.name = si.draft.name;
if(si.draft.request) {
di.request = {
url: si.draft.request.url,
method: si.draft.request.method,
headers: si.draft.request.headers,
body: si.draft.request.body
};
}
} else {
di.name = si.name;
if(si.request) {
di.request = {
url: si.request.url,
method: si.request.method,
headers: si.request.headers,
body: si.request.body
}
};
}
destItems.push(di);
if(si.items && si.items.length) {
di.items = [];
copyItems(si.items, di.items);
}
});
}
const collectionToSave = {};
collectionToSave.name = collection.name;
collectionToSave.uid = collection.uid;
collectionToSave.userId = collection.userId;
collectionToSave.environments = cloneDeep(collection.environments);
collectionToSave.items = [];
copyItems(collection.items, collectionToSave.items);
return collectionToSave;
};

View File

@ -4,7 +4,7 @@ const sendNetworkRequest = async (item) => {
return new Promise((resolve, reject) => {
if(item.type === 'http-request') {
const timeStart = Date.now();
sendHttpRequest(item.request)
sendHttpRequest(item.draft ? item.draft.request : item.request)
.then((response) => {
const timeEnd = Date.now();
resolve({