forked from extern/bruno
feat(#1162): warn if there are unsaved requests when quitting
This commit is contained in:
parent
85f24eec77
commit
3e627522b7
@ -86,7 +86,7 @@ const Modal = ({
|
|||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', escFunction, false);
|
document.removeEventListener('keydown', escFunction, false);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [disableEscapeKey, document]);
|
||||||
|
|
||||||
let classes = 'bruno-modal';
|
let classes = 'bruno-modal';
|
||||||
if (isClosing) {
|
if (isClosing) {
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import {
|
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||||
cancelCloseDraft,
|
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
closeAndSaveDraft,
|
|
||||||
closeTabs,
|
|
||||||
closeWithoutSavingDraft,
|
|
||||||
setShowConfirmClose
|
|
||||||
} from 'providers/ReduxStore/slices/tabs';
|
|
||||||
import { useTheme } from 'providers/Theme';
|
import { useTheme } from 'providers/Theme';
|
||||||
import React from 'react';
|
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import darkTheme from 'themes/dark';
|
import darkTheme from 'themes/dark';
|
||||||
import lightTheme from 'themes/light';
|
import lightTheme from 'themes/light';
|
||||||
@ -20,6 +15,7 @@ import StyledWrapper from './StyledWrapper';
|
|||||||
const RequestTab = ({ tab, collection }) => {
|
const RequestTab = ({ tab, collection }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { storedTheme } = useTheme();
|
const { storedTheme } = useTheme();
|
||||||
|
const [showConfirmClose, setShowConfirmClose] = useState(false);
|
||||||
|
|
||||||
const handleCloseClick = (event) => {
|
const handleCloseClick = (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@ -31,15 +27,6 @@ const RequestTab = ({ tab, collection }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showConfirmClose = () => {
|
|
||||||
dispatch(
|
|
||||||
setShowConfirmClose({
|
|
||||||
tabUid: tab.uid,
|
|
||||||
showConfirmClose: true
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMethodColor = (method = '') => {
|
const getMethodColor = (method = '') => {
|
||||||
const theme = storedTheme === 'dark' ? darkTheme : lightTheme;
|
const theme = storedTheme === 'dark' ? darkTheme : lightTheme;
|
||||||
|
|
||||||
@ -102,12 +89,38 @@ const RequestTab = ({ tab, collection }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper className="flex items-center justify-between tab-container px-1">
|
<StyledWrapper className="flex items-center justify-between tab-container px-1">
|
||||||
{tab.showConfirmClose && (
|
{showConfirmClose && (
|
||||||
<ConfirmRequestClose
|
<ConfirmRequestClose
|
||||||
item={item}
|
item={item}
|
||||||
onCancel={() => dispatch(cancelCloseDraft(item.uid))}
|
onCancel={() => setShowConfirmClose(false)}
|
||||||
onCloseWithoutSave={() => dispatch(closeWithoutSavingDraft(item.uid, collection.uid))}
|
onCloseWithoutSave={() => {
|
||||||
onSaveAndClose={() => dispatch(closeAndSaveDraft(item.uid, collection.uid))}
|
dispatch(
|
||||||
|
deleteRequestDraft({
|
||||||
|
itemUid: item.uid,
|
||||||
|
collectionUid: collection.uid
|
||||||
|
})
|
||||||
|
);
|
||||||
|
dispatch(
|
||||||
|
closeTabs({
|
||||||
|
tabUids: [tab.uid]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setShowConfirmClose(false);
|
||||||
|
}}
|
||||||
|
onSaveAndClose={() => {
|
||||||
|
dispatch(saveRequest(item.uid, collection.uid))
|
||||||
|
.then(() => {
|
||||||
|
dispatch(
|
||||||
|
closeTabs({
|
||||||
|
tabUids: [tab.uid]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setShowConfirmClose(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('err', err);
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-baseline tab-label pl-2">
|
<div className="flex items-baseline tab-label pl-2">
|
||||||
@ -122,7 +135,7 @@ const RequestTab = ({ tab, collection }) => {
|
|||||||
className="flex px-2 close-icon-container"
|
className="flex px-2 close-icon-container"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (!item.draft) return handleCloseClick(e);
|
if (!item.draft) return handleCloseClick(e);
|
||||||
showConfirmClose();
|
setShowConfirmClose(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!item.draft ? (
|
{!item.draft ? (
|
||||||
|
@ -0,0 +1,114 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import each from 'lodash/each';
|
||||||
|
import filter from 'lodash/filter';
|
||||||
|
import groupBy from 'lodash/groupBy';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { findCollectionByUid, flattenItems, isItemARequest } from 'utils/collections';
|
||||||
|
import { pluralizeWord } from 'utils/common';
|
||||||
|
import { completeQuitFlow } from 'providers/ReduxStore/slices/app';
|
||||||
|
import { saveMultipleRequests } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
|
import { IconAlertTriangle } from '@tabler/icons';
|
||||||
|
import Modal from 'components/Modal';
|
||||||
|
|
||||||
|
const SaveRequestsModal = ({ onClose }) => {
|
||||||
|
const MAX_UNSAVED_REQUESTS_TO_SHOW = 5;
|
||||||
|
const currentDrafts = [];
|
||||||
|
const collections = useSelector((state) => state.collections.collections);
|
||||||
|
const tabs = useSelector((state) => state.tabs.tabs);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const tabsByCollection = groupBy(tabs, (t) => t.collectionUid);
|
||||||
|
Object.keys(tabsByCollection).forEach((collectionUid) => {
|
||||||
|
const collection = findCollectionByUid(collections, collectionUid);
|
||||||
|
if (collection) {
|
||||||
|
const items = flattenItems(collection.items);
|
||||||
|
const drafts = filter(items, (item) => isItemARequest(item) && item.draft);
|
||||||
|
each(drafts, (draft) => {
|
||||||
|
currentDrafts.push({
|
||||||
|
...draft,
|
||||||
|
collectionUid: collectionUid
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentDrafts.length === 0) {
|
||||||
|
return dispatch(completeQuitFlow());
|
||||||
|
}
|
||||||
|
}, [currentDrafts, dispatch]);
|
||||||
|
|
||||||
|
const closeWithoutSave = () => {
|
||||||
|
dispatch(completeQuitFlow());
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeWithSave = () => {
|
||||||
|
dispatch(saveMultipleRequests(currentDrafts))
|
||||||
|
.then(() => dispatch(completeQuitFlow()))
|
||||||
|
.then(() => onClose());
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!currentDrafts.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
size="md"
|
||||||
|
title="Unsaved changes"
|
||||||
|
confirmText="Save and Close"
|
||||||
|
cancelText="Close without saving"
|
||||||
|
handleCancel={onClose}
|
||||||
|
disableEscapeKey={true}
|
||||||
|
disableCloseOnOutsideClick={true}
|
||||||
|
closeModalFadeTimeout={150}
|
||||||
|
hideFooter={true}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
|
||||||
|
<h1 className="ml-2 text-lg font-semibold">Hold on..</h1>
|
||||||
|
</div>
|
||||||
|
<p className="mt-4">
|
||||||
|
Do you want to save the changes you made to the following{' '}
|
||||||
|
<span className="font-medium">{currentDrafts.length}</span> {pluralizeWord('request', currentDrafts.length)}?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="mt-4">
|
||||||
|
{currentDrafts.slice(0, MAX_UNSAVED_REQUESTS_TO_SHOW).map((item) => {
|
||||||
|
return (
|
||||||
|
<li key={item.uid} className="mt-1 text-xs">
|
||||||
|
{item.filename}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{currentDrafts.length > MAX_UNSAVED_REQUESTS_TO_SHOW && (
|
||||||
|
<p className="mt-1 text-xs">
|
||||||
|
...{currentDrafts.length - MAX_UNSAVED_REQUESTS_TO_SHOW} additional{' '}
|
||||||
|
{pluralizeWord('request', currentDrafts.length - MAX_UNSAVED_REQUESTS_TO_SHOW)} not shown
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-between mt-6">
|
||||||
|
<div>
|
||||||
|
<button className="btn btn-sm btn-close btn-border" onClick={closeWithoutSave}>
|
||||||
|
Don't Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button className="btn btn-close btn-sm mr-2" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-secondary btn-sm" onClick={closeWithSave}>
|
||||||
|
{currentDrafts.length > 1 ? 'Save All' : 'Save'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SaveRequestsModal;
|
@ -0,0 +1,32 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import SaveRequestsModal from './SaveRequestsModal';
|
||||||
|
import { isElectron } from 'utils/common/platform';
|
||||||
|
|
||||||
|
const ConfirmAppClose = () => {
|
||||||
|
const { ipcRenderer } = window;
|
||||||
|
const [showConfirmClose, setShowConfirmClose] = useState(false);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isElectron()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearListener = ipcRenderer.on('main:start-quit-flow', () => {
|
||||||
|
setShowConfirmClose(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearListener();
|
||||||
|
};
|
||||||
|
}, [isElectron, ipcRenderer, dispatch, setShowConfirmClose]);
|
||||||
|
|
||||||
|
if (!showConfirmClose) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SaveRequestsModal onClose={() => setShowConfirmClose(false)} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfirmAppClose;
|
@ -1,9 +1,10 @@
|
|||||||
import { refreshScreenWidth } from 'providers/ReduxStore/slices/app';
|
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import { refreshScreenWidth } from 'providers/ReduxStore/slices/app';
|
||||||
|
import ConfirmAppClose from './ConfirmAppClose';
|
||||||
import useIpcEvents from './useIpcEvents';
|
import useIpcEvents from './useIpcEvents';
|
||||||
import useTelemetry from './useTelemetry';
|
import useTelemetry from './useTelemetry';
|
||||||
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
export const AppContext = React.createContext();
|
export const AppContext = React.createContext();
|
||||||
|
|
||||||
@ -29,7 +30,10 @@ export const AppProvider = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContext.Provider {...props} value="appProvider">
|
<AppContext.Provider {...props} value="appProvider">
|
||||||
<StyledWrapper>{props.children}</StyledWrapper>
|
<StyledWrapper>
|
||||||
|
<ConfirmAppClose />
|
||||||
|
{props.children}
|
||||||
|
</StyledWrapper>
|
||||||
</AppContext.Provider>
|
</AppContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { showPreferences, startQuitFlow, updateCookies, updatePreferences } from 'providers/ReduxStore/slices/app';
|
import { useEffect } from 'react';
|
||||||
|
import { showPreferences, updateCookies, updatePreferences } from 'providers/ReduxStore/slices/app';
|
||||||
import {
|
import {
|
||||||
brunoConfigUpdateEvent,
|
brunoConfigUpdateEvent,
|
||||||
collectionAddDirectoryEvent,
|
collectionAddDirectoryEvent,
|
||||||
@ -14,7 +15,6 @@ import {
|
|||||||
scriptEnvironmentUpdateEvent
|
scriptEnvironmentUpdateEvent
|
||||||
} from 'providers/ReduxStore/slices/collections';
|
} from 'providers/ReduxStore/slices/collections';
|
||||||
import { collectionAddEnvFileEvent, openCollectionEvent } from 'providers/ReduxStore/slices/collections/actions';
|
import { collectionAddEnvFileEvent, openCollectionEvent } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
import { useEffect } from 'react';
|
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { isElectron } from 'utils/common/platform';
|
import { isElectron } from 'utils/common/platform';
|
||||||
@ -140,10 +140,6 @@ const useIpcEvents = () => {
|
|||||||
dispatch(updateCookies(val));
|
dispatch(updateCookies(val));
|
||||||
});
|
});
|
||||||
|
|
||||||
const removeStartQuitFlowListener = ipcRenderer.on('main:start-quit-flow', () => {
|
|
||||||
dispatch(startQuitFlow());
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
removeCollectionTreeUpdateListener();
|
removeCollectionTreeUpdateListener();
|
||||||
removeOpenCollectionListener();
|
removeOpenCollectionListener();
|
||||||
@ -159,7 +155,6 @@ const useIpcEvents = () => {
|
|||||||
removeShowPreferencesListener();
|
removeShowPreferencesListener();
|
||||||
removePreferencesUpdatesListener();
|
removePreferencesUpdatesListener();
|
||||||
removeCookieUpdateListener();
|
removeCookieUpdateListener();
|
||||||
removeStartQuitFlowListener();
|
|
||||||
};
|
};
|
||||||
}, [isElectron]);
|
}, [isElectron]);
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { configureStore } from '@reduxjs/toolkit';
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
import listenerMiddleware from './middlewares/listenerMiddleware';
|
import tasksMiddleware from './middlewares/tasks/middleware';
|
||||||
import appReducer from './slices/app';
|
import appReducer from './slices/app';
|
||||||
import collectionsReducer from './slices/collections';
|
import collectionsReducer from './slices/collections';
|
||||||
import tabsReducer from './slices/tabs';
|
import tabsReducer from './slices/tabs';
|
||||||
@ -10,7 +10,7 @@ export const store = configureStore({
|
|||||||
collections: collectionsReducer,
|
collections: collectionsReducer,
|
||||||
tabs: tabsReducer
|
tabs: tabsReducer
|
||||||
},
|
},
|
||||||
middleware: (getDefaultMiddleware) => getDefaultMiddleware().prepend(listenerMiddleware.middleware)
|
middleware: (getDefaultMiddleware) => getDefaultMiddleware().prepend(tasksMiddleware.middleware)
|
||||||
});
|
});
|
||||||
|
|
||||||
export default store;
|
export default store;
|
||||||
|
@ -1,123 +0,0 @@
|
|||||||
import { createListenerMiddleware } from '@reduxjs/toolkit';
|
|
||||||
import { completeQuitFlow, removeEventsFromQueue } from 'providers/ReduxStore/slices/app';
|
|
||||||
import { addTab, closeTabs, focusTab, setShowConfirmClose } from 'providers/ReduxStore/slices/tabs';
|
|
||||||
import {
|
|
||||||
findCollectionByUid,
|
|
||||||
findItemInCollection,
|
|
||||||
findItemInCollectionByPathname,
|
|
||||||
getDefaultRequestPaneTab
|
|
||||||
} from 'utils/collections/index';
|
|
||||||
import { eventMatchesItem, eventTypes } from 'utils/events-queue/index';
|
|
||||||
import { itemIsOpenedInTabs } from 'utils/tabs/index';
|
|
||||||
|
|
||||||
const listenerMiddleware = createListenerMiddleware();
|
|
||||||
|
|
||||||
listenerMiddleware.startListening({
|
|
||||||
predicate: (action) => ['app/insertEventsIntoQueue', 'app/removeEventsFromQueue'].includes(action.type),
|
|
||||||
effect: async (action, listenerApi) => {
|
|
||||||
const state = listenerApi.getState();
|
|
||||||
const { tabs } = state.tabs;
|
|
||||||
|
|
||||||
// after events are added or removed from queue, it will handle the first (if there is any left)
|
|
||||||
const [firstEvent] = state.app.eventsQueue;
|
|
||||||
if (!firstEvent) return;
|
|
||||||
|
|
||||||
if (firstEvent.eventType === eventTypes.CLOSE_APP) {
|
|
||||||
// this events closes the window
|
|
||||||
return listenerApi.dispatch(completeQuitFlow());
|
|
||||||
}
|
|
||||||
|
|
||||||
const { itemUid, itemPathname, collectionUid, eventType } = firstEvent;
|
|
||||||
let eventItem = null;
|
|
||||||
if (firstEvent.eventType === eventTypes.OPEN_REQUEST) {
|
|
||||||
// this event adds or opens a request
|
|
||||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
|
||||||
eventItem = findItemInCollectionByPathname(collection, itemPathname);
|
|
||||||
if (!eventItem) {
|
|
||||||
// waiting until item is added into collection (only happens after IO completes) before handling event
|
|
||||||
// this happens when first opening a request just after creating it
|
|
||||||
await listenerApi.condition((action, currentState, originalState) => {
|
|
||||||
const { collections } = currentState.collections;
|
|
||||||
const collection = findCollectionByUid(collections, collectionUid);
|
|
||||||
const item = findItemInCollectionByPathname(collection, itemPathname);
|
|
||||||
if (item) eventItem = item;
|
|
||||||
return !!item;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const { collections } = state.collections;
|
|
||||||
const collection = findCollectionByUid(collections, collectionUid);
|
|
||||||
const item = findItemInCollection(collection, itemUid);
|
|
||||||
if (item) eventItem = item;
|
|
||||||
}
|
|
||||||
if (eventItem) {
|
|
||||||
switch (eventType) {
|
|
||||||
case eventTypes.OPEN_REQUEST: // this event adds or opens a request
|
|
||||||
return listenerApi.dispatch(
|
|
||||||
itemIsOpenedInTabs(eventItem, tabs)
|
|
||||||
? focusTab({
|
|
||||||
uid: eventItem.uid
|
|
||||||
})
|
|
||||||
: addTab({
|
|
||||||
uid: eventItem.uid,
|
|
||||||
collectionUid,
|
|
||||||
requestPaneTab: getDefaultRequestPaneTab(eventItem)
|
|
||||||
})
|
|
||||||
);
|
|
||||||
case eventTypes.CLOSE_REQUEST: // this event closes a request or prompts the user to save it if has pending changes
|
|
||||||
return listenerApi.dispatch(
|
|
||||||
eventItem.draft
|
|
||||||
? setShowConfirmClose({
|
|
||||||
tabUid: eventItem.uid,
|
|
||||||
showConfirmClose: true
|
|
||||||
})
|
|
||||||
: closeTabs({
|
|
||||||
tabUids: [eventItem.uid]
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
listenerMiddleware.startListening({
|
|
||||||
predicate: (action) => ['tabs/addTab', 'tabs/focusTab'].includes(action.type),
|
|
||||||
effect: (action, listenerApi) => {
|
|
||||||
let { uid, collectionUid } = action.payload;
|
|
||||||
const state = listenerApi.getState();
|
|
||||||
const { eventsQueue } = state.app;
|
|
||||||
const { collections } = state.collections;
|
|
||||||
const { tabs } = state.tabs;
|
|
||||||
|
|
||||||
// after tab is opened, remove corresponding event from start of queue (if any)
|
|
||||||
const [firstEvent] = eventsQueue;
|
|
||||||
if (firstEvent && firstEvent.eventType == eventTypes.OPEN_REQUEST) {
|
|
||||||
collectionUid = collectionUid ?? tabs.find((t) => t.uid === uid).collectionUid;
|
|
||||||
const collection = findCollectionByUid(collections, collectionUid);
|
|
||||||
const item = findItemInCollection(collection, uid);
|
|
||||||
const eventToRemove = eventMatchesItem(firstEvent, item) ? firstEvent : null;
|
|
||||||
if (eventToRemove) {
|
|
||||||
listenerApi.dispatch(removeEventsFromQueue([eventToRemove]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
listenerMiddleware.startListening({
|
|
||||||
actionCreator: closeTabs,
|
|
||||||
effect: (action, listenerApi) => {
|
|
||||||
const state = listenerApi.getState();
|
|
||||||
const { tabUids } = action.payload;
|
|
||||||
const { eventsQueue } = state.app;
|
|
||||||
|
|
||||||
// after tab is closed, remove corresponding event from start of queue (if any)
|
|
||||||
const [firstEvent] = eventsQueue;
|
|
||||||
if (!firstEvent || firstEvent.eventType !== eventTypes.CLOSE_REQUEST) return;
|
|
||||||
const eventToRemove = tabUids.some((uid) => uid === firstEvent.itemUid) ? firstEvent : null;
|
|
||||||
if (eventToRemove) {
|
|
||||||
listenerApi.dispatch(removeEventsFromQueue([eventToRemove]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default listenerMiddleware;
|
|
@ -0,0 +1,52 @@
|
|||||||
|
import get from 'lodash/get';
|
||||||
|
import each from 'lodash/each';
|
||||||
|
import filter from 'lodash/filter';
|
||||||
|
import { uuid } from 'utils/common';
|
||||||
|
import { createListenerMiddleware } from '@reduxjs/toolkit';
|
||||||
|
import { completeQuitFlow, removeTaskFromQueue, hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||||
|
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||||
|
import { collectionAddFileEvent } from 'providers/ReduxStore/slices/collections';
|
||||||
|
import { findCollectionByUid, findItemInCollectionByPathname, getDefaultRequestPaneTab } from 'utils/collections/index';
|
||||||
|
import { taskTypes } from './utils';
|
||||||
|
|
||||||
|
const taskMiddleware = createListenerMiddleware();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When a new request is created in the app, a task to open the request is added to the queue.
|
||||||
|
* We wait for the File IO to complete, after which the "collectionAddFileEvent" gets dispatched.
|
||||||
|
* This middleware listens for the event and checks if there is a task in the queue that matches
|
||||||
|
* the collectionUid and itemPathname. If there is a match, we open the request and remove the task
|
||||||
|
* from the queue.
|
||||||
|
*/
|
||||||
|
taskMiddleware.startListening({
|
||||||
|
actionCreator: collectionAddFileEvent,
|
||||||
|
effect: (action, listenerApi) => {
|
||||||
|
const state = listenerApi.getState();
|
||||||
|
const collectionUid = get(action, 'payload.file.meta.collectionUid');
|
||||||
|
|
||||||
|
const openRequestTasks = filter(state.app.taskQueue, { type: taskTypes.OPEN_REQUEST });
|
||||||
|
each(openRequestTasks, (task) => {
|
||||||
|
if (collectionUid === task.collectionUid) {
|
||||||
|
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||||
|
const item = findItemInCollectionByPathname(collection, task.itemPathname);
|
||||||
|
if (item) {
|
||||||
|
listenerApi.dispatch(
|
||||||
|
addTab({
|
||||||
|
uid: item.uid,
|
||||||
|
collectionUid: collection.uid,
|
||||||
|
requestPaneTab: getDefaultRequestPaneTab(item)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
listenerApi.dispatch(hideHomePage());
|
||||||
|
listenerApi.dispatch(
|
||||||
|
removeTaskFromQueue({
|
||||||
|
taskUid: task.uid
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default taskMiddleware;
|
@ -0,0 +1,3 @@
|
|||||||
|
export const taskTypes = {
|
||||||
|
OPEN_REQUEST: 'OPEN_REQUEST'
|
||||||
|
};
|
@ -1,11 +1,6 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import filter from 'lodash/filter';
|
import filter from 'lodash/filter';
|
||||||
import groupBy from 'lodash/groupBy';
|
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { isItemARequest } from 'utils/collections';
|
|
||||||
import { findCollectionByUid, flattenItems } from 'utils/collections/index';
|
|
||||||
import { uuid } from 'utils/common';
|
|
||||||
import { eventTypes } from 'utils/events-queue/index';
|
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
@ -28,7 +23,7 @@ const initialState = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
cookies: [],
|
cookies: [],
|
||||||
eventsQueue: []
|
taskQueue: []
|
||||||
};
|
};
|
||||||
|
|
||||||
export const appSlice = createSlice({
|
export const appSlice = createSlice({
|
||||||
@ -62,18 +57,14 @@ export const appSlice = createSlice({
|
|||||||
updateCookies: (state, action) => {
|
updateCookies: (state, action) => {
|
||||||
state.cookies = action.payload;
|
state.cookies = action.payload;
|
||||||
},
|
},
|
||||||
insertEventsIntoQueue: (state, action) => {
|
insertTaskIntoQueue: (state, action) => {
|
||||||
state.eventsQueue = state.eventsQueue.concat(action.payload);
|
state.taskQueue.push(action.payload);
|
||||||
},
|
},
|
||||||
removeEventsFromQueue: (state, action) => {
|
removeTaskFromQueue: (state, action) => {
|
||||||
const eventsToRemove = action.payload;
|
state.taskQueue = filter(state.taskQueue, (task) => task.uid !== action.payload.taskUid);
|
||||||
state.eventsQueue = filter(
|
|
||||||
state.eventsQueue,
|
|
||||||
(event) => !eventsToRemove.some((e) => e.eventUid === event.eventUid)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
removeAllEventsFromQueue: (state) => {
|
removeAllTasksFromQueue: (state) => {
|
||||||
state.eventsQueue = [];
|
state.taskQueue = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -88,9 +79,9 @@ export const {
|
|||||||
showPreferences,
|
showPreferences,
|
||||||
updatePreferences,
|
updatePreferences,
|
||||||
updateCookies,
|
updateCookies,
|
||||||
insertEventsIntoQueue,
|
insertTaskIntoQueue,
|
||||||
removeEventsFromQueue,
|
removeTaskFromQueue,
|
||||||
removeAllEventsFromQueue
|
removeAllTasksFromQueue
|
||||||
} = appSlice.actions;
|
} = appSlice.actions;
|
||||||
|
|
||||||
export const savePreferences = (preferences) => (dispatch, getState) => {
|
export const savePreferences = (preferences) => (dispatch, getState) => {
|
||||||
@ -118,52 +109,6 @@ export const deleteCookiesForDomain = (domain) => (dispatch, getState) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const startQuitFlow = () => (dispatch, getState) => {
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
// Before closing the app, checks for unsaved requests (drafts)
|
|
||||||
const currentDrafts = [];
|
|
||||||
const { collections } = state.collections;
|
|
||||||
const { tabs } = state.tabs;
|
|
||||||
|
|
||||||
const tabsByCollection = groupBy(tabs, (t) => t.collectionUid);
|
|
||||||
Object.keys(tabsByCollection).forEach((collectionUid) => {
|
|
||||||
const collectionItems = flattenItems(findCollectionByUid(collections, collectionUid).items);
|
|
||||||
let openedTabs = tabsByCollection[collectionUid];
|
|
||||||
for (const item of collectionItems) {
|
|
||||||
if (isItemARequest(item) && item.draft) {
|
|
||||||
openedTabs = filter(openedTabs, (t) => t.uid !== item.uid);
|
|
||||||
currentDrafts.push({ ...item, collectionUid });
|
|
||||||
}
|
|
||||||
if (!openedTabs.length) return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// If there are no drafts, closes the window
|
|
||||||
if (currentDrafts.length === 0) {
|
|
||||||
return dispatch(completeQuitFlow());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sequence of events tracked by listener middleware
|
|
||||||
// For every draft, it will focus the request and immediately prompt if the user wants to save it
|
|
||||||
// At the end of the sequence, closes the window
|
|
||||||
const events = currentDrafts
|
|
||||||
.reduce((acc, draft) => {
|
|
||||||
const { uid, pathname, collectionUid } = draft;
|
|
||||||
const defaultProperties = { itemUid: uid, collectionUid, itemPathname: pathname };
|
|
||||||
acc.push(
|
|
||||||
...[
|
|
||||||
{ eventUid: uuid(), eventType: eventTypes.OPEN_REQUEST, ...defaultProperties },
|
|
||||||
{ eventUid: uuid(), eventType: eventTypes.CLOSE_REQUEST, ...defaultProperties }
|
|
||||||
]
|
|
||||||
);
|
|
||||||
return acc;
|
|
||||||
}, [])
|
|
||||||
.concat([{ eventUid: uuid(), eventType: eventTypes.CLOSE_APP }]);
|
|
||||||
|
|
||||||
dispatch(insertEventsIntoQueue(events));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const completeQuitFlow = () => (dispatch, getState) => {
|
export const completeQuitFlow = () => (dispatch, getState) => {
|
||||||
const { ipcRenderer } = window;
|
const { ipcRenderer } = window;
|
||||||
return ipcRenderer.invoke('main:complete-quit-flow');
|
return ipcRenderer.invoke('main:complete-quit-flow');
|
||||||
|
@ -5,7 +5,7 @@ import find from 'lodash/find';
|
|||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import trim from 'lodash/trim';
|
import trim from 'lodash/trim';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { insertEventsIntoQueue } from 'providers/ReduxStore/slices/app';
|
import { insertTaskIntoQueue } from 'providers/ReduxStore/slices/app';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import {
|
import {
|
||||||
findCollectionByUid,
|
findCollectionByUid,
|
||||||
@ -84,6 +84,38 @@ export const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const saveMultipleRequests = (items) => (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
const { collections } = state.collections;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const itemsToSave = [];
|
||||||
|
each(items, (item) => {
|
||||||
|
const collection = findCollectionByUid(collections, item.collectionUid);
|
||||||
|
if (collection) {
|
||||||
|
const itemToSave = transformRequestToSaveToFilesystem(item);
|
||||||
|
const itemIsValid = itemSchema.validateSync(itemToSave);
|
||||||
|
if (itemIsValid) {
|
||||||
|
itemsToSave.push({
|
||||||
|
item: itemToSave,
|
||||||
|
pathname: item.pathname
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ipcRenderer } = window;
|
||||||
|
|
||||||
|
ipcRenderer
|
||||||
|
.invoke('renderer:save-multiple-requests', itemsToSave)
|
||||||
|
.then(resolve)
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error('Failed to save requests!');
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const saveCollectionRoot = (collectionUid) => (dispatch, getState) => {
|
export const saveCollectionRoot = (collectionUid) => (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||||
@ -625,16 +657,14 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
|||||||
const { ipcRenderer } = window;
|
const { ipcRenderer } = window;
|
||||||
|
|
||||||
ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
|
ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
|
||||||
// listener middleware will track this and open the new request in a new tab once request is created
|
// task middleware will track this and open the new request in a new tab once request is created
|
||||||
dispatch(
|
dispatch(
|
||||||
insertEventsIntoQueue([
|
insertTaskIntoQueue({
|
||||||
{
|
uid: uuid(),
|
||||||
eventUid: uuid(),
|
type: 'OPEN_REQUEST',
|
||||||
eventType: 'OPEN_REQUEST',
|
collectionUid,
|
||||||
collectionUid,
|
itemPathname: fullName
|
||||||
itemPathname: fullName
|
})
|
||||||
}
|
|
||||||
])
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return reject(new Error('Duplicate request names are not allowed under the same folder'));
|
return reject(new Error('Duplicate request names are not allowed under the same folder'));
|
||||||
@ -653,16 +683,14 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
|||||||
const { ipcRenderer } = window;
|
const { ipcRenderer } = window;
|
||||||
|
|
||||||
ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
|
ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
|
||||||
// listener middleware will track this and open the new request in a new tab once request is created
|
// task middleware will track this and open the new request in a new tab once request is created
|
||||||
dispatch(
|
dispatch(
|
||||||
insertEventsIntoQueue([
|
insertTaskIntoQueue({
|
||||||
{
|
uid: uuid(),
|
||||||
eventUid: uuid(),
|
type: 'OPEN_REQUEST',
|
||||||
eventType: 'OPEN_REQUEST',
|
collectionUid,
|
||||||
collectionUid,
|
itemPathname: fullName
|
||||||
itemPathname: fullName
|
})
|
||||||
}
|
|
||||||
])
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return reject(new Error('Duplicate request names are not allowed under the same folder'));
|
return reject(new Error('Duplicate request names are not allowed under the same folder'));
|
||||||
|
@ -2,10 +2,6 @@ import { createSlice } from '@reduxjs/toolkit';
|
|||||||
import filter from 'lodash/filter';
|
import filter from 'lodash/filter';
|
||||||
import find from 'lodash/find';
|
import find from 'lodash/find';
|
||||||
import last from 'lodash/last';
|
import last from 'lodash/last';
|
||||||
import { removeAllEventsFromQueue } from 'providers/ReduxStore/slices/app';
|
|
||||||
import { deleteRequestDraft } from 'providers/ReduxStore/slices/collections';
|
|
||||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
|
||||||
import { eventTypes } from 'utils/events-queue/index';
|
|
||||||
|
|
||||||
// todo: errors should be tracked in each slice and displayed as toasts
|
// todo: errors should be tracked in each slice and displayed as toasts
|
||||||
|
|
||||||
@ -105,11 +101,6 @@ export const tabsSlice = createSlice({
|
|||||||
const collectionUid = action.payload.collectionUid;
|
const collectionUid = action.payload.collectionUid;
|
||||||
state.tabs = filter(state.tabs, (t) => t.collectionUid !== collectionUid);
|
state.tabs = filter(state.tabs, (t) => t.collectionUid !== collectionUid);
|
||||||
state.activeTabUid = null;
|
state.activeTabUid = null;
|
||||||
},
|
|
||||||
setShowConfirmClose: (state, action) => {
|
|
||||||
const { tabUid, showConfirmClose } = action.payload;
|
|
||||||
const tab = find(state.tabs, (t) => t.uid === tabUid);
|
|
||||||
if (tab) tab.showConfirmClose = showConfirmClose;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -121,46 +112,7 @@ export const {
|
|||||||
updateRequestPaneTab,
|
updateRequestPaneTab,
|
||||||
updateResponsePaneTab,
|
updateResponsePaneTab,
|
||||||
closeTabs,
|
closeTabs,
|
||||||
closeAllCollectionTabs,
|
closeAllCollectionTabs
|
||||||
setShowConfirmClose
|
|
||||||
} = tabsSlice.actions;
|
} = tabsSlice.actions;
|
||||||
|
|
||||||
export const closeAndSaveDraft = (itemUid, collectionUid) => (dispatch) => {
|
|
||||||
dispatch(saveRequest(itemUid, collectionUid)).then(() => {
|
|
||||||
dispatch(
|
|
||||||
closeTabs({
|
|
||||||
tabUids: [itemUid]
|
|
||||||
})
|
|
||||||
);
|
|
||||||
dispatch(setShowConfirmClose({ tabUid: itemUid, showConfirmClose: false }));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const closeWithoutSavingDraft = (itemUid, collectionUid) => (dispatch) => {
|
|
||||||
dispatch(
|
|
||||||
deleteRequestDraft({
|
|
||||||
itemUid: itemUid,
|
|
||||||
collectionUid: collectionUid
|
|
||||||
})
|
|
||||||
);
|
|
||||||
dispatch(
|
|
||||||
closeTabs({
|
|
||||||
tabUids: [itemUid]
|
|
||||||
})
|
|
||||||
);
|
|
||||||
dispatch(setShowConfirmClose({ tabUid: itemUid, showConfirmClose: false }));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cancelCloseDraft = (itemUid) => (dispatch, getState) => {
|
|
||||||
const state = getState();
|
|
||||||
dispatch(setShowConfirmClose({ tabUid: itemUid, showConfirmClose: false }));
|
|
||||||
|
|
||||||
// check if there was an event to close this tab and aborts the sequence
|
|
||||||
const { eventsQueue } = state.app;
|
|
||||||
const [firstEvent] = eventsQueue;
|
|
||||||
if (firstEvent && firstEvent.eventType === eventTypes.CLOSE_REQUEST && firstEvent.itemUid === itemUid) {
|
|
||||||
dispatch(removeAllEventsFromQueue());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default tabsSlice.reducer;
|
export default tabsSlice.reducer;
|
||||||
|
@ -106,3 +106,7 @@ export const startsWith = (str, search) => {
|
|||||||
|
|
||||||
return str.substr(0, search.length) === search;
|
return str.substr(0, search.length) === search;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const pluralizeWord = (word, count) => {
|
||||||
|
return count === 1 ? word : `${word}s`;
|
||||||
|
};
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
export const eventTypes = {
|
|
||||||
OPEN_REQUEST: 'OPEN_REQUEST',
|
|
||||||
CLOSE_REQUEST: 'CLOSE_REQUEST',
|
|
||||||
CLOSE_APP: 'CLOSE_APP'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const eventMatchesItem = (event, item) => {
|
|
||||||
return event.itemUid === item.uid || event.itemPathname === item.pathname;
|
|
||||||
};
|
|
@ -178,6 +178,25 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// save multiple requests
|
||||||
|
ipcMain.handle('renderer:save-multiple-requests', async (event, requestsToSave) => {
|
||||||
|
try {
|
||||||
|
for (let r of requestsToSave) {
|
||||||
|
const request = r.item;
|
||||||
|
const pathname = r.pathname;
|
||||||
|
|
||||||
|
if (!fs.existsSync(pathname)) {
|
||||||
|
throw new Error(`path: ${pathname} does not exist`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = jsonToBru(request);
|
||||||
|
await writeFile(pathname, content);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// create environment
|
// create environment
|
||||||
ipcMain.handle('renderer:create-environment', async (event, collectionPathname, name, variables) => {
|
ipcMain.handle('renderer:create-environment', async (event, collectionPathname, name, variables) => {
|
||||||
try {
|
try {
|
||||||
@ -586,6 +605,7 @@ const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) =
|
|||||||
lastOpenedCollections.add(pathname);
|
lastOpenedCollections.add(pathname);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The app listen for this event and allows the user to save unsaved requests before closing the app
|
||||||
ipcMain.on('main:start-quit-flow', () => {
|
ipcMain.on('main:start-quit-flow', () => {
|
||||||
mainWindow.webContents.send('main:start-quit-flow');
|
mainWindow.webContents.send('main:start-quit-flow');
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user