mirror of
https://github.com/usebruno/bruno.git
synced 2025-06-02 00:05:53 +02:00
request save control
This commit is contained in:
parent
1e27427d09
commit
12f3f802a7
@ -61,19 +61,20 @@ const Modal = ({
|
|||||||
children,
|
children,
|
||||||
confirmDisabled,
|
confirmDisabled,
|
||||||
hideCancel,
|
hideCancel,
|
||||||
hideFooter
|
hideFooter,
|
||||||
|
closeModalFadeTimeout = 500
|
||||||
}) => {
|
}) => {
|
||||||
const [isClosing, setIsClosing] = useState(false);
|
const [isClosing, setIsClosing] = useState(false);
|
||||||
const escFunction = (event) => {
|
const escFunction = (event) => {
|
||||||
const escKeyCode = 27;
|
const escKeyCode = 27;
|
||||||
if (event.keyCode === escKeyCode) {
|
if (event.keyCode === escKeyCode) {
|
||||||
closeModal();
|
closeModal({ type: 'esc' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = (args) => {
|
||||||
setIsClosing(true);
|
setIsClosing(true);
|
||||||
setTimeout(() => handleCancel(), 500);
|
setTimeout(() => handleCancel(args), closeModalFadeTimeout);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -94,12 +95,12 @@ const Modal = ({
|
|||||||
return (
|
return (
|
||||||
<StyledWrapper className={classes}>
|
<StyledWrapper className={classes}>
|
||||||
<div className={`bruno-modal-card modal-${size}`}>
|
<div className={`bruno-modal-card modal-${size}`}>
|
||||||
<ModalHeader title={title} handleCancel={() => closeModal()} />
|
<ModalHeader title={title} handleCancel={() => closeModal({ type: 'icon' })} />
|
||||||
<ModalContent>{children}</ModalContent>
|
<ModalContent>{children}</ModalContent>
|
||||||
<ModalFooter
|
<ModalFooter
|
||||||
confirmText={confirmText}
|
confirmText={confirmText}
|
||||||
cancelText={cancelText}
|
cancelText={cancelText}
|
||||||
handleCancel={() => closeModal()}
|
handleCancel={() => closeModal({ type: 'button' })}
|
||||||
handleSubmit={handleConfirm}
|
handleSubmit={handleConfirm}
|
||||||
confirmDisabled={confirmDisabled}
|
confirmDisabled={confirmDisabled}
|
||||||
hideCancel={hideCancel}
|
hideCancel={hideCancel}
|
||||||
@ -108,7 +109,12 @@ const Modal = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Clicking on backdrop closes the modal */}
|
{/* Clicking on backdrop closes the modal */}
|
||||||
<div className="bruno-modal-backdrop" onClick={() => closeModal()} />
|
<div
|
||||||
|
className="bruno-modal-backdrop"
|
||||||
|
onClick={() => {
|
||||||
|
closeModal({ type: 'backdrop' });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -32,6 +32,50 @@ const Wrapper = styled.div`
|
|||||||
position: relative;
|
position: relative;
|
||||||
top: 1px;
|
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;
|
export default Wrapper;
|
||||||
|
@ -6,7 +6,9 @@ import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
|||||||
import HttpMethodSelector from './HttpMethodSelector';
|
import HttpMethodSelector from './HttpMethodSelector';
|
||||||
import { useTheme } from 'providers/Theme';
|
import { useTheme } from 'providers/Theme';
|
||||||
import SendIcon from 'components/Icons/Send';
|
import SendIcon from 'components/Icons/Send';
|
||||||
|
import { IconDeviceFloppy } from '@tabler/icons';
|
||||||
import SingleLineEditor from 'components/SingleLineEditor';
|
import SingleLineEditor from 'components/SingleLineEditor';
|
||||||
|
import { isMacOS } from 'utils/common/platform';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
const QueryUrl = ({ item, collection, handleRun }) => {
|
const QueryUrl = ({ item, collection, handleRun }) => {
|
||||||
@ -14,6 +16,8 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
|
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 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);
|
const [methodSelectorWidth, setMethodSelectorWidth] = useState(90);
|
||||||
|
|
||||||
@ -22,7 +26,10 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
|||||||
setMethodSelectorWidth(el.offsetWidth);
|
setMethodSelectorWidth(el.offsetWidth);
|
||||||
}, [method]);
|
}, [method]);
|
||||||
|
|
||||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
const onSave = () => {
|
||||||
|
dispatch(saveRequest(item.uid, collection.uid));
|
||||||
|
};
|
||||||
|
|
||||||
const onUrlChange = (value) => {
|
const onUrlChange = (value) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
requestUrlChanged({
|
requestUrlChanged({
|
||||||
@ -65,6 +72,24 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
|||||||
collection={collection}
|
collection={collection}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
|
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
|
||||||
|
<div
|
||||||
|
className="tooltip mr-3"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!item.draft) return;
|
||||||
|
onSave();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconDeviceFloppy
|
||||||
|
color={item.draft ? theme.colors.text.yellow : theme.requestTabs.icon.color}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
size={22}
|
||||||
|
className={`${item.draft ? 'cursor-pointer' : 'cursor-default'}`}
|
||||||
|
/>
|
||||||
|
<span className="tooltiptext text-xs">
|
||||||
|
Save <span className="shortcut">({saveShortcut})</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<SendIcon color={theme.requestTabPanel.url.icon} width={22} />
|
<SendIcon color={theme.requestTabPanel.url.icon} width={22} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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 (
|
||||||
|
<Modal
|
||||||
|
size="sm"
|
||||||
|
title="Unsaved changes"
|
||||||
|
confirmText="Save and Close"
|
||||||
|
cancelText="Close without saving"
|
||||||
|
handleConfirm={onSaveAndClose}
|
||||||
|
handleCancel={_handleCancel}
|
||||||
|
disableEscapeKey={true}
|
||||||
|
disableCloseOnOutsideClick={true}
|
||||||
|
closeModalFadeTimeout={150}
|
||||||
|
>
|
||||||
|
<div className="font-normal">You have unsaved changes in you request.</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfirmRequestClose;
|
@ -1,10 +1,13 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
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 { useDispatch } from 'react-redux';
|
||||||
import { findItemInCollection } from 'utils/collections';
|
import { findItemInCollection } from 'utils/collections';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
import RequestTabNotFound from './RequestTabNotFound';
|
import RequestTabNotFound from './RequestTabNotFound';
|
||||||
|
import ConfirmRequestClose from './ConfirmRequestClose';
|
||||||
import SpecialTab from './SpecialTab';
|
import SpecialTab from './SpecialTab';
|
||||||
import { useTheme } from 'providers/Theme';
|
import { useTheme } from 'providers/Theme';
|
||||||
import darkTheme from 'themes/dark';
|
import darkTheme from 'themes/dark';
|
||||||
@ -13,6 +16,7 @@ import lightTheme from 'themes/light';
|
|||||||
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();
|
||||||
@ -86,6 +90,39 @@ 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">
|
||||||
|
{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);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="flex items-baseline tab-label pl-2">
|
<div className="flex items-baseline tab-label pl-2">
|
||||||
<span className="tab-method uppercase" style={{ color: getMethodColor(method), fontSize: 12 }}>
|
<span className="tab-method uppercase" style={{ color: getMethodColor(method), fontSize: 12 }}>
|
||||||
{method}
|
{method}
|
||||||
@ -94,7 +131,14 @@ const RequestTab = ({ tab, collection }) => {
|
|||||||
{item.name}
|
{item.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
|
<div
|
||||||
|
className="flex px-2 close-icon-container"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (!item.draft) return handleCloseClick(e);
|
||||||
|
|
||||||
|
setShowConfirmClose(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
{!item.draft ? (
|
{!item.draft ? (
|
||||||
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
|
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
|
||||||
<path
|
<path
|
||||||
|
@ -82,8 +82,12 @@ export const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => {
|
|||||||
itemSchema
|
itemSchema
|
||||||
.validate(itemToSave)
|
.validate(itemToSave)
|
||||||
.then(() => ipcRenderer.invoke('renderer:save-request', item.pathname, itemToSave))
|
.then(() => ipcRenderer.invoke('renderer:save-request', item.pathname, itemToSave))
|
||||||
|
.then(() => toast.success('Request saved successfully'))
|
||||||
.then(resolve)
|
.then(resolve)
|
||||||
.catch(reject);
|
.catch((err) => {
|
||||||
|
toast.error('Failed to save request!');
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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) => {
|
newEphemeralHttpRequest: (state, action) => {
|
||||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||||
|
|
||||||
@ -1224,6 +1235,7 @@ export const {
|
|||||||
requestCancelled,
|
requestCancelled,
|
||||||
responseReceived,
|
responseReceived,
|
||||||
saveRequest,
|
saveRequest,
|
||||||
|
deleteRequestDraft,
|
||||||
newEphemeralHttpRequest,
|
newEphemeralHttpRequest,
|
||||||
collectionClicked,
|
collectionClicked,
|
||||||
collectionFolderClicked,
|
collectionFolderClicked,
|
||||||
|
@ -41,3 +41,10 @@ export const isWindowsOS = () => {
|
|||||||
|
|
||||||
return osFamily.includes('windows');
|
return osFamily.includes('windows');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isMacOS = () => {
|
||||||
|
const os = platform.os;
|
||||||
|
const osFamily = os.family.toLowerCase();
|
||||||
|
|
||||||
|
return osFamily.includes('os x');
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user