fix: handle transient requests during app quit flow in SaveRequestsModal (#8003)

* fix: handle transient requests during app quit flow in SaveRequestsModal

* test: non serial

* chore: fix theme

* fix: ui polish

* chore: import

* chore: cr
This commit is contained in:
Sid
2026-05-15 16:27:56 +05:30
committed by GitHub
parent bdc5d1e017
commit 48c88df3a8
5 changed files with 114 additions and 13 deletions

View File

@@ -0,0 +1,21 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
background: ${({ theme }) => theme.background.crust};
border: 1px solid ${({ theme }) => theme.border.border0};
border-radius: ${({ theme }) => theme.border.radius.sm};
.request-name {
color: ${({ theme }) => theme.text};
}
.collection-name{
color: ${({ theme }) => theme.colors.text.subtext1};
}
`;
export default StyledWrapper;

View File

@@ -7,7 +7,8 @@ import { closeTabs } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
import Modal from 'components/Modal';
import Button from 'ui/Button';
import SaveTransientRequest from './index';
import SaveTransientRequest from 'components/SaveTransientRequest';
import StyledWrapper from './StyledWrapper';
const SaveTransientRequestContainer = () => {
const dispatch = useDispatch();
@@ -86,13 +87,13 @@ const SaveTransientRequestContainer = () => {
{modals.map((modal) => {
const { item, collection } = modal;
return (
<div
<StyledWrapper
key={item.uid}
className="flex items-center justify-between py-2 px-3 bg-gray-50 rounded border border-gray-200"
className="flex items-center justify-between"
>
<div className="flex flex-col flex-1 min-w-0 mr-3">
<span className="text-sm text-gray-700 truncate">{item.name}</span>
<span className="text-xs text-gray-500 truncate">
<span className="text-sm request-name truncate">{item.name}</span>
<span className="text-xs collection-name truncate">
{collection.name}
</span>
</div>
@@ -105,13 +106,13 @@ const SaveTransientRequestContainer = () => {
>
Save
</Button>
</div>
</StyledWrapper>
);
})}
</div>
</div>
<div className="flex justify-end mt-6 pt-4 border-t">
<div className="flex justify-end mt-6 pt-4">
<Button color="danger" onClick={handleDiscardAll}>
Discard All
</Button>

View File

@@ -358,6 +358,8 @@ const SaveTransientRequest = ({ item: itemProp, collection: collectionProp, isOp
return null;
}
const showNewFolderFooterButton = !showNewFolderInput && !isSelectingCollection && (filteredFolders.length > 0 && !searchText.trim());
return (
<StyledWrapper>
<Modal
@@ -539,7 +541,7 @@ const SaveTransientRequest = ({ item: itemProp, collection: collectionProp, isOp
size="sm"
onClick={handleCreateNewCollection}
>
Save
Create
</Button>
</div>
</li>
@@ -736,7 +738,20 @@ const SaveTransientRequest = ({ item: itemProp, collection: collectionProp, isOp
</ul>
) : (
<div className="folder-empty-state">
{searchText.trim() ? 'No folders found' : 'No folders available'}
<div className="flex flex-col items-center">
<span>
{searchText.trim() ? 'No folders found' : 'No folders available' }
</span>
<Button
type="button"
color="primary"
variant="ghost"
icon={<IconFolder size={16} strokeWidth={1.5} />}
onClick={handleShowNewFolder}
>
New Folder
</Button>
</div>
</div>
)}
</div>
@@ -747,7 +762,7 @@ const SaveTransientRequest = ({ item: itemProp, collection: collectionProp, isOp
<div className="custom-modal-footer">
<div className="footer-left">
{!showNewFolderInput && !isSelectingCollection && (
{showNewFolderFooterButton && (
<Button
type="button"
color="primary"

View File

@@ -8,7 +8,7 @@ import { findCollectionByUid, flattenItems, isItemARequest, hasRequestChanges, f
import { pluralizeWord } from 'utils/common';
import { getInvalidVariableNames } from 'utils/common/variables';
import { completeQuitFlow } from 'providers/ReduxStore/slices/app';
import { saveMultipleRequests, saveMultipleCollections, saveMultipleFolders, saveEnvironment, closeTabs } from 'providers/ReduxStore/slices/collections/actions';
import { saveRequest, saveMultipleRequests, saveMultipleCollections, saveMultipleFolders, saveEnvironment, closeTabs } from 'providers/ReduxStore/slices/collections/actions';
import { saveGlobalEnvironment, clearGlobalEnvironmentDraft } from 'providers/ReduxStore/slices/global-environments';
import { deleteRequestDraft, deleteCollectionDraft, deleteFolderDraft, clearEnvironmentsDraft } from 'providers/ReduxStore/slices/collections';
import { IconAlertTriangle } from '@tabler/icons';
@@ -150,6 +150,8 @@ const SaveRequestsModal = ({ onClose, forceCloseTabs = false, tabUidsToClose = [
const collectionDrafts = allDrafts.filter((d) => d.type === 'collection');
const folderDrafts = allDrafts.filter((d) => d.type === 'folder');
const requestDrafts = allDrafts.filter((d) => isItemARequest(d));
const transientRequestDrafts = requestDrafts.filter((d) => d.isTransient);
const nonTransientRequestDrafts = requestDrafts.filter((d) => !d.isTransient);
const collectionEnvironmentDrafts = allDrafts.filter((d) => d.type === 'collection-environment');
const globalEnvironmentDrafts = allDrafts.filter((d) => d.type === 'global-environment');
@@ -164,8 +166,18 @@ const SaveRequestsModal = ({ onClose, forceCloseTabs = false, tabUidsToClose = [
}
// Save all request drafts
if (requestDrafts.length > 0) {
await dispatch(saveMultipleRequests(requestDrafts));
if (nonTransientRequestDrafts.length > 0) {
await dispatch(saveMultipleRequests(nonTransientRequestDrafts));
}
if (transientRequestDrafts.length > 0) {
await Promise.all(
transientRequestDrafts.map((draft) =>
dispatch(saveRequest(draft.uid, draft.collectionUid, true)).catch(() => null)
)
);
onClose();
return;
}
// Save environment drafts, skipping any with invalid variable names

View File

@@ -0,0 +1,52 @@
import { test, expect } from '../../playwright';
import { createCollection, createTransientRequest, fillRequestUrl, closeAllCollections } from '../utils/page';
import { buildCommonLocators } from '../utils/page/locators';
test.describe('Transient Requests - Quit Flow', () => {
test('should open transient save modal when saving during app quit flow', async ({ page, electronApp, createTmpDir }) => {
const locators = buildCommonLocators(page);
const collectionPath = await createTmpDir('transient-quit-flow');
await test.step('Create collection and transient request', async () => {
await createCollection(page, 'transient-quit-flow-test', collectionPath);
await createTransientRequest(page, { requestType: 'HTTP' });
await fillRequestUrl(page, 'http://localhost:8081/ping');
});
await test.step('Trigger app quit flow from main process', async () => {
await electronApp.evaluate(({ BrowserWindow }) => {
for (const win of BrowserWindow.getAllWindows()) {
if (!win.isDestroyed()) {
win.close();
}
}
});
const unsavedChangesModal = page.locator('.bruno-modal-card').filter({ hasText: 'Unsaved changes' });
await expect(unsavedChangesModal).toBeVisible({ timeout: 10000 });
await unsavedChangesModal.getByRole('button', { name: 'Save', exact: true }).click();
});
await test.step('Save transient request using existing Save Request flow', async () => {
const saveTransientModal = page.locator('.bruno-modal-card').filter({ hasText: 'Save Request' });
await expect(saveTransientModal).toBeVisible({ timeout: 10000 });
const requestNameInput = saveTransientModal.locator('#request-name');
await requestNameInput.clear();
await requestNameInput.fill('Saved via quit flow');
await saveTransientModal.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Request saved successfully').last()).toBeVisible({ timeout: 10000 });
});
await test.step('Verify app remains open and request is saved', async () => {
await expect(locators.sidebar.collection('transient-quit-flow-test')).toBeVisible();
await locators.sidebar.collection('transient-quit-flow-test').click();
await expect(locators.sidebar.request('Saved via quit flow')).toBeVisible({ timeout: 10000 });
});
await test.step('Cleanup collections', async () => {
await closeAllCollections(page);
});
});
});