diff --git a/packages/bruno-app/src/components/Modal/index.js b/packages/bruno-app/src/components/Modal/index.js index 213c62efe..ec715a03d 100644 --- a/packages/bruno-app/src/components/Modal/index.js +++ b/packages/bruno-app/src/components/Modal/index.js @@ -61,19 +61,20 @@ const Modal = ({ children, confirmDisabled, hideCancel, - hideFooter + hideFooter, + closeModalFadeTimeout = 500 }) => { const [isClosing, setIsClosing] = useState(false); const escFunction = (event) => { const escKeyCode = 27; if (event.keyCode === escKeyCode) { - closeModal(); + closeModal({ type: 'esc' }); } }; - const closeModal = () => { + const closeModal = (args) => { setIsClosing(true); - setTimeout(() => handleCancel(), 500); + setTimeout(() => handleCancel(args), closeModalFadeTimeout); }; useEffect(() => { @@ -94,12 +95,12 @@ const Modal = ({ return (
- closeModal()} /> + closeModal({ type: 'icon' })} /> {children} closeModal()} + handleCancel={() => closeModal({ type: 'button' })} handleSubmit={handleConfirm} confirmDisabled={confirmDisabled} hideCancel={hideCancel} @@ -108,7 +109,12 @@ const Modal = ({
{/* Clicking on backdrop closes the modal */} -
closeModal()} /> +
{ + closeModal({ type: 'backdrop' }); + }} + /> ); }; diff --git a/packages/bruno-app/src/components/RequestPane/QueryUrl/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/QueryUrl/StyledWrapper.js index 7ec35f33e..2308dec4f 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryUrl/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/QueryUrl/StyledWrapper.js @@ -32,6 +32,50 @@ const Wrapper = styled.div` position: relative; top: 1px; } + + .tooltip { + position: relative; + display: inline-block; + cursor: pointer; + } + + .tooltip:hover .tooltiptext { + visibility: visible; + opacity: 1; + } + + .tooltiptext { + visibility: hidden; + width: auto; + background-color: ${(props) => props.theme.requestTabs.active.bg}; + color: ${(props) => props.theme.text}; + text-align: center; + border-radius: 4px; + padding: 4px 8px; + position: absolute; + z-index: 1; + bottom: 34px; + left: 50%; + transform: translateX(-50%); + opacity: 0; + transition: opacity 0.3s; + white-space: nowrap; + } + + .tooltiptext::after { + content: ''; + position: absolute; + top: 100%; + left: 50%; + margin-left: -4px; + border-width: 4px; + border-style: solid; + border-color: ${(props) => props.theme.requestTabs.active.bg} transparent transparent transparent; + } + + .shortcut { + font-size: 0.625rem; + } `; export default Wrapper; diff --git a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js index f783bb9d5..4b5b68a00 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js @@ -6,7 +6,9 @@ import { saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import HttpMethodSelector from './HttpMethodSelector'; import { useTheme } from 'providers/Theme'; import SendIcon from 'components/Icons/Send'; +import { IconDeviceFloppy } from '@tabler/icons'; import SingleLineEditor from 'components/SingleLineEditor'; +import { isMacOS } from 'utils/common/platform'; import StyledWrapper from './StyledWrapper'; const QueryUrl = ({ item, collection, handleRun }) => { @@ -14,6 +16,8 @@ const QueryUrl = ({ item, collection, handleRun }) => { const dispatch = useDispatch(); const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method'); const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url'); + const isMac = isMacOS(); + const saveShortcut = isMac ? 'Cmd + S' : 'Ctrl + S'; const [methodSelectorWidth, setMethodSelectorWidth] = useState(90); @@ -22,7 +26,10 @@ const QueryUrl = ({ item, collection, handleRun }) => { setMethodSelectorWidth(el.offsetWidth); }, [method]); - const onSave = () => dispatch(saveRequest(item.uid, collection.uid)); + const onSave = () => { + dispatch(saveRequest(item.uid, collection.uid)); + }; + const onUrlChange = (value) => { dispatch( requestUrlChanged({ @@ -65,6 +72,24 @@ const QueryUrl = ({ item, collection, handleRun }) => { collection={collection} />
+
{ + e.stopPropagation(); + if (!item.draft) return; + onSave(); + }} + > + + + Save ({saveShortcut}) + +
diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/ConfirmRequestClose/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/ConfirmRequestClose/index.js new file mode 100644 index 000000000..94460cf48 --- /dev/null +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/ConfirmRequestClose/index.js @@ -0,0 +1,30 @@ +import React from 'react'; +import Modal from 'components/Modal'; + +const ConfirmRequestClose = ({ onCancel, onCloseWithoutSave, onSaveAndClose }) => { + const _handleCancel = ({ type }) => { + if (type === 'button') { + return onCloseWithoutSave(); + } + + return onCancel(); + }; + + return ( + +
You have unsaved changes in you request.
+
+ ); +}; + +export default ConfirmRequestClose; diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js index 9b7b59a30..8fc27e90c 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js @@ -1,10 +1,13 @@ -import React from 'react'; +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 { useTheme } from 'providers/Theme'; import darkTheme from 'themes/dark'; @@ -13,6 +16,7 @@ import lightTheme from 'themes/light'; const RequestTab = ({ tab, collection }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); + const [showConfirmClose, setShowConfirmClose] = useState(false); const handleCloseClick = (event) => { event.stopPropagation(); @@ -86,6 +90,39 @@ const RequestTab = ({ tab, collection }) => { return ( + {showConfirmClose && ( + 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); + }); + }} + /> + )}
{method} @@ -94,7 +131,14 @@ const RequestTab = ({ tab, collection }) => { {item.name}
-
handleCloseClick(e)}> +
{ + if (!item.draft) return handleCloseClick(e); + + setShowConfirmClose(true); + }} + > {!item.draft ? ( (dispatch, getState) => { itemSchema .validate(itemToSave) .then(() => ipcRenderer.invoke('renderer:save-request', item.pathname, itemToSave)) + .then(() => toast.success('Request saved successfully')) .then(resolve) - .catch(reject); + .catch((err) => { + toast.error('Failed to save request!'); + reject(err); + }); }); }; 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 31cf6d6ca..f1f9699a8 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -258,6 +258,17 @@ export const collectionsSlice = createSlice({ } } }, + deleteRequestDraft: (state, action) => { + const collection = findCollectionByUid(state.collections, action.payload.collectionUid); + + if (collection) { + const item = findItemInCollection(collection, action.payload.itemUid); + + if (item && item.draft) { + item.draft = null; + } + } + }, newEphemeralHttpRequest: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); @@ -1224,6 +1235,7 @@ export const { requestCancelled, responseReceived, saveRequest, + deleteRequestDraft, newEphemeralHttpRequest, collectionClicked, collectionFolderClicked, diff --git a/packages/bruno-app/src/utils/common/platform.js b/packages/bruno-app/src/utils/common/platform.js index e49a66ec9..771daaf14 100644 --- a/packages/bruno-app/src/utils/common/platform.js +++ b/packages/bruno-app/src/utils/common/platform.js @@ -41,3 +41,10 @@ export const isWindowsOS = () => { return osFamily.includes('windows'); }; + +export const isMacOS = () => { + const os = platform.os; + const osFamily = os.family.toLowerCase(); + + return osFamily.includes('os x'); +};