Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add save prompt before closing requests using tab context menu #3225

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

This file was deleted.

236 changes: 129 additions & 107 deletions packages/bruno-app/src/components/RequestTabs/RequestTab/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import React, { useState, useRef, Fragment } from 'react';
import get from 'lodash/get';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import SaveRequestsModal from 'components/SaveRequestsModal';
import { deleteRequestDraft } from 'providers/ReduxStore/slices/collections';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } 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';
Expand All @@ -22,7 +21,7 @@ import { flattenItems } from 'utils/collections/index';
const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const [showConfirmClose, setShowConfirmClose] = useState(false);
const [tabsUidsToClose, setTabsUidsToClose] = useState([]);

const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
Expand Down Expand Up @@ -69,6 +68,20 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
return theme.request.methods[method.toLocaleLowerCase()];
};

const showSaveModalForCurrentTab = () => {
setTabsUidsToClose([tab.uid]);
}

const hideSaveModal = () => {
setTabsUidsToClose([]);
}

const handleCloseTabs = tabUids => {
const pendingSaveCount = tabUids.map(tabUid => findItemInCollection(collection, tabUid)).filter(item => item && item.draft).length;
if (pendingSaveCount === 0) return dispatch(closeTabs({ tabUids }));
setTabsUidsToClose(tabUids);
}

const folder = folderUid ? findItemInCollection(collection, folderUid) : null;
if (['collection-settings', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
return (
Expand All @@ -91,14 +104,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
return (
<StyledWrapper
className="flex items-center justify-between tab-container px-1"
onMouseUp={(e) => {
if (e.button === 1) {
e.preventDefault();
e.stopPropagation();

dispatch(closeTabs({ tabUids: [tab.uid] }));
}
}}
onMouseUp={handleMouseUp}
>
<RequestTabNotFound handleCloseClick={handleCloseClick} />
</StyledWrapper>
Expand All @@ -108,91 +114,108 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');

return (
<StyledWrapper className="flex items-center justify-between tab-container px-1">
{showConfirmClose && (
<ConfirmRequestClose
item={item}
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);
});
}}
/>
<>
{tabsUidsToClose.length > 0 && (
<SaveModal tabsUidsToClose={tabsUidsToClose} collection={collection} onCloseModal={hideSaveModal} />
)}
<div
className="flex items-baseline tab-label pl-2"
onContextMenu={handleRightClick}
onMouseUp={(e) => {
if (!item.draft) return handleMouseUp(e);
<StyledWrapper className="flex items-center justify-between tab-container px-1">
<div
className="flex items-baseline tab-label pl-2"
onContextMenu={handleRightClick}
onMouseUp={(e) => {
if (!item.draft) return handleMouseUp(e);

if (e.button === 1) {
e.stopPropagation();
e.preventDefault();
showSaveModalForCurrentTab();
}
}}
>
<span className="tab-method uppercase" style={{ color: getMethodColor(method), fontSize: 12 }}>
{method}
</span>
<span className="ml-1 tab-name" title={item.name}>
{item.name}
</span>
<RequestTabMenu
onDropdownCreate={onDropdownCreate}
onCloseTabs={handleCloseTabs}
tabIndex={tabIndex}
collectionRequestTabs={collectionRequestTabs}
tabItem={item}
collection={collection}
dropdownTippyRef={dropdownTippyRef}
dispatch={dispatch}
/>
</div>
<div
className="flex px-2 close-icon-container"
onClick={(e) => {
if (!item.draft) return handleCloseClick(e);

if (e.button === 1) {
e.stopPropagation();
e.preventDefault();
setShowConfirmClose(true);
}
}}
>
<span className="tab-method uppercase" style={{ color: getMethodColor(method), fontSize: 12 }}>
{method}
</span>
<span className="ml-1 tab-name" title={item.name}>
{item.name}
</span>
<RequestTabMenu
onDropdownCreate={onDropdownCreate}
tabIndex={tabIndex}
collectionRequestTabs={collectionRequestTabs}
tabItem={item}
collection={collection}
dropdownTippyRef={dropdownTippyRef}
dispatch={dispatch}
/>
</div>
<div
className="flex px-2 close-icon-container"
onClick={(e) => {
if (!item.draft) return handleCloseClick(e);

e.stopPropagation();
e.preventDefault();
setShowConfirmClose(true);
}}
>
{!item.draft ? (
<CloseTabIcon />
) : (
<DraftTabIcon />
)}
</div>
</StyledWrapper>
showSaveModalForCurrentTab();
}}
>
{!item.draft ? (
<CloseTabIcon />
) : (
<DraftTabIcon />
)}
</div>
</StyledWrapper>
</>
);
};

function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, collection, dropdownTippyRef, dispatch }) {
function SaveModal ({ tabsUidsToClose, collection, onCloseModal }) {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const tabUids = tabs.map(tab => tab.uid);

const handleCloseTabs = () => {
return dispatch(closeTabs({ tabUids: tabsUidsToClose }));
}

const handleCloseWithoutSave = async discardedItems => {
await Promise.all(discardedItems.map(item => {
dispatch(
deleteRequestDraft({
itemUid: item.uid,
collectionUid: collection.uid
})
);
}));
handleCloseTabs();
onCloseModal();
}

const handleSaveAndClose = async () => {
handleCloseTabs();
onCloseModal();
}

if (!tabsUidsToClose.some(tabUid => tabUids.includes(tabUid))) return null;

const itemsPendingSave = tabsUidsToClose.reduce((acc, tabUid) => {
const item = findItemInCollection(collection, tabUid);
if (item && item.draft) acc.push({ ...item, collectionUid: collection.uid });
return acc;
}, []);

if (!itemsPendingSave.length) return null;

return (
<SaveRequestsModal items={itemsPendingSave}
onCancel={onCloseModal}
onCloseWithoutSave={handleCloseWithoutSave}
onSaveAndClose={handleSaveAndClose} />
);
}

function RequestTabMenu({ onDropdownCreate, onCloseTabs, collectionRequestTabs, tabIndex, collection, dropdownTippyRef, dispatch }) {
const [showCloneRequestModal, setShowCloneRequestModal] = useState(false);
const [showAddNewRequestModal, setShowAddNewRequestModal] = useState(false);

Expand All @@ -203,58 +226,57 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
const hasLeftTabs = tabIndex !== 0;
const hasRightTabs = totalTabs > tabIndex + 1;
const hasOtherTabs = totalTabs > 1;
const hasSavedTabs = collectionRequestTabs.map((tab) => findItemInCollection(collection, tab.uid)).some((item) => item && !item.draft);

async function handleCloseTab(event, tabUid) {
function handleCurrentTabClose(event, tabUid) {
event.stopPropagation();
dropdownTippyRef.current.hide();

if (!tabUid) {
return;
}

try {
const item = findItemInCollection(collection, tabUid);
// silently save unsaved changes before closing the tab
if (item.draft) {
await dispatch(saveRequest(item.uid, collection.uid, true));
}

dispatch(closeTabs({ tabUids: [tabUid] }));
} catch (err) {}
return onCloseTabs([tabUid]);
}

function handleCloseOtherTabs(event) {
event.stopPropagation();
dropdownTippyRef.current.hide();

const otherTabs = collectionRequestTabs.filter((_, index) => index !== tabIndex);
otherTabs.forEach((tab) => handleCloseTab(event, tab.uid));
onCloseTabs(otherTabs.map((tab) => tab.uid));
}

function handleCloseTabsToTheLeft(event) {
event.stopPropagation();
dropdownTippyRef.current.hide();

const leftTabs = collectionRequestTabs.filter((_, index) => index < tabIndex);
leftTabs.forEach((tab) => handleCloseTab(event, tab.uid));
onCloseTabs(leftTabs.map((tab) => tab.uid));
}

function handleCloseTabsToTheRight(event) {
event.stopPropagation();
dropdownTippyRef.current.hide();

const rightTabs = collectionRequestTabs.filter((_, index) => index > tabIndex);
rightTabs.forEach((tab) => handleCloseTab(event, tab.uid));
onCloseTabs(rightTabs.map((tab) => tab.uid));
}

function handleCloseSavedTabs(event) {
event.stopPropagation();
dropdownTippyRef.current.hide();

const items = flattenItems(collection?.items);
const savedTabs = items?.filter?.((item) => !item.draft);
const savedTabIds = savedTabs?.map((item) => item.uid) || [];
dispatch(closeTabs({ tabUids: savedTabIds }));
onCloseTabs(savedTabIds);
}

function handleCloseAllTabs(event) {
collectionRequestTabs.forEach((tab) => handleCloseTab(event, tab.uid));
event.stopPropagation();
dropdownTippyRef.current.hide();
onCloseTabs(collectionRequestTabs.map((tab) => tab.uid));
}

return (
Expand Down Expand Up @@ -290,7 +312,7 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
>
Clone Request
</button>
<button className="dropdown-item w-full" onClick={(e) => handleCloseTab(e, currentTabUid)}>
<button className="dropdown-item w-full" onClick={(e) => handleCurrentTabClose(e, currentTabUid)}>
Close
</button>
<button disabled={!hasOtherTabs} className="dropdown-item w-full" onClick={handleCloseOtherTabs}>
Expand All @@ -302,7 +324,7 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
<button disabled={!hasRightTabs} className="dropdown-item w-full" onClick={handleCloseTabsToTheRight}>
Close to the Right
</button>
<button className="dropdown-item w-full" onClick={handleCloseSavedTabs}>
<button disabled={!hasSavedTabs} className="dropdown-item w-full" onClick={handleCloseSavedTabs}>
Close Saved
</button>
<button className="dropdown-item w-full" onClick={handleCloseAllTabs}>
Expand Down
Loading