refactor(migration): extract migration modal (#8359)

This commit is contained in:
naman-bruno
2026-06-25 16:53:31 +05:30
committed by GitHub
parent 437e0c7dac
commit d8d2fec166
6 changed files with 154 additions and 128 deletions

View File

@@ -0,0 +1,40 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.backup-section {
border: 1px solid ${(props) => props.theme.border.border2};
border-radius: ${(props) => props.theme.border.radius.base};
background-color: ${(props) => props.theme.background.mantle};
padding: 12px 14px;
}
.backup-section-head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
color: ${(props) => props.theme.text};
}
.backup-section-title {
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 500;
color: ${(props) => props.theme.colors.text.muted};
text-transform: uppercase;
letter-spacing: 0.04em;
}
.backup-section-help {
font-size: ${(props) => props.theme.font.size.base};
color: ${(props) => props.theme.colors.text.muted};
line-height: 1.45;
margin: 0 0 10px 0;
}
.backup-section-action {
display: flex;
justify-content: flex-start;
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,92 @@
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import toast from 'react-hot-toast';
import { migrateCollectionToYml } from 'providers/ReduxStore/slices/collections/actions';
import Modal from 'components/Modal';
import Portal from 'components/Portal';
import Button from 'ui/Button';
import StyledWrapper from './StyledWrapper';
const MigrateToYmlModal = ({ collection, onClose }) => {
const dispatch = useDispatch();
const [isMigrating, setIsMigrating] = useState(false);
const [isExporting, setIsExporting] = useState(false);
const handleMigrate = () => {
setIsMigrating(true);
dispatch(migrateCollectionToYml(collection.uid))
.catch(() => {})
.finally(() => {
setIsMigrating(false);
onClose();
});
};
const handleExportBackup = async () => {
if (isExporting) return;
setIsExporting(true);
try {
const { ipcRenderer } = window;
const result = await ipcRenderer.invoke('renderer:export-collection-zip', collection.pathname, collection.name);
if (result?.success) {
toast.success('Collection backup exported');
}
} catch (error) {
toast.error('Failed to export backup: ' + error.message);
} finally {
setIsExporting(false);
}
};
return (
<Portal>
<StyledWrapper>
<Modal
size="md"
title="Migrate to YML format"
confirmText="Migrate"
confirmDisabled={isExporting || isMigrating}
handleConfirm={handleMigrate}
handleCancel={onClose}
>
<div>
<p>
This will convert all files in <strong>{collection.name}</strong> from <code>.bru</code> format to <code>.yml</code> format.
</p>
<div className="mt-4 text-sm text-muted">
<p className="font-medium mb-2">What will happen:</p>
<ul className="list-disc ml-5 flex flex-col gap-1">
<li>All <code>.bru</code> request files will be converted to <code>.yml</code></li>
<li>Environment files will be converted to YML format</li>
<li><code>bruno.json</code> will be replaced with <code>opencollection.yml</code></li>
<li>The collection will be reloaded after migration</li>
</ul>
</div>
<div className="backup-section mt-4">
<div className="backup-section-head">
<span className="backup-section-title">Backup</span>
</div>
<p className="backup-section-help">
Export this collection as a ZIP archive before migrating, in case you want to restore it later.
</p>
<div className="backup-section-action">
<Button
data-testid="export-collection-backup-button"
size="sm"
color="secondary"
variant="outline"
onClick={handleExportBackup}
disabled={isExporting}
>
{isExporting ? 'Exporting…' : 'Export Collection'}
</Button>
</div>
</div>
</div>
</Modal>
</StyledWrapper>
</Portal>
);
};
export default MigrateToYmlModal;

View File

@@ -15,41 +15,6 @@ const StyledWrapper = styled.div`
color: ${(props) => props.theme.colors.text.yellow};
}
}
.backup-section {
border: 1px solid ${(props) => props.theme.border.border2};
border-radius: ${(props) => props.theme.border.radius.base};
background-color: ${(props) => props.theme.background.mantle};
padding: 12px 14px;
}
.backup-section-head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
color: ${(props) => props.theme.text};
}
.backup-section-title {
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 500;
color: ${(props) => props.theme.colors.text.muted};
text-transform: uppercase;
letter-spacing: 0.04em;
}
.backup-section-help {
font-size: ${(props) => props.theme.font.size.base};
color: ${(props) => props.theme.colors.text.muted};
line-height: 1.45;
margin: 0 0 10px 0;
}
.backup-section-action {
display: flex;
justify-content: flex-start;
}
`;
export default StyledWrapper;

View File

@@ -1,47 +1,17 @@
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { IconFileCode, IconTransform } from '@tabler/icons';
import toast from 'react-hot-toast';
import { migrateCollectionToYml } from 'providers/ReduxStore/slices/collections/actions';
import Modal from 'components/Modal';
import Button from 'ui/Button';
import MigrateToYmlModal from './MigrateToYmlModal';
import StyledWrapper from './StyledWrapper';
const Migration = ({ collection }) => {
const dispatch = useDispatch();
const [showConfirmModal, setShowConfirmModal] = useState(false);
const [isMigrating, setIsMigrating] = useState(false);
const [isExporting, setIsExporting] = useState(false);
// Only show for bru format collections
if (collection.format !== 'bru') {
return null;
}
const handleMigrate = () => {
setIsMigrating(true);
setShowConfirmModal(false);
dispatch(migrateCollectionToYml(collection.uid))
.catch(() => { })
.finally(() => setIsMigrating(false));
};
const handleExportBackup = async () => {
if (isExporting) return;
setIsExporting(true);
try {
const { ipcRenderer } = window;
const result = await ipcRenderer.invoke('renderer:export-collection-zip', collection.pathname, collection.name);
if (result?.success) {
toast.success('Collection backup exported');
}
} catch (error) {
toast.error('Failed to export backup: ' + error.message);
} finally {
setIsExporting(false);
}
};
return (
<StyledWrapper>
<div className="migration-section">
@@ -74,8 +44,6 @@ const Migration = ({ collection }) => {
color="primary"
className="mt-2"
onClick={() => setShowConfirmModal(true)}
disabled={isMigrating}
loading={isMigrating}
>
Convert to YML
</Button>
@@ -84,49 +52,10 @@ const Migration = ({ collection }) => {
</div>
{showConfirmModal && (
<Modal
size="md"
title="Migrate to YML format"
confirmText="Migrate"
confirmDisabled={isExporting}
handleConfirm={handleMigrate}
handleCancel={() => setShowConfirmModal(false)}
>
<div>
<p>
This will convert all files in <strong>{collection.name}</strong> from <code>.bru</code> format to <code>.yml</code> format.
</p>
<div className="mt-4 text-sm text-muted">
<p className="font-medium mb-2">What will happen:</p>
<ul className="list-disc ml-5 flex flex-col gap-1">
<li>All <code>.bru</code> request files will be converted to <code>.yml</code></li>
<li>Environment files will be converted to YML format</li>
<li><code>bruno.json</code> will be replaced with <code>opencollection.yml</code></li>
<li>The collection will be reloaded after migration</li>
</ul>
</div>
<div className="backup-section mt-4">
<div className="backup-section-head">
<span className="backup-section-title">Backup</span>
</div>
<p className="backup-section-help">
Export this collection as a ZIP archive before migrating, in case you want to restore it later.
</p>
<div className="backup-section-action">
<Button
data-testid="export-collection-backup-button"
size="sm"
color="secondary"
variant="outline"
onClick={handleExportBackup}
disabled={isExporting}
>
{isExporting ? 'Exporting…' : 'Export Collection'}
</Button>
</div>
</div>
</div>
</Modal>
<MigrateToYmlModal
collection={collection}
onClose={() => setShowConfirmModal(false)}
/>
)}
</StyledWrapper>
);

View File

@@ -23,7 +23,8 @@ import OpenAPISyncIcon from 'components/Icons/OpenAPISync';
import { switchWorkspace, renameWorkspaceAction, exportWorkspaceAction, confirmWorkspaceCreation, cancelWorkspaceCreation } from 'providers/ReduxStore/slices/workspaces/actions';
import { updateWorkspace } from 'providers/ReduxStore/slices/workspaces';
import { showInFolder } from 'providers/ReduxStore/slices/collections/actions';
import { toggleCollectionFileMode, toggleAppMode, updateSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
import { toggleCollectionFileMode, toggleAppMode } from 'providers/ReduxStore/slices/collections';
import MigrateToYmlModal from 'components/CollectionSettings/Overview/Migration/MigrateToYmlModal';
import { findItemInCollection, findItemInCollectionByPathname } from 'utils/collections';
import find from 'lodash/find';
import get from 'lodash/get';
@@ -92,6 +93,7 @@ const CollectionHeader = ({ collection, isScratchCollection }) => {
const [workspaceNameError, setWorkspaceNameError] = useState('');
const [closeWorkspaceModalOpen, setCloseWorkspaceModalOpen] = useState(false);
const [createWorkspaceModalOpen, setCreateWorkspaceModalOpen] = useState(false);
const [showMigrateModal, setShowMigrateModal] = useState(false);
// Migrate-to-YML pill dismissal state (persisted by collection pathname)
const [migratePillDismissed, setMigratePillDismissed] = useState(true);
@@ -270,17 +272,6 @@ const CollectionHeader = ({ collection, isScratchCollection }) => {
);
};
const viewMigrationSettings = () => {
dispatch(
addTab({
uid: collection.uid,
collectionUid: collection.uid,
type: 'collection-settings'
})
);
dispatch(updateSettingsSelectedTab({ collectionUid: collection.uid, tab: 'overview' }));
};
const viewOpenApiSync = () => {
dispatch(addTab({
uid: uuid(),
@@ -708,7 +699,7 @@ const CollectionHeader = ({ collection, isScratchCollection }) => {
<button
type="button"
className="pill-main"
onClick={viewMigrationSettings}
onClick={() => setShowMigrateModal(true)}
>
<IconTransform size={13} strokeWidth={1.5} />
<span className="pill-label">Migrate to YML</span>
@@ -760,6 +751,12 @@ const CollectionHeader = ({ collection, isScratchCollection }) => {
</div>
)}
</div>
{showMigrateModal && (
<MigrateToYmlModal
collection={collection}
onClose={() => setShowMigrateModal(false)}
/>
)}
</StyledWrapper>
);
};

View File

@@ -36,12 +36,15 @@ test.describe('Migrate-to-YML pill in collection toolbar', () => {
await expect(pillDismiss).toBeVisible();
});
await test.step('Clicking the pill body opens the collection settings overview without dismissing', async () => {
await test.step('Clicking the pill body opens the migrate-to-yml modal without dismissing', async () => {
await pill.click();
await expect(page.getByTestId('collection-settings-tab-overview')).toBeVisible();
await expect(page.getByText('Migrate to YML file format')).toBeVisible();
// Pill should still be visible — clicking the body navigates, it should not dismiss
const migrateModal = page.locator('.bruno-modal-card', { hasText: 'Migrate to YML format' });
await expect(migrateModal).toBeVisible();
// Pill should still be visible — clicking the body opens the modal, it should not dismiss
await expect(pill).toBeVisible();
// Close the modal so the next step can interact with the pill
await page.keyboard.press('Escape');
await expect(page.locator('.bruno-modal-backdrop')).toHaveCount(0);
});
await test.step('Dismiss the pill via the cross icon', async () => {