setShowConfirmClose(false)}
- onCloseWithoutSave={() => {
- 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);
- });
- }}
+ item={item}
+ onCancel={() => dispatch(cancelCloseDraft(item.uid))}
+ onCloseWithoutSave={() => dispatch(closeWithoutSavingDraft(item.uid, collection.uid))}
+ onSaveAndClose={() => dispatch(closeAndSaveDraft(item.uid, collection.uid))}
/>
)}
@@ -135,8 +122,7 @@ const RequestTab = ({ tab, collection }) => {
className="flex px-2 close-icon-container"
onClick={(e) => {
if (!item.draft) return handleCloseClick(e);
-
- setShowConfirmClose(true);
+ showConfirmClose();
}}
>
{!item.draft ? (
diff --git a/packages/bruno-app/src/providers/App/index.js b/packages/bruno-app/src/providers/App/index.js
index 2fbd17e75..1022b5eec 100644
--- a/packages/bruno-app/src/providers/App/index.js
+++ b/packages/bruno-app/src/providers/App/index.js
@@ -1,17 +1,15 @@
-import React, { useEffect } from 'react';
-import useTelemetry from './useTelemetry';
-import useIpcEvents from './useIpcEvents';
-import useCollectionNextAction from './useCollectionNextAction';
-import { useDispatch } from 'react-redux';
import { refreshScreenWidth } from 'providers/ReduxStore/slices/app';
+import React, { useEffect } from 'react';
+import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
+import useIpcEvents from './useIpcEvents';
+import useTelemetry from './useTelemetry';
export const AppContext = React.createContext();
export const AppProvider = (props) => {
useTelemetry();
useIpcEvents();
- useCollectionNextAction();
const dispatch = useDispatch();
diff --git a/packages/bruno-app/src/providers/App/useCollectionNextAction.js b/packages/bruno-app/src/providers/App/useCollectionNextAction.js
deleted file mode 100644
index 57e14c2fc..000000000
--- a/packages/bruno-app/src/providers/App/useCollectionNextAction.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import React, { useEffect } from 'react';
-import get from 'lodash/get';
-import each from 'lodash/each';
-import { addTab } from 'providers/ReduxStore/slices/tabs';
-import { getDefaultRequestPaneTab, findItemInCollectionByPathname } from 'utils/collections/index';
-import { hideHomePage } from 'providers/ReduxStore/slices/app';
-import { updateNextAction } from 'providers/ReduxStore/slices/collections/index';
-import { useSelector, useDispatch } from 'react-redux';
-
-const useCollectionNextAction = () => {
- const collections = useSelector((state) => state.collections.collections);
- const dispatch = useDispatch();
-
- useEffect(() => {
- each(collections, (collection) => {
- if (collection.nextAction && collection.nextAction.type === 'OPEN_REQUEST') {
- const item = findItemInCollectionByPathname(collection, get(collection, 'nextAction.payload.pathname'));
-
- if (item) {
- dispatch(updateNextAction({ collectionUid: collection.uid, nextAction: null }));
- dispatch(
- addTab({
- uid: item.uid,
- collectionUid: collection.uid,
- requestPaneTab: getDefaultRequestPaneTab(item.type)
- })
- );
- dispatch(hideHomePage());
- }
- }
- });
- }, [collections, each, dispatch, updateNextAction, hideHomePage, addTab]);
-};
-
-export default useCollectionNextAction;
diff --git a/packages/bruno-app/src/providers/App/useIpcEvents.js b/packages/bruno-app/src/providers/App/useIpcEvents.js
index 3f251f2fe..58e902e56 100644
--- a/packages/bruno-app/src/providers/App/useIpcEvents.js
+++ b/packages/bruno-app/src/providers/App/useIpcEvents.js
@@ -1,22 +1,22 @@
-import { useEffect } from 'react';
-import { useDispatch } from 'react-redux';
+import { showPreferences, startQuitFlow, updateCookies, updatePreferences } from 'providers/ReduxStore/slices/app';
import {
+ brunoConfigUpdateEvent,
collectionAddDirectoryEvent,
collectionAddFileEvent,
collectionChangeFileEvent,
- collectionUnlinkFileEvent,
+ collectionRenamedEvent,
collectionUnlinkDirectoryEvent,
collectionUnlinkEnvFileEvent,
- scriptEnvironmentUpdateEvent,
+ collectionUnlinkFileEvent,
processEnvUpdateEvent,
- collectionRenamedEvent,
- runRequestEvent,
runFolderEvent,
- brunoConfigUpdateEvent
+ runRequestEvent,
+ scriptEnvironmentUpdateEvent
} from 'providers/ReduxStore/slices/collections';
-import { showPreferences, updatePreferences, updateCookies } from 'providers/ReduxStore/slices/app';
+import { collectionAddEnvFileEvent, openCollectionEvent } from 'providers/ReduxStore/slices/collections/actions';
+import { useEffect } from 'react';
import toast from 'react-hot-toast';
-import { openCollectionEvent, collectionAddEnvFileEvent } from 'providers/ReduxStore/slices/collections/actions';
+import { useDispatch } from 'react-redux';
import { isElectron } from 'utils/common/platform';
const useIpcEvents = () => {
@@ -80,6 +80,7 @@ const useIpcEvents = () => {
};
ipcRenderer.invoke('renderer:ready');
+
const removeCollectionTreeUpdateListener = ipcRenderer.on('main:collection-tree-updated', _collectionTreeUpdated);
const removeOpenCollectionListener = ipcRenderer.on('main:collection-opened', (pathname, uid, brunoConfig) => {
@@ -127,7 +128,7 @@ const useIpcEvents = () => {
dispatch(brunoConfigUpdateEvent(val))
);
- const showPreferencesListener = ipcRenderer.on('main:open-preferences', () => {
+ const removeShowPreferencesListener = ipcRenderer.on('main:open-preferences', () => {
dispatch(showPreferences(true));
});
@@ -139,6 +140,10 @@ const useIpcEvents = () => {
dispatch(updateCookies(val));
});
+ const removeStartQuitFlowListener = ipcRenderer.on('main:start-quit-flow', () => {
+ dispatch(startQuitFlow());
+ });
+
return () => {
removeCollectionTreeUpdateListener();
removeOpenCollectionListener();
@@ -151,9 +156,10 @@ const useIpcEvents = () => {
removeProcessEnvUpdatesListener();
removeConsoleLogListener();
removeConfigUpdatesListener();
- showPreferencesListener();
+ removeShowPreferencesListener();
removePreferencesUpdatesListener();
removeCookieUpdateListener();
+ removeStartQuitFlowListener();
};
}, [isElectron]);
};
diff --git a/packages/bruno-app/src/providers/ReduxStore/index.js b/packages/bruno-app/src/providers/ReduxStore/index.js
index d86b18fc4..5ac8056f9 100644
--- a/packages/bruno-app/src/providers/ReduxStore/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/index.js
@@ -1,4 +1,5 @@
import { configureStore } from '@reduxjs/toolkit';
+import listenerMiddleware from './middlewares/listenerMiddleware';
import appReducer from './slices/app';
import collectionsReducer from './slices/collections';
import tabsReducer from './slices/tabs';
@@ -8,7 +9,8 @@ export const store = configureStore({
app: appReducer,
collections: collectionsReducer,
tabs: tabsReducer
- }
+ },
+ middleware: (getDefaultMiddleware) => getDefaultMiddleware().prepend(listenerMiddleware.middleware)
});
export default store;
diff --git a/packages/bruno-app/src/providers/ReduxStore/middlewares/listenerMiddleware.js b/packages/bruno-app/src/providers/ReduxStore/middlewares/listenerMiddleware.js
new file mode 100644
index 000000000..27f2e894b
--- /dev/null
+++ b/packages/bruno-app/src/providers/ReduxStore/middlewares/listenerMiddleware.js
@@ -0,0 +1,123 @@
+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;
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/app.js b/packages/bruno-app/src/providers/ReduxStore/slices/app.js
index 622a4a7bd..054ebf737 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/app.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/app.js
@@ -1,5 +1,11 @@
import { createSlice } from '@reduxjs/toolkit';
+import filter from 'lodash/filter';
+import groupBy from 'lodash/groupBy';
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 = {
isDragging: false,
@@ -21,7 +27,8 @@ const initialState = {
codeFont: 'default'
}
},
- cookies: []
+ cookies: [],
+ eventsQueue: []
};
export const appSlice = createSlice({
@@ -54,6 +61,19 @@ export const appSlice = createSlice({
},
updateCookies: (state, action) => {
state.cookies = action.payload;
+ },
+ insertEventsIntoQueue: (state, action) => {
+ state.eventsQueue = state.eventsQueue.concat(action.payload);
+ },
+ removeEventsFromQueue: (state, action) => {
+ const eventsToRemove = action.payload;
+ state.eventsQueue = filter(
+ state.eventsQueue,
+ (event) => !eventsToRemove.some((e) => e.eventUid === event.eventUid)
+ );
+ },
+ removeAllEventsFromQueue: (state) => {
+ state.eventsQueue = [];
}
}
});
@@ -67,7 +87,10 @@ export const {
hideHomePage,
showPreferences,
updatePreferences,
- updateCookies
+ updateCookies,
+ insertEventsIntoQueue,
+ removeEventsFromQueue,
+ removeAllEventsFromQueue
} = appSlice.actions;
export const savePreferences = (preferences) => (dispatch, getState) => {
@@ -95,4 +118,55 @@ 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) => {
+ const { ipcRenderer } = window;
+ return ipcRenderer.invoke('main:complete-quit-flow');
+};
+
export default appSlice.reducer;
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
index 9c799c234..e393e40f4 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -1,51 +1,45 @@
-import path from 'path';
-import toast from 'react-hot-toast';
-import trim from 'lodash/trim';
+import { collectionSchema, environmentSchema, itemSchema } from '@usebruno/schema';
+import cloneDeep from 'lodash/cloneDeep';
+import filter from 'lodash/filter';
import find from 'lodash/find';
import get from 'lodash/get';
-import filter from 'lodash/filter';
-import { uuid } from 'utils/common';
-import cloneDeep from 'lodash/cloneDeep';
+import trim from 'lodash/trim';
+import path from 'path';
+import { insertEventsIntoQueue } from 'providers/ReduxStore/slices/app';
+import toast from 'react-hot-toast';
import {
- findItemInCollection,
- moveCollectionItem,
- getItemsToResequence,
- moveCollectionItemToRootOfCollection,
findCollectionByUid,
- transformRequestToSaveToFilesystem,
- findParentItemInCollection,
findEnvironmentInCollection,
- isItemARequest,
+ findItemInCollection,
+ findParentItemInCollection,
+ getItemsToResequence,
isItemAFolder,
- refreshUidsInItem
+ isItemARequest,
+ moveCollectionItem,
+ moveCollectionItemToRootOfCollection,
+ refreshUidsInItem,
+ transformRequestToSaveToFilesystem
} from 'utils/collections';
-import { collectionSchema, itemSchema, environmentSchema, environmentsSchema } from '@usebruno/schema';
-import { waitForNextTick } from 'utils/common';
-import { getDirectoryName, isWindowsOS, PATH_SEPARATOR } from 'utils/common/platform';
-import { sendNetworkRequest, cancelNetworkRequest } from 'utils/network';
+import { uuid, waitForNextTick } from 'utils/common';
+import { PATH_SEPARATOR, getDirectoryName } from 'utils/common/platform';
+import { cancelNetworkRequest, sendNetworkRequest } from 'utils/network';
import {
- updateLastAction,
- updateNextAction,
- resetRunResults,
- requestCancelled,
- responseReceived,
- newItem as _newItem,
- cloneItem as _cloneItem,
- deleteItem as _deleteItem,
- saveRequest as _saveRequest,
- selectEnvironment as _selectEnvironment,
+ collectionAddEnvFileEvent as _collectionAddEnvFileEvent,
createCollection as _createCollection,
- renameCollection as _renameCollection,
removeCollection as _removeCollection,
+ selectEnvironment as _selectEnvironment,
sortCollections as _sortCollections,
- collectionAddEnvFileEvent as _collectionAddEnvFileEvent
+ requestCancelled,
+ resetRunResults,
+ responseReceived,
+ updateLastAction
} from './index';
+import { each } from 'lodash';
import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
import { resolveRequestFilename } from 'utils/common/platform';
import { parseQueryParams, splitOnFirst } from 'utils/url/index';
-import { each } from 'lodash';
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
const state = getState();
@@ -595,7 +589,6 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
urlParam.enabled = true;
});
- const collectionCopy = cloneDeep(collection);
const item = {
uid: uuid(),
type: requestType,
@@ -632,18 +625,16 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
- // the useCollectionNextAction() will track this and open the new request in a new tab
- // once the request is created
+ // listener middleware will track this and open the new request in a new tab once request is created
dispatch(
- updateNextAction({
- nextAction: {
- type: 'OPEN_REQUEST',
- payload: {
- pathname: fullName
- }
- },
- collectionUid
- })
+ insertEventsIntoQueue([
+ {
+ eventUid: uuid(),
+ eventType: 'OPEN_REQUEST',
+ collectionUid,
+ itemPathname: fullName
+ }
+ ])
);
} else {
return reject(new Error('Duplicate request names are not allowed under the same folder'));
@@ -662,19 +653,16 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
-
- // the useCollectionNextAction() will track this and open the new request in a new tab
- // once the request is created
+ // listener middleware will track this and open the new request in a new tab once request is created
dispatch(
- updateNextAction({
- nextAction: {
- type: 'OPEN_REQUEST',
- payload: {
- pathname: fullName
- }
- },
- collectionUid
- })
+ insertEventsIntoQueue([
+ {
+ eventUid: uuid(),
+ eventType: 'OPEN_REQUEST',
+ collectionUid,
+ itemPathname: fullName
+ }
+ ])
);
} else {
return reject(new Error('Duplicate request names are not allowed under the same folder'));
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
index ae2df4a0b..09bf07061 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -1,30 +1,29 @@
-import { uuid } from 'utils/common';
-import find from 'lodash/find';
-import map from 'lodash/map';
-import forOwn from 'lodash/forOwn';
-import concat from 'lodash/concat';
-import filter from 'lodash/filter';
-import each from 'lodash/each';
-import cloneDeep from 'lodash/cloneDeep';
-import get from 'lodash/get';
-import set from 'lodash/set';
import { createSlice } from '@reduxjs/toolkit';
-import { splitOnFirst } from 'utils/url';
+import cloneDeep from 'lodash/cloneDeep';
+import concat from 'lodash/concat';
+import each from 'lodash/each';
+import filter from 'lodash/filter';
+import find from 'lodash/find';
+import forOwn from 'lodash/forOwn';
+import get from 'lodash/get';
+import map from 'lodash/map';
+import set from 'lodash/set';
import {
- findCollectionByUid,
- findCollectionByPathname,
- findItemInCollection,
- findEnvironmentInCollection,
- findItemInCollectionByPathname,
addDepth,
+ areItemsTheSameExceptSeqUpdate,
collapseCollection,
deleteItemInCollection,
deleteItemInCollectionByPathname,
- isItemARequest,
- areItemsTheSameExceptSeqUpdate
+ findCollectionByPathname,
+ findCollectionByUid,
+ findEnvironmentInCollection,
+ findItemInCollection,
+ findItemInCollectionByPathname,
+ isItemARequest
} from 'utils/collections';
-import { parseQueryParams, stringifyQueryParams } from 'utils/url';
-import { getSubdirectoriesFromRoot, getDirectoryName, PATH_SEPARATOR } from 'utils/common/platform';
+import { uuid } from 'utils/common';
+import { PATH_SEPARATOR, getDirectoryName, getSubdirectoriesFromRoot } from 'utils/common/platform';
+import { parseQueryParams, splitOnFirst, stringifyQueryParams } from 'utils/url';
const initialState = {
collections: [],
@@ -50,10 +49,6 @@ export const collectionsSlice = createSlice({
collection.importedAt = new Date().getTime();
collection.lastAction = null;
- // an improvement over the above approach.
- // this defines an action that need to be performed next and is executed vy the useCollectionNextAction()
- collection.nextAction = null;
-
collapseCollection(collection);
addDepth(collection.items);
if (!collectionUids.includes(collection.uid)) {
@@ -100,14 +95,6 @@ export const collectionsSlice = createSlice({
collection.lastAction = lastAction;
}
},
- updateNextAction: (state, action) => {
- const { collectionUid, nextAction } = action.payload;
- const collection = findCollectionByUid(state.collections, collectionUid);
-
- if (collection) {
- collection.nextAction = nextAction;
- }
- },
updateSettingsSelectedTab: (state, action) => {
const { collectionUid, tab } = action.payload;
@@ -1394,7 +1381,6 @@ export const {
removeCollection,
sortCollections,
updateLastAction,
- updateNextAction,
updateSettingsSelectedTab,
collectionUnlinkEnvFileEvent,
saveEnvironment,
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js
index 3cf070d68..ffc4254a5 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js
@@ -1,7 +1,11 @@
-import find from 'lodash/find';
-import filter from 'lodash/filter';
-import last from 'lodash/last';
import { createSlice } from '@reduxjs/toolkit';
+import filter from 'lodash/filter';
+import find from 'lodash/find';
+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
@@ -101,6 +105,11 @@ export const tabsSlice = createSlice({
const collectionUid = action.payload.collectionUid;
state.tabs = filter(state.tabs, (t) => t.collectionUid !== collectionUid);
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;
}
}
});
@@ -112,7 +121,46 @@ export const {
updateRequestPaneTab,
updateResponsePaneTab,
closeTabs,
- closeAllCollectionTabs
+ closeAllCollectionTabs,
+ setShowConfirmClose
} = 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;
diff --git a/packages/bruno-app/src/utils/events-queue/index.js b/packages/bruno-app/src/utils/events-queue/index.js
new file mode 100644
index 000000000..694b8bc15
--- /dev/null
+++ b/packages/bruno-app/src/utils/events-queue/index.js
@@ -0,0 +1,9 @@
+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;
+};
diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js
index c9095b3a4..d82b93a42 100644
--- a/packages/bruno-electron/src/index.js
+++ b/packages/bruno-electron/src/index.js
@@ -1,7 +1,7 @@
const path = require('path');
const isDev = require('electron-is-dev');
const { format } = require('url');
-const { BrowserWindow, app, Menu } = require('electron');
+const { BrowserWindow, app, Menu, ipcMain } = require('electron');
const { setContentSecurityPolicy } = require('electron-util');
const menuTemplate = require('./app/menu-template');
@@ -97,6 +97,10 @@ app.on('ready', async () => {
mainWindow.on('maximize', () => saveMaximized(true));
mainWindow.on('unmaximize', () => saveMaximized(false));
+ mainWindow.on('close', (e) => {
+ e.preventDefault();
+ ipcMain.emit('main:start-quit-flow');
+ });
mainWindow.webContents.on('will-redirect', (event, url) => {
event.preventDefault();
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index 76bb661d8..9cb9829ef 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -1,7 +1,7 @@
const _ = require('lodash');
const fs = require('fs');
const path = require('path');
-const { ipcMain, shell, dialog } = require('electron');
+const { ipcMain, shell, dialog, app } = require('electron');
const { envJsonToBru, bruToJson, jsonToBru, jsonToCollectionBru } = require('../bru');
const {
@@ -585,6 +585,14 @@ const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) =
watcher.addWatcher(win, pathname, uid);
lastOpenedCollections.add(pathname);
});
+
+ ipcMain.on('main:start-quit-flow', () => {
+ mainWindow.webContents.send('main:start-quit-flow');
+ });
+
+ ipcMain.handle('main:complete-quit-flow', () => {
+ mainWindow.destroy();
+ });
};
const registerCollectionsIpc = (mainWindow, watcher, lastOpenedCollections) => {