mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-25 21:55:49 +00:00
refactor(migration): extract migration modal (#8359)
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user