[Feature] Prompt user to save requests before exiting app (#1317)

* Starting quit flow and focusing in draft
* Finishing app if there is no draft to save
* Automatically opening request after creation through event queue
* Fix remove events from queue using pathname to find item
* Removing updateNextAction
* Listening via predicate
* Confirm close dialog toggle moved to store
* Draft operations as tab actions
* Complete quit flow
* Fixing close app/window hooks
* Breaking the chain when dismissing dialog
* Displaying request name in ConfirmRequestClose modal
* Added disableEscapeKey and disableCloseOnOutsideClick props to Modal (passed in ConfirmRequestClose)
* Removing logs
* Refactor
* listenerMiddleware module
* ipc events listeners names
* Update next action
* Helpful comments
* Eventually handle events to close request even if is no draft
* Request name in bold
This commit is contained in:
Ricardo Silverio 2024-01-08 08:51:55 -03:00 committed by GitHub
parent bdfcd78f3a
commit 85f24eec77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 404 additions and 198 deletions

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import StyledWrapper from './StyledWrapper';
const ModalHeader = ({ title, handleCancel }) => (
@ -62,6 +62,8 @@ const Modal = ({
confirmDisabled,
hideCancel,
hideFooter,
disableCloseOnOutsideClick,
disableEscapeKey,
closeModalFadeTimeout = 500
}) => {
const [isClosing, setIsClosing] = useState(false);
@ -78,6 +80,7 @@ const Modal = ({
};
useEffect(() => {
if (disableEscapeKey) return;
document.addEventListener('keydown', escFunction, false);
return () => {
@ -111,9 +114,13 @@ const Modal = ({
{/* Clicking on backdrop closes the modal */}
<div
className="bruno-modal-backdrop"
onClick={() => {
onClick={
disableCloseOnOutsideClick
? null
: () => {
closeModal({ type: 'backdrop' });
}}
}
}
/>
</StyledWrapper>
);

View File

@ -1,7 +1,7 @@
import Modal from 'components/Modal';
import React from 'react';
const ConfirmRequestClose = ({ onCancel, onCloseWithoutSave, onSaveAndClose }) => {
const ConfirmRequestClose = ({ item, onCancel, onCloseWithoutSave, onSaveAndClose }) => {
const _handleCancel = ({ type }) => {
if (type === 'button') {
return onCloseWithoutSave();
@ -22,7 +22,9 @@ const ConfirmRequestClose = ({ onCancel, onCloseWithoutSave, onSaveAndClose }) =
disableCloseOnOutsideClick={true}
closeModalFadeTimeout={150}
>
<div className="font-normal">You have unsaved changes in your request.</div>
<div className="font-normal">
You have unsaved changes in request <span className="font-semibold">{item.name}</span>.
</div>
</Modal>
);
};

View File

@ -1,22 +1,25 @@
import React, { useState } from 'react';
import get from 'lodash/get';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { deleteRequestDraft } from 'providers/ReduxStore/slices/collections';
import { useDispatch } from 'react-redux';
import { findItemInCollection } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
import RequestTabNotFound from './RequestTabNotFound';
import ConfirmRequestClose from './ConfirmRequestClose';
import SpecialTab from './SpecialTab';
import {
cancelCloseDraft,
closeAndSaveDraft,
closeTabs,
closeWithoutSavingDraft,
setShowConfirmClose
} from 'providers/ReduxStore/slices/tabs';
import { useTheme } from 'providers/Theme';
import React from 'react';
import { useDispatch } from 'react-redux';
import darkTheme from 'themes/dark';
import lightTheme from 'themes/light';
import { findItemInCollection } from 'utils/collections';
import ConfirmRequestClose from './ConfirmRequestClose';
import RequestTabNotFound from './RequestTabNotFound';
import SpecialTab from './SpecialTab';
import StyledWrapper from './StyledWrapper';
const RequestTab = ({ tab, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const [showConfirmClose, setShowConfirmClose] = useState(false);
const handleCloseClick = (event) => {
event.stopPropagation();
@ -28,6 +31,15 @@ const RequestTab = ({ tab, collection }) => {
);
};
const showConfirmClose = () => {
dispatch(
setShowConfirmClose({
tabUid: tab.uid,
showConfirmClose: true
})
);
};
const getMethodColor = (method = '') => {
const theme = storedTheme === 'dark' ? darkTheme : lightTheme;
@ -90,37 +102,12 @@ const RequestTab = ({ tab, collection }) => {
return (
<StyledWrapper className="flex items-center justify-between tab-container px-1">
{showConfirmClose && (
{tab.showConfirmClose && (
<ConfirmRequestClose
onCancel={() => 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))}
/>
)}
<div className="flex items-baseline tab-label pl-2">
@ -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 ? (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
insertEventsIntoQueue([
{
eventUid: uuid(),
eventType: 'OPEN_REQUEST',
collectionUid,
itemPathname: fullName
}
},
collectionUid
})
])
);
} 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
insertEventsIntoQueue([
{
eventUid: uuid(),
eventType: 'OPEN_REQUEST',
collectionUid,
itemPathname: fullName
}
},
collectionUid
})
])
);
} else {
return reject(new Error('Duplicate request names are not allowed under the same folder'));

View File

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

View File

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

View File

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

View File

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

View File

@ -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) => {