From 3e627522b735d18d1ec7229b829c65952c6687cc Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Tue, 9 Jan 2024 14:01:04 +0530 Subject: [PATCH] feat(#1162): warn if there are unsaved requests when quitting --- .../bruno-app/src/components/Modal/index.js | 2 +- .../RequestTabs/RequestTab/index.js | 57 ++++---- .../App/ConfirmAppClose/SaveRequestsModal.js | 114 ++++++++++++++++ .../providers/App/ConfirmAppClose/index.js | 32 +++++ packages/bruno-app/src/providers/App/index.js | 10 +- .../src/providers/App/useIpcEvents.js | 9 +- .../src/providers/ReduxStore/index.js | 4 +- .../middlewares/listenerMiddleware.js | 123 ------------------ .../middlewares/tasks/middleware.js | 52 ++++++++ .../ReduxStore/middlewares/tasks/utils.js | 3 + .../src/providers/ReduxStore/slices/app.js | 75 ++--------- .../ReduxStore/slices/collections/actions.js | 66 +++++++--- .../src/providers/ReduxStore/slices/tabs.js | 50 +------ packages/bruno-app/src/utils/common/index.js | 4 + .../bruno-app/src/utils/events-queue/index.js | 9 -- packages/bruno-electron/src/ipc/collection.js | 20 +++ 16 files changed, 330 insertions(+), 300 deletions(-) create mode 100644 packages/bruno-app/src/providers/App/ConfirmAppClose/SaveRequestsModal.js create mode 100644 packages/bruno-app/src/providers/App/ConfirmAppClose/index.js delete mode 100644 packages/bruno-app/src/providers/ReduxStore/middlewares/listenerMiddleware.js create mode 100644 packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/middleware.js create mode 100644 packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/utils.js delete mode 100644 packages/bruno-app/src/utils/events-queue/index.js diff --git a/packages/bruno-app/src/components/Modal/index.js b/packages/bruno-app/src/components/Modal/index.js index 41311881..7640464e 100644 --- a/packages/bruno-app/src/components/Modal/index.js +++ b/packages/bruno-app/src/components/Modal/index.js @@ -86,7 +86,7 @@ const Modal = ({ return () => { document.removeEventListener('keydown', escFunction, false); }; - }, []); + }, [disableEscapeKey, document]); let classes = 'bruno-modal'; if (isClosing) { diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js index 9c608d8d..63f4beba 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js @@ -1,13 +1,8 @@ +import React, { useState } from 'react'; import get from 'lodash/get'; -import { - cancelCloseDraft, - closeAndSaveDraft, - closeTabs, - closeWithoutSavingDraft, - setShowConfirmClose -} from 'providers/ReduxStore/slices/tabs'; +import { closeTabs } from 'providers/ReduxStore/slices/tabs'; +import { saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import { useTheme } from 'providers/Theme'; -import React from 'react'; import { useDispatch } from 'react-redux'; import darkTheme from 'themes/dark'; import lightTheme from 'themes/light'; @@ -20,6 +15,7 @@ import StyledWrapper from './StyledWrapper'; const RequestTab = ({ tab, collection }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); + const [showConfirmClose, setShowConfirmClose] = useState(false); const handleCloseClick = (event) => { event.stopPropagation(); @@ -31,15 +27,6 @@ const RequestTab = ({ tab, collection }) => { ); }; - const showConfirmClose = () => { - dispatch( - setShowConfirmClose({ - tabUid: tab.uid, - showConfirmClose: true - }) - ); - }; - const getMethodColor = (method = '') => { const theme = storedTheme === 'dark' ? darkTheme : lightTheme; @@ -102,12 +89,38 @@ const RequestTab = ({ tab, collection }) => { return ( - {tab.showConfirmClose && ( + {showConfirmClose && ( dispatch(cancelCloseDraft(item.uid))} - onCloseWithoutSave={() => dispatch(closeWithoutSavingDraft(item.uid, collection.uid))} - onSaveAndClose={() => dispatch(closeAndSaveDraft(item.uid, collection.uid))} + 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); + }); + }} /> )}
@@ -122,7 +135,7 @@ const RequestTab = ({ tab, collection }) => { className="flex px-2 close-icon-container" onClick={(e) => { if (!item.draft) return handleCloseClick(e); - showConfirmClose(); + setShowConfirmClose(true); }} > {!item.draft ? ( diff --git a/packages/bruno-app/src/providers/App/ConfirmAppClose/SaveRequestsModal.js b/packages/bruno-app/src/providers/App/ConfirmAppClose/SaveRequestsModal.js new file mode 100644 index 00000000..fb261bad --- /dev/null +++ b/packages/bruno-app/src/providers/App/ConfirmAppClose/SaveRequestsModal.js @@ -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 ( + +
+ +

Hold on..

+
+

+ Do you want to save the changes you made to the following{' '} + {currentDrafts.length} {pluralizeWord('request', currentDrafts.length)}? +

+ +
    + {currentDrafts.slice(0, MAX_UNSAVED_REQUESTS_TO_SHOW).map((item) => { + return ( +
  • + {item.filename} +
  • + ); + })} +
+ + {currentDrafts.length > MAX_UNSAVED_REQUESTS_TO_SHOW && ( +

+ ...{currentDrafts.length - MAX_UNSAVED_REQUESTS_TO_SHOW} additional{' '} + {pluralizeWord('request', currentDrafts.length - MAX_UNSAVED_REQUESTS_TO_SHOW)} not shown +

+ )} + +
+
+ +
+
+ + +
+
+
+ ); +}; + +export default SaveRequestsModal; diff --git a/packages/bruno-app/src/providers/App/ConfirmAppClose/index.js b/packages/bruno-app/src/providers/App/ConfirmAppClose/index.js new file mode 100644 index 00000000..15a36136 --- /dev/null +++ b/packages/bruno-app/src/providers/App/ConfirmAppClose/index.js @@ -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 setShowConfirmClose(false)} />; +}; + +export default ConfirmAppClose; diff --git a/packages/bruno-app/src/providers/App/index.js b/packages/bruno-app/src/providers/App/index.js index 1022b5ee..c54d5386 100644 --- a/packages/bruno-app/src/providers/App/index.js +++ b/packages/bruno-app/src/providers/App/index.js @@ -1,9 +1,10 @@ -import { refreshScreenWidth } from 'providers/ReduxStore/slices/app'; import React, { useEffect } from 'react'; 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 useTelemetry from './useTelemetry'; +import StyledWrapper from './StyledWrapper'; export const AppContext = React.createContext(); @@ -29,7 +30,10 @@ export const AppProvider = (props) => { return ( - {props.children} + + + {props.children} + ); }; diff --git a/packages/bruno-app/src/providers/App/useIpcEvents.js b/packages/bruno-app/src/providers/App/useIpcEvents.js index 58e902e5..53d5d195 100644 --- a/packages/bruno-app/src/providers/App/useIpcEvents.js +++ b/packages/bruno-app/src/providers/App/useIpcEvents.js @@ -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 { brunoConfigUpdateEvent, collectionAddDirectoryEvent, @@ -14,7 +15,6 @@ import { scriptEnvironmentUpdateEvent } from 'providers/ReduxStore/slices/collections'; import { collectionAddEnvFileEvent, openCollectionEvent } from 'providers/ReduxStore/slices/collections/actions'; -import { useEffect } from 'react'; import toast from 'react-hot-toast'; import { useDispatch } from 'react-redux'; import { isElectron } from 'utils/common/platform'; @@ -140,10 +140,6 @@ const useIpcEvents = () => { dispatch(updateCookies(val)); }); - const removeStartQuitFlowListener = ipcRenderer.on('main:start-quit-flow', () => { - dispatch(startQuitFlow()); - }); - return () => { removeCollectionTreeUpdateListener(); removeOpenCollectionListener(); @@ -159,7 +155,6 @@ const useIpcEvents = () => { 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 5ac8056f..269d1a59 100644 --- a/packages/bruno-app/src/providers/ReduxStore/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/index.js @@ -1,5 +1,5 @@ import { configureStore } from '@reduxjs/toolkit'; -import listenerMiddleware from './middlewares/listenerMiddleware'; +import tasksMiddleware from './middlewares/tasks/middleware'; import appReducer from './slices/app'; import collectionsReducer from './slices/collections'; import tabsReducer from './slices/tabs'; @@ -10,7 +10,7 @@ export const store = configureStore({ collections: collectionsReducer, tabs: tabsReducer }, - middleware: (getDefaultMiddleware) => getDefaultMiddleware().prepend(listenerMiddleware.middleware) + middleware: (getDefaultMiddleware) => getDefaultMiddleware().prepend(tasksMiddleware.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 deleted file mode 100644 index 27f2e894..00000000 --- a/packages/bruno-app/src/providers/ReduxStore/middlewares/listenerMiddleware.js +++ /dev/null @@ -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; diff --git a/packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/middleware.js b/packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/middleware.js new file mode 100644 index 00000000..66fe3862 --- /dev/null +++ b/packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/middleware.js @@ -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; diff --git a/packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/utils.js b/packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/utils.js new file mode 100644 index 00000000..4452e273 --- /dev/null +++ b/packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/utils.js @@ -0,0 +1,3 @@ +export const taskTypes = { + OPEN_REQUEST: 'OPEN_REQUEST' +}; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/app.js b/packages/bruno-app/src/providers/ReduxStore/slices/app.js index 054ebf73..c383099f 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/app.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/app.js @@ -1,11 +1,6 @@ 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, @@ -28,7 +23,7 @@ const initialState = { } }, cookies: [], - eventsQueue: [] + taskQueue: [] }; export const appSlice = createSlice({ @@ -62,18 +57,14 @@ export const appSlice = createSlice({ updateCookies: (state, action) => { state.cookies = action.payload; }, - insertEventsIntoQueue: (state, action) => { - state.eventsQueue = state.eventsQueue.concat(action.payload); + insertTaskIntoQueue: (state, action) => { + state.taskQueue.push(action.payload); }, - removeEventsFromQueue: (state, action) => { - const eventsToRemove = action.payload; - state.eventsQueue = filter( - state.eventsQueue, - (event) => !eventsToRemove.some((e) => e.eventUid === event.eventUid) - ); + removeTaskFromQueue: (state, action) => { + state.taskQueue = filter(state.taskQueue, (task) => task.uid !== action.payload.taskUid); }, - removeAllEventsFromQueue: (state) => { - state.eventsQueue = []; + removeAllTasksFromQueue: (state) => { + state.taskQueue = []; } } }); @@ -88,9 +79,9 @@ export const { showPreferences, updatePreferences, updateCookies, - insertEventsIntoQueue, - removeEventsFromQueue, - removeAllEventsFromQueue + insertTaskIntoQueue, + removeTaskFromQueue, + removeAllTasksFromQueue } = appSlice.actions; 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) => { const { ipcRenderer } = window; return ipcRenderer.invoke('main:complete-quit-flow'); 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 e393e40f..e8d7093e 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -5,7 +5,7 @@ import find from 'lodash/find'; import get from 'lodash/get'; import trim from 'lodash/trim'; 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 { 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) => { const state = getState(); const collection = findCollectionByUid(state.collections.collections, collectionUid); @@ -625,16 +657,14 @@ export const newHttpRequest = (params) => (dispatch, getState) => { const { ipcRenderer } = window; 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( - insertEventsIntoQueue([ - { - eventUid: uuid(), - eventType: 'OPEN_REQUEST', - collectionUid, - itemPathname: fullName - } - ]) + insertTaskIntoQueue({ + uid: uuid(), + type: 'OPEN_REQUEST', + collectionUid, + itemPathname: fullName + }) ); } else { 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; 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( - insertEventsIntoQueue([ - { - eventUid: uuid(), - eventType: 'OPEN_REQUEST', - collectionUid, - itemPathname: fullName - } - ]) + insertTaskIntoQueue({ + uid: uuid(), + type: '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/tabs.js b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js index ffc4254a..74c503da 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js @@ -2,10 +2,6 @@ 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 @@ -105,11 +101,6 @@ 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; } } }); @@ -121,46 +112,7 @@ export const { updateRequestPaneTab, updateResponsePaneTab, closeTabs, - closeAllCollectionTabs, - setShowConfirmClose + closeAllCollectionTabs } = 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/common/index.js b/packages/bruno-app/src/utils/common/index.js index 0df2de52..34a068e6 100644 --- a/packages/bruno-app/src/utils/common/index.js +++ b/packages/bruno-app/src/utils/common/index.js @@ -106,3 +106,7 @@ export const startsWith = (str, search) => { return str.substr(0, search.length) === search; }; + +export const pluralizeWord = (word, count) => { + return count === 1 ? word : `${word}s`; +}; diff --git a/packages/bruno-app/src/utils/events-queue/index.js b/packages/bruno-app/src/utils/events-queue/index.js deleted file mode 100644 index 694b8bc1..00000000 --- a/packages/bruno-app/src/utils/events-queue/index.js +++ /dev/null @@ -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; -}; diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 9cb9829e..e6a1c2c3 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -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 ipcMain.handle('renderer:create-environment', async (event, collectionPathname, name, variables) => { try { @@ -586,6 +605,7 @@ const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) = 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', () => { mainWindow.webContents.send('main:start-quit-flow'); });