feat(#1162): warn if there are unsaved requests when quitting

This commit is contained in:
Anoop M D 2024-01-09 14:01:04 +05:30
parent 85f24eec77
commit 3e627522b7
16 changed files with 330 additions and 300 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
export const taskTypes = {
OPEN_REQUEST: 'OPEN_REQUEST'
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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