mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
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:
@@ -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;
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
52
tests/transient-requests/transient-request-quit-flow.spec.ts
Normal file
52
tests/transient-requests/transient-request-quit-flow.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user