mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
fix(openapi-sync): simplify IPC calls, fix state priorities, and improve stored spec missing UX (#7489)
* refactor(OpenAPISyncTab): remove unused props and streamline IPC calls - Eliminated unnecessary sourceUrl prop from various components and hooks in the OpenAPISyncTab. - Improved pretty-printing logic in OpenAPISpecTab to handle non-JSON content gracefully. - Updated IPC calls to remove redundant parameters, enhancing code clarity and maintainability. * feat(OpenAPISyncTab): enhance user interaction and visual feedback - Added onTabSelect prop to OpenAPISyncTab for improved tab navigation. - Updated color properties in StyledWrapper for better consistency with theme. - Replaced IconClock with IconAlertTriangle in CollectionStatusSection for clearer status indication. - Enhanced messaging in OverviewSection and SpecStatusSection to provide clearer user guidance. - Introduced handleRestoreSpec function in useSyncFlow for better spec restoration handling. * fix(OpenAPISyncTab): update button labels for clarity in OverviewSection - Changed button label from 'restore' to 'spec-details' for better context. - Updated the button text from 'View Details' to 'Go to Spec Updates' to enhance user understanding of navigation options. * refactor(OpenAPISyncTab): remove unused props and streamline component logic - Eliminated unnecessary props from OpenAPISyncTab, CollectionStatusSection, and SpecStatusSection for cleaner code. - Removed commented-out code in OverviewSection and SpecStatusSection to enhance readability. - Introduced posixifyPath utility function in filesystem.js to standardize path formatting. * fix(OpenAPISyncTab): update openapi config handling to support array format - Modified the logic in loadBrunoConfig to handle openapi as an array, ensuring consistent resolution of source URLs for all entries. This change improves the configuration handling for OpenAPI specifications. * fix(OpenAPISyncTab): improve openapi config handling and merge logic - Updated loadBrunoConfig to ensure openapi is treated as an array, enhancing source URL resolution. - Modified mergeWithUserValues to handle cases where specItems may be undefined, improving robustness in merging user values with specifications.
This commit is contained in:
@@ -9,6 +9,7 @@ import StyledWrapper from 'components/ApiSpecPanel/StyledWrapper';
|
||||
*/
|
||||
const prettyPrintSpec = (content) => {
|
||||
if (!content) return content;
|
||||
if (content.trimStart()[0] !== '{') return content;
|
||||
try {
|
||||
return fastJsonFormat(content);
|
||||
} catch {
|
||||
@@ -32,8 +33,7 @@ const OpenAPISpecTab = ({ collection }) => {
|
||||
try {
|
||||
const { ipcRenderer } = window;
|
||||
const result = await ipcRenderer.invoke('renderer:read-openapi-spec', {
|
||||
collectionPath: collection.pathname,
|
||||
sourceUrl
|
||||
collectionPath: collection.pathname
|
||||
});
|
||||
if (result.error) {
|
||||
// Local file not found — fall back to fetching from remote URL
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
IconTrash,
|
||||
IconArrowBackUp,
|
||||
IconExternalLink,
|
||||
IconClock,
|
||||
IconAlertTriangle,
|
||||
IconInfoCircle,
|
||||
IconLoader2
|
||||
} from '@tabler/icons';
|
||||
@@ -25,7 +25,8 @@ const CollectionStatusSection = ({
|
||||
storedSpec,
|
||||
lastSyncDate,
|
||||
onOpenEndpoint,
|
||||
isLoading
|
||||
isLoading,
|
||||
onTabSelect
|
||||
}) => {
|
||||
const {
|
||||
pendingAction, setPendingAction,
|
||||
@@ -71,8 +72,6 @@ const CollectionStatusSection = ({
|
||||
variant: 'muted',
|
||||
message: 'Collection has changes since last sync',
|
||||
badges: { modifiedCount, missingCount, localOnlyCount },
|
||||
version,
|
||||
lastSyncDate,
|
||||
actions: ['revert-all']
|
||||
};
|
||||
}
|
||||
@@ -89,12 +88,6 @@ const CollectionStatusSection = ({
|
||||
: <div className={`status-dot ${bannerState.variant}`} />}
|
||||
<span className="banner-title">
|
||||
{bannerState.message}
|
||||
{bannerState.version && (
|
||||
<> · <code style={{ fontStyle: 'normal' }} className="checked-text">v{bannerState.version}</code></>
|
||||
)}
|
||||
{bannerState.lastSyncDate && (
|
||||
<span className="checked-text"> · Synced {moment(bannerState.lastSyncDate).fromNow()}</span>
|
||||
)}
|
||||
</span>
|
||||
{bannerState.badges && (
|
||||
<span className="banner-details">
|
||||
@@ -117,7 +110,7 @@ const CollectionStatusSection = ({
|
||||
{hasDrift && (
|
||||
<div className="sync-info-notice mt-4">
|
||||
<IconInfoCircle size={14} className="sync-info-icon" />
|
||||
<span><span className="whats-updated-title">What's tracked:</span> Changes to URL, parameters, headers, body and auth compared to the synced spec. Your variables, scripts, tests, assertions, settings etc. are not tracked here.</span>
|
||||
<span><span className="whats-updated-title">What's tracked:</span> Changes to parameters, headers, body and auth compared to the synced spec. Your variables, scripts, tests, assertions, settings etc. are not tracked here.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -222,26 +215,15 @@ const CollectionStatusSection = ({
|
||||
<p>Comparing your collection with the last synced spec...</p>
|
||||
</div>
|
||||
) : !hasStoredSpec ? (
|
||||
<>
|
||||
<div className="spec-update-banner warning">
|
||||
<div className="banner-left">
|
||||
<div className="status-dot warning" />
|
||||
<span className="banner-title">
|
||||
{lastSyncDate
|
||||
? 'Last synced spec is required to show collection changes. Restore the latest spec from the source to track future changes.'
|
||||
: 'Collection changes will be available after the initial sync'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sync-review-empty-state mt-5">
|
||||
<IconClock size={40} className="empty-state-icon" />
|
||||
<h4>{lastSyncDate ? 'Last Synced Spec missing from storage' : 'Waiting for initial sync'}</h4>
|
||||
<p>{lastSyncDate
|
||||
? 'Restore the latest spec from the source to track future changes.'
|
||||
: 'Once you sync your collection with the spec, changes will appear here.'}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
<div className="sync-review-empty-state mt-5">
|
||||
<IconAlertTriangle size={40} className="empty-state-icon" />
|
||||
<h4>{lastSyncDate ? 'Cannot track collection changes' : 'Waiting for initial sync'}</h4>
|
||||
<p>{lastSyncDate
|
||||
? 'The last synced spec is missing. Go to the \'Spec Updates\' tab to restore it, or sync the collection if updates are available to track future changes.'
|
||||
: 'Once you sync your collection with the spec, local changes will appear here.'}
|
||||
</p>
|
||||
<Button variant="outline" size="sm" className="mt-4" onClick={() => onTabSelect('spec-updates')}>Go to Spec Updates</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="sync-review-empty-state mt-5">
|
||||
<IconCheck size={40} className="empty-state-icon" />
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} from '@tabler/icons';
|
||||
import toast from 'react-hot-toast';
|
||||
import Button from 'ui/Button';
|
||||
import StatusBadge from 'ui/StatusBadge';
|
||||
import ActionIcon from 'ui/ActionIcon/index';
|
||||
import MenuDropdown from 'ui/MenuDropdown';
|
||||
import Help from 'components/Help';
|
||||
|
||||
@@ -21,7 +21,7 @@ const SUMMARY_CARDS = [
|
||||
key: 'inSync',
|
||||
label: 'In Sync with Spec',
|
||||
color: 'green',
|
||||
tooltip: 'Endpoints that currently match the latest spec'
|
||||
tooltip: 'Endpoints that currently match the latest spec from the source'
|
||||
},
|
||||
{
|
||||
key: 'changed',
|
||||
@@ -46,10 +46,8 @@ const OverviewSection = ({ collection, storedSpec, collectionDrift, specDrift, r
|
||||
const specMeta = useSelector(selectStoredSpecMeta(collection.uid));
|
||||
const activeError = error || reduxError;
|
||||
|
||||
const version = storedSpec?.info?.version ?? specMeta?.version;
|
||||
const newVersion = specDrift?.newVersion;
|
||||
const hasVersionChange = version && newVersion && version !== newVersion;
|
||||
const endpointCount = countEndpoints(storedSpec) ?? specMeta?.endpointCount ?? null;
|
||||
const version = specMeta?.version;
|
||||
const endpointCount = specMeta?.endpointCount ?? null;
|
||||
const lastSyncDate = openApiSyncConfig?.lastSyncDate;
|
||||
const groupBy = openApiSyncConfig?.groupBy || 'tags';
|
||||
const autoCheckEnabled = openApiSyncConfig?.autoCheck !== false;
|
||||
@@ -90,7 +88,7 @@ const OverviewSection = ({ collection, storedSpec, collectionDrift, specDrift, r
|
||||
};
|
||||
|
||||
const details = [
|
||||
{ label: 'Spec Version', value: hasVersionChange ? `v${version} → v${newVersion}` : version ? `v${version}` : '–' },
|
||||
{ label: 'Spec Version', value: version ? `v${version}` : '–' },
|
||||
{ label: 'Endpoints in Spec', value: endpointCount != null ? endpointCount : '–' },
|
||||
{ label: 'Last Synced At', value: lastSyncDate ? moment(lastSyncDate).fromNow() : '–', tooltip: lastSyncDate ? moment(lastSyncDate).format('MMMM D, YYYY [at] h:mm A') : undefined },
|
||||
{ label: 'Folder Grouping', value: capitalize(groupBy) },
|
||||
@@ -121,19 +119,10 @@ const OverviewSection = ({ collection, storedSpec, collectionDrift, specDrift, r
|
||||
buttons: ['review']
|
||||
};
|
||||
}
|
||||
if (specDrift?.storedSpecMissing && lastSyncDate) {
|
||||
return {
|
||||
variant: 'warning',
|
||||
title: 'Last synced spec not found',
|
||||
subtitle: 'The last synced spec is missing in the storage. Restore the latest spec from the source to track future changes.',
|
||||
buttons: ['restore']
|
||||
};
|
||||
}
|
||||
if (!hasDriftData) return null;
|
||||
if (hasSpecUpdates && hasCollectionChanges) {
|
||||
return {
|
||||
variant: 'warning',
|
||||
title: `The API spec has new updates${versionInfo} and the collection has changes`,
|
||||
title: `OpenAPI spec has new updates${versionInfo} and the collection has changes`,
|
||||
subtitle: 'New or changed requests are available. Some collection changes may be overwritten.',
|
||||
buttons: ['sync', 'changes']
|
||||
};
|
||||
@@ -141,11 +130,20 @@ const OverviewSection = ({ collection, storedSpec, collectionDrift, specDrift, r
|
||||
if (hasSpecUpdates) {
|
||||
return {
|
||||
variant: 'warning',
|
||||
title: `The API spec has new updates${versionInfo}`,
|
||||
title: `OpenAPI spec has new updates${versionInfo}`,
|
||||
subtitle: 'New or changed requests are available.',
|
||||
buttons: ['sync']
|
||||
};
|
||||
}
|
||||
if (specDrift?.storedSpecMissing && lastSyncDate) {
|
||||
return {
|
||||
variant: 'warning',
|
||||
title: 'Last synced spec not found',
|
||||
subtitle: 'The last synced spec is missing in the storage. Restore the latest spec from the source to track collection changes.',
|
||||
buttons: ['spec-details']
|
||||
};
|
||||
}
|
||||
if (!hasDriftData) return null;
|
||||
if (hasCollectionChanges) {
|
||||
return {
|
||||
variant: 'muted',
|
||||
@@ -154,12 +152,6 @@ const OverviewSection = ({ collection, storedSpec, collectionDrift, specDrift, r
|
||||
buttons: ['changes']
|
||||
};
|
||||
}
|
||||
// return {
|
||||
// variant: 'success',
|
||||
// title: 'Collection is in sync with the spec',
|
||||
// subtitle: null,
|
||||
// buttons: []
|
||||
// };
|
||||
return null;
|
||||
}, [activeError, hasDriftData, hasSpecUpdates, hasCollectionChanges, specDrift?.storedSpecMissing, specDrift?.storedVersion, specDrift?.newVersion, lastSyncDate]);
|
||||
|
||||
@@ -195,14 +187,9 @@ const OverviewSection = ({ collection, storedSpec, collectionDrift, specDrift, r
|
||||
Review and Sync Collection
|
||||
</Button>
|
||||
)}
|
||||
{bannerState.buttons.includes('restore') && (
|
||||
<Button size="sm" onClick={() => onTabSelect('spec-updates')}>
|
||||
Restore Spec File
|
||||
</Button>
|
||||
)}
|
||||
{bannerState.buttons.includes('spec-details') && (
|
||||
<Button variant="outline" size="sm" onClick={() => onTabSelect('spec-updates')}>
|
||||
View Details
|
||||
Go to Spec Updates
|
||||
</Button>
|
||||
)}
|
||||
{bannerState.buttons.includes('open-settings') && (
|
||||
|
||||
@@ -3,7 +3,8 @@ import { useSelector } from 'react-redux';
|
||||
import {
|
||||
IconCheck,
|
||||
IconRefresh,
|
||||
IconAlertTriangle
|
||||
IconAlertTriangle,
|
||||
IconClock
|
||||
} from '@tabler/icons';
|
||||
import Button from 'ui/Button';
|
||||
import StatusBadge from 'ui/StatusBadge';
|
||||
@@ -23,14 +24,20 @@ const SpecStatusSection = ({
|
||||
|
||||
const {
|
||||
isSyncing, showConfirmModal, confirmGroups,
|
||||
handleSyncNow, handleApplySync, cancelConfirmModal, handleConfirmModalSync
|
||||
handleRestoreSpec, handleApplySync, cancelConfirmModal, handleConfirmModalSync
|
||||
} = useSyncFlow({
|
||||
collection, specDrift, remoteDrift, collectionDrift,
|
||||
sourceUrl, setError, checkForUpdates: onCheck
|
||||
setError, checkForUpdates: onCheck
|
||||
});
|
||||
|
||||
const lastSyncedAt = openApiSyncConfig?.lastSyncDate;
|
||||
|
||||
const hasRemoteUpdates = remoteDrift && (
|
||||
(remoteDrift.missing?.length || 0)
|
||||
+ (remoteDrift.modified?.length || 0)
|
||||
+ (remoteDrift.localOnly?.length || 0)
|
||||
) > 0;
|
||||
|
||||
const bannerState = useMemo(() => {
|
||||
if (fileNotFound) {
|
||||
return { variant: 'danger', message: `Source file not found at ${sourceUrl}`, actions: ['open-settings'] };
|
||||
@@ -41,13 +48,12 @@ const SpecStatusSection = ({
|
||||
if (!specDrift) {
|
||||
return null;
|
||||
}
|
||||
if (specDrift.storedSpecMissing) {
|
||||
if (!lastSyncedAt) {
|
||||
return { variant: 'warning', message: 'Initial sync required — your collection differs from the spec', actions: [] };
|
||||
}
|
||||
return { variant: 'warning', message: 'Last synced spec not found — Restore the latest spec from the source to track future changes.', actions: [] };
|
||||
if (specDrift.storedSpecMissing && !hasRemoteUpdates) {
|
||||
return null;
|
||||
}
|
||||
const hasEndpointUpdates = (specDrift.added?.length || 0) + (specDrift.modified?.length || 0) + (specDrift.removed?.length || 0) > 0;
|
||||
const hasEndpointUpdates = specDrift.storedSpecMissing
|
||||
? hasRemoteUpdates
|
||||
: (specDrift.added?.length || 0) + (specDrift.modified?.length || 0) + (specDrift.removed?.length || 0) > 0;
|
||||
if (hasEndpointUpdates) {
|
||||
const versionInfo = (specDrift.storedVersion && specDrift.newVersion && specDrift.storedVersion !== specDrift.newVersion)
|
||||
? ` (v${specDrift.storedVersion} → v${specDrift.newVersion})`
|
||||
@@ -57,13 +63,8 @@ const SpecStatusSection = ({
|
||||
changes: { added: specDrift.added?.length || 0, modified: specDrift.modified?.length || 0, removed: specDrift.removed?.length || 0 }
|
||||
};
|
||||
}
|
||||
// return {
|
||||
// variant: 'success', message: 'Spec is up to date', actions: [],
|
||||
// version: specDrift.newVersion || storedSpec?.info?.version || specDrift.storedVersion,
|
||||
// lastChecked: lastCheckedAt ? moment(lastCheckedAt).fromNow() : 'just now'
|
||||
// };
|
||||
return null;
|
||||
}, [fileNotFound, error, sourceUrl, specDrift, lastSyncedAt, storedSpec, lastCheckedAt]);
|
||||
}, [fileNotFound, error, sourceUrl, specDrift, lastSyncedAt, storedSpec, lastCheckedAt, hasRemoteUpdates]);
|
||||
return (
|
||||
<>
|
||||
{bannerState && (
|
||||
@@ -92,9 +93,6 @@ const SpecStatusSection = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="banner-actions">
|
||||
{bannerState.actions.includes('quick-sync') && (
|
||||
<Button size="xs" onClick={handleSyncNow}>Restore Spec File</Button>
|
||||
)}
|
||||
{bannerState.actions.includes('open-settings') && (
|
||||
<Button variant="ghost" size="sm" onClick={onOpenSettings}>
|
||||
Update connection settings
|
||||
@@ -111,12 +109,12 @@ const SpecStatusSection = ({
|
||||
<h4>Unable to check for updates</h4>
|
||||
<p>Fix the connection issue above and check again.</p>
|
||||
</div>
|
||||
) : specDrift?.storedSpecMissing && openApiSyncConfig?.lastSyncDate ? (
|
||||
) : specDrift?.storedSpecMissing && openApiSyncConfig?.lastSyncDate && !hasRemoteUpdates ? (
|
||||
<div className="sync-review-empty-state mt-5">
|
||||
<IconRefresh size={40} className="empty-state-icon" />
|
||||
<h4>Last Synced Spec not found in storage</h4>
|
||||
<p>The last synced spec is missing in the storage. Restore the latest spec from the source to track future changes.</p>
|
||||
<Button className="mt-4" color="warning" onClick={handleSyncNow} loading={isSyncing}>
|
||||
<IconCheck size={40} className="empty-state-icon" />
|
||||
<h4>No updates from the spec</h4>
|
||||
<p>The spec endpoints have not been updated since the last sync. You can restore the spec file to track local collection changes.</p>
|
||||
<Button className="mt-4" color="warning" onClick={handleRestoreSpec} loading={isSyncing}>
|
||||
Restore Spec File
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -625,7 +625,7 @@ const StyledWrapper = styled.div`
|
||||
.settings-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
color: ${(props) => props.theme.text};
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
@@ -670,7 +670,7 @@ const StyledWrapper = styled.div`
|
||||
|
||||
.toggle-description {
|
||||
font-size: 11px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
color: ${(props) => props.theme.text};
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
@@ -1251,7 +1251,6 @@ const StyledWrapper = styled.div`
|
||||
.disconnect-modal {
|
||||
.disconnect-message {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
line-height: 1.5;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
@@ -1281,7 +1280,7 @@ const StyledWrapper = styled.div`
|
||||
.action-confirm-modal {
|
||||
.confirm-message {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
color: ${(props) => props.theme.text};
|
||||
line-height: 1.5;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
@@ -32,14 +32,12 @@ const useOpenAPISync = (collection) => {
|
||||
|
||||
const updateStoredSpec = (spec) => {
|
||||
setStoredSpec(spec);
|
||||
if (spec) {
|
||||
dispatch(setStoredSpecMeta({
|
||||
collectionUid: collection.uid,
|
||||
title: spec.info?.title || null,
|
||||
version: spec.info?.version || null,
|
||||
endpointCount: countEndpoints(spec)
|
||||
}));
|
||||
}
|
||||
dispatch(setStoredSpecMeta({
|
||||
collectionUid: collection.uid,
|
||||
title: spec?.info?.title || null,
|
||||
version: spec?.info?.version || null,
|
||||
endpointCount: spec ? countEndpoints(spec) : null
|
||||
}));
|
||||
};
|
||||
|
||||
// Flatten collection items including nested items in folders
|
||||
@@ -100,8 +98,7 @@ const useOpenAPISync = (collection) => {
|
||||
try {
|
||||
const { ipcRenderer } = window;
|
||||
const result = await ipcRenderer.invoke('renderer:get-collection-drift', {
|
||||
collectionPath: collection.pathname,
|
||||
brunoConfig: collection.brunoConfig
|
||||
collectionPath: collection.pathname
|
||||
});
|
||||
|
||||
if (!result.error) {
|
||||
@@ -150,9 +147,7 @@ const useOpenAPISync = (collection) => {
|
||||
}
|
||||
|
||||
setSpecDrift(result);
|
||||
if (result.storedSpec) {
|
||||
updateStoredSpec(result.storedSpec);
|
||||
}
|
||||
updateStoredSpec(result.storedSpec || null);
|
||||
|
||||
// Update Redux store so toolbar status stays in sync
|
||||
dispatch(setCollectionUpdate({
|
||||
@@ -166,7 +161,6 @@ const useOpenAPISync = (collection) => {
|
||||
if (result.newSpec) {
|
||||
const remoteComparison = await ipcRenderer.invoke('renderer:get-collection-drift', {
|
||||
collectionPath: collection.pathname,
|
||||
brunoConfig: collection.brunoConfig,
|
||||
compareSpec: result.newSpec
|
||||
});
|
||||
if (remoteComparison.error) {
|
||||
@@ -271,7 +265,6 @@ const useOpenAPISync = (collection) => {
|
||||
if (result.newSpec) {
|
||||
const drift = await ipcRenderer.invoke('renderer:get-collection-drift', {
|
||||
collectionPath: collection.pathname,
|
||||
brunoConfig: collection.brunoConfig,
|
||||
compareSpec: result.newSpec
|
||||
});
|
||||
|
||||
@@ -284,8 +277,7 @@ const useOpenAPISync = (collection) => {
|
||||
// Collection matches — save spec file silently to complete setup
|
||||
await ipcRenderer.invoke('renderer:save-openapi-spec', {
|
||||
collectionPath: collection.pathname,
|
||||
specContent: result.newSpecContent || JSON.stringify(result.newSpec, null, 2),
|
||||
sourceUrl: trimmedUrl
|
||||
specContent: result.newSpecContent || JSON.stringify(result.newSpec, null, 2)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -304,7 +296,6 @@ const useOpenAPISync = (collection) => {
|
||||
const { ipcRenderer } = window;
|
||||
await ipcRenderer.invoke('renderer:remove-openapi-sync-config', {
|
||||
collectionPath: collection.pathname,
|
||||
sourceUrl: openApiSyncConfig?.sourceUrl || sourceUrl,
|
||||
deleteSpecFile: true
|
||||
});
|
||||
setSourceUrl('');
|
||||
@@ -343,7 +334,6 @@ const useOpenAPISync = (collection) => {
|
||||
const { ipcRenderer } = window;
|
||||
const remoteComparison = await ipcRenderer.invoke('renderer:get-collection-drift', {
|
||||
collectionPath: collection.pathname,
|
||||
brunoConfig: collection.brunoConfig,
|
||||
compareSpec: currentSpecDrift.newSpec
|
||||
});
|
||||
if (!remoteComparison.error) {
|
||||
@@ -380,7 +370,6 @@ const useOpenAPISync = (collection) => {
|
||||
|
||||
await ipcRenderer.invoke('renderer:update-openapi-sync-config', {
|
||||
collectionPath: collection.pathname,
|
||||
oldSourceUrl: openApiSyncConfig?.sourceUrl,
|
||||
config: {
|
||||
sourceUrl: newUrl,
|
||||
autoCheck,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { formatIpcError } from 'utils/common/error';
|
||||
|
||||
const useSyncFlow = ({
|
||||
collection, specDrift, remoteDrift, collectionDrift,
|
||||
sourceUrl, setError, checkForUpdates
|
||||
setError, checkForUpdates
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -65,7 +65,6 @@ const useSyncFlow = ({
|
||||
await ipcRenderer.invoke('renderer:apply-openapi-sync', {
|
||||
collectionUid: collection.uid,
|
||||
collectionPath: collection.pathname,
|
||||
sourceUrl: sourceUrl.trim(),
|
||||
addNewRequests: mode !== 'spec-only',
|
||||
removeDeletedRequests: localOnlyIds.length > 0,
|
||||
diff: filteredDiff,
|
||||
@@ -121,6 +120,13 @@ const useSyncFlow = ({
|
||||
return new Set((specDrift?.removed || []).map((ep) => ep.id));
|
||||
}, [specDrift]);
|
||||
|
||||
const handleRestoreSpec = () => {
|
||||
const localOnlyIds = (remoteDrift?.localOnly || [])
|
||||
.filter((ep) => specRemovedIds.has(ep.id))
|
||||
.map((ep) => ep.id);
|
||||
performSync({ localOnlyIds, endpointDecisions: {} }, 'sync');
|
||||
};
|
||||
|
||||
const handleConfirmModalSync = () => {
|
||||
const localOnlyIds = (remoteDrift?.localOnly || [])
|
||||
.filter((ep) => specRemovedIds.has(ep.id))
|
||||
@@ -150,7 +156,7 @@ const useSyncFlow = ({
|
||||
|
||||
return {
|
||||
isSyncing, showConfirmModal, confirmGroups,
|
||||
handleSyncNow,
|
||||
handleSyncNow, handleRestoreSpec,
|
||||
handleApplySync, cancelConfirmModal, handleConfirmModalSync
|
||||
};
|
||||
};
|
||||
|
||||
@@ -129,7 +129,6 @@ const OpenAPISyncTab = ({ collection }) => {
|
||||
remoteDrift={remoteDrift}
|
||||
onTabSelect={setActiveTab}
|
||||
error={error}
|
||||
isLoading={isLoading}
|
||||
onOpenSettings={() => setShowSettingsModal(true)}
|
||||
/>
|
||||
<p className="beta-feedback-inline">
|
||||
@@ -157,6 +156,7 @@ const OpenAPISyncTab = ({ collection }) => {
|
||||
lastSyncDate={openApiSyncConfig?.lastSyncDate}
|
||||
onOpenEndpoint={openEndpointInTab}
|
||||
isLoading={isDriftLoading || isLoading}
|
||||
onTabSelect={setActiveTab}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1271,11 +1271,10 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
|
||||
|
||||
// Save OpenAPI spec file for sync support
|
||||
if (rawOpenAPISpec && brunoConfig.openapi?.length) {
|
||||
const importSourceUrl = brunoConfig.openapi[0].sourceUrl;
|
||||
const specContent = typeof rawOpenAPISpec === 'string'
|
||||
? rawOpenAPISpec
|
||||
: JSON.stringify(rawOpenAPISpec, null, 2);
|
||||
await saveSpecAndUpdateMetadata({ collectionPath, specContent, sourceUrl: importSourceUrl });
|
||||
await saveSpecAndUpdateMetadata({ collectionPath, specContent });
|
||||
}
|
||||
|
||||
const { size, filesCount } = await getCollectionStats(collectionPath);
|
||||
|
||||
@@ -12,7 +12,7 @@ const {
|
||||
stringifyFolder
|
||||
} = require('@usebruno/filestore');
|
||||
const { openApiToBruno } = require('@usebruno/converters');
|
||||
const { writeFile, sanitizeName, getCollectionFormat } = require('../utils/filesystem');
|
||||
const { writeFile, sanitizeName, getCollectionFormat, posixifyPath } = require('../utils/filesystem');
|
||||
const { getEnvVars } = require('../utils/collection');
|
||||
const { getProcessEnvVars } = require('../store/process-env');
|
||||
const { getCertsAndProxyConfig } = require('./network/cert-utils');
|
||||
@@ -84,6 +84,11 @@ const isValidHttpUrl = (urlString) => {
|
||||
|
||||
const isLocalFilePath = (str) => !isValidHttpUrl(str) && typeof str === 'string' && str.length > 0;
|
||||
|
||||
const resolveSourceUrl = (collectionPath, sourceUrl) => {
|
||||
if (!sourceUrl || isValidHttpUrl(sourceUrl)) return sourceUrl;
|
||||
return path.resolve(collectionPath, sourceUrl);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the directory where OpenAPI spec files are stored in AppData.
|
||||
*/
|
||||
@@ -127,8 +132,8 @@ const getSpecEntriesForCollection = (collectionPath) => {
|
||||
/**
|
||||
* Get the spec entry for a specific sourceUrl within a collection.
|
||||
*/
|
||||
const getSpecEntryForUrl = (collectionPath, sourceUrl) => {
|
||||
return getSpecEntriesForCollection(collectionPath).find((e) => e.sourceUrl === sourceUrl) || null;
|
||||
const getSpecEntryForUrl = (collectionPath) => {
|
||||
return getSpecEntriesForCollection(collectionPath)[0] || null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -260,6 +265,14 @@ const loadBrunoConfig = (collectionPath) => {
|
||||
brunoConfig = JSON.parse(fs.readFileSync(brunoJsonPath, 'utf8'));
|
||||
}
|
||||
|
||||
// Resolve relative openapi sourceUrls to absolute so all callers get consistent paths
|
||||
if (Array.isArray(brunoConfig?.openapi)) {
|
||||
brunoConfig.openapi = brunoConfig.openapi.map((entry) => ({
|
||||
...entry,
|
||||
sourceUrl: resolveSourceUrl(collectionPath, entry.sourceUrl)
|
||||
}));
|
||||
}
|
||||
|
||||
return { format, brunoConfig, collectionRoot };
|
||||
};
|
||||
|
||||
@@ -267,12 +280,23 @@ const loadBrunoConfig = (collectionPath) => {
|
||||
* Save bruno config to disk (bruno.json or opencollection.yml).
|
||||
*/
|
||||
const saveBrunoConfig = async (collectionPath, format, brunoConfig, collectionRoot) => {
|
||||
// Convert absolute openapi sourceUrls back to collection-relative for git-shareability
|
||||
const configToSave = { ...brunoConfig };
|
||||
if (Array.isArray(configToSave?.openapi)) {
|
||||
configToSave.openapi = configToSave.openapi.map((entry) => ({
|
||||
...entry,
|
||||
sourceUrl: (entry.sourceUrl && !isValidHttpUrl(entry.sourceUrl))
|
||||
? posixifyPath(path.relative(collectionPath, entry.sourceUrl))
|
||||
: entry.sourceUrl
|
||||
}));
|
||||
}
|
||||
|
||||
if (format === 'yml') {
|
||||
const content = await stringifyCollection(collectionRoot, brunoConfig, { format });
|
||||
const content = await stringifyCollection(collectionRoot, configToSave, { format });
|
||||
await writeFile(path.join(collectionPath, 'opencollection.yml'), content);
|
||||
} else {
|
||||
const brunoJsonPath = path.join(collectionPath, 'bruno.json');
|
||||
await writeFile(brunoJsonPath, JSON.stringify(brunoConfig, null, 2));
|
||||
await writeFile(brunoJsonPath, JSON.stringify(configToSave, null, 2));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -346,9 +370,9 @@ const saveOpenApiSpecFile = async ({ collectionPath, content, sourceUrl }) => {
|
||||
const specsDir = getSpecsDir();
|
||||
await fsExtra.ensureDir(specsDir);
|
||||
|
||||
const resolvedUrl = resolveSourceUrl(collectionPath, sourceUrl);
|
||||
const meta = loadSpecMetadata();
|
||||
const entries = meta[collectionPath] || [];
|
||||
const existingEntry = entries.find((e) => e.sourceUrl === sourceUrl);
|
||||
const existingEntry = (meta[collectionPath] || [])[0];
|
||||
|
||||
let filename;
|
||||
if (existingEntry) {
|
||||
@@ -358,10 +382,12 @@ const saveOpenApiSpecFile = async ({ collectionPath, content, sourceUrl }) => {
|
||||
// Generate a new UUID filename based on content type
|
||||
const ext = isYamlContent(content) ? 'yaml' : 'json';
|
||||
filename = `${crypto.randomUUID()}.${ext}`;
|
||||
meta[collectionPath] = [...entries, { filename, sourceUrl }];
|
||||
saveSpecMetadata(meta);
|
||||
}
|
||||
|
||||
// Always replace with a single entry (one spec per collection for now)
|
||||
meta[collectionPath] = [{ filename, sourceUrl: resolvedUrl }];
|
||||
saveSpecMetadata(meta);
|
||||
|
||||
await writeFile(path.join(specsDir, filename), content);
|
||||
};
|
||||
|
||||
@@ -369,8 +395,9 @@ const saveOpenApiSpecFile = async ({ collectionPath, content, sourceUrl }) => {
|
||||
* Save an OpenAPI spec file and update sync metadata (lastSyncDate, specHash) in brunoConfig.
|
||||
* Shared by both the IPC handler (connect flow) and the import flow.
|
||||
*/
|
||||
const saveSpecAndUpdateMetadata = async ({ collectionPath, specContent, sourceUrl }) => {
|
||||
const saveSpecAndUpdateMetadata = async ({ collectionPath, specContent }) => {
|
||||
const { format, brunoConfig, collectionRoot } = loadBrunoConfig(collectionPath);
|
||||
const sourceUrl = brunoConfig?.openapi?.[0]?.sourceUrl;
|
||||
|
||||
await saveOpenApiSpecFile({ collectionPath, content: specContent, sourceUrl });
|
||||
|
||||
@@ -383,14 +410,9 @@ const saveSpecAndUpdateMetadata = async ({ collectionPath, specContent, sourceUr
|
||||
|
||||
const specHash = generateSpecHash(parsedSpec);
|
||||
const lastSyncDate = new Date().toISOString();
|
||||
const openapi = brunoConfig.openapi || [];
|
||||
const idx = openapi.findIndex((e) => e.sourceUrl === sourceUrl);
|
||||
if (idx !== -1) {
|
||||
openapi[idx] = { ...openapi[idx], lastSyncDate, specHash };
|
||||
} else {
|
||||
openapi.push({ sourceUrl, lastSyncDate, specHash });
|
||||
}
|
||||
brunoConfig.openapi = openapi;
|
||||
if (brunoConfig.openapi?.[0]) {
|
||||
brunoConfig.openapi[0] = { ...brunoConfig.openapi[0], lastSyncDate, specHash };
|
||||
};
|
||||
|
||||
await saveBrunoConfig(collectionPath, format, brunoConfig, collectionRoot);
|
||||
};
|
||||
@@ -417,7 +439,7 @@ const cleanupSpecFilesForCollection = (collectionPath) => {
|
||||
* Only preserves the user's enabled state; values come from the spec.
|
||||
*/
|
||||
const mergeWithUserValues = (specItems, existingItems) => {
|
||||
return specItems?.map((specItem) => {
|
||||
return (specItems || []).map((specItem) => {
|
||||
const existing = (existingItems || []).find(
|
||||
(e) => e.name === specItem.name && e.value === specItem.value
|
||||
);
|
||||
@@ -440,7 +462,12 @@ const mergeSpecIntoRequest = (existingRequest, specItem, { fullReset = false } =
|
||||
return {
|
||||
...existingRequest,
|
||||
request: {
|
||||
...specItem.request,
|
||||
...existingRequest.request,
|
||||
url: specItem.request.url,
|
||||
method: specItem.request.method,
|
||||
body: specItem.request.body,
|
||||
auth: specItem.request.auth,
|
||||
docs: specItem.request.docs,
|
||||
params: mergedParams || [],
|
||||
headers: mergedHeaders || []
|
||||
}
|
||||
@@ -648,7 +675,7 @@ const compareRequestFields = (specRequest, actualRequest) => {
|
||||
*/
|
||||
const loadStoredSpecCollection = (collectionPath, brunoConfig) => {
|
||||
const sourceUrl = brunoConfig?.openapi?.[0]?.sourceUrl;
|
||||
const specEntry = sourceUrl ? getSpecEntryForUrl(collectionPath, sourceUrl) : null;
|
||||
const specEntry = sourceUrl ? getSpecEntryForUrl(collectionPath) : null;
|
||||
const specPath = specEntry ? path.join(getSpecsDir(), specEntry.filename) : null;
|
||||
|
||||
if (!specPath || !fs.existsSync(specPath)) {
|
||||
@@ -761,7 +788,7 @@ const registerOpenAPISyncIpc = (mainWindow) => {
|
||||
};
|
||||
};
|
||||
|
||||
const specEntry = getSpecEntryForUrl(collectionPath, sourceUrl);
|
||||
const specEntry = getSpecEntryForUrl(collectionPath);
|
||||
const storedSpecPath = specEntry ? path.join(getSpecsDir(), specEntry.filename) : null;
|
||||
|
||||
let storedSpec = null;
|
||||
@@ -866,18 +893,13 @@ const registerOpenAPISyncIpc = (mainWindow) => {
|
||||
});
|
||||
|
||||
// Collection Drift Detection - compare stored spec (converted to bru) vs actual .bru files
|
||||
ipcMain.handle('renderer:get-collection-drift', async (event, { collectionPath, brunoConfig: passedBrunoConfig, compareSpec }) => {
|
||||
ipcMain.handle('renderer:get-collection-drift', async (event, { collectionPath, compareSpec }) => {
|
||||
try {
|
||||
// Use passed brunoConfig if available, otherwise read from disk
|
||||
let brunoConfig;
|
||||
if (passedBrunoConfig) {
|
||||
brunoConfig = passedBrunoConfig;
|
||||
} else {
|
||||
try {
|
||||
({ brunoConfig } = loadBrunoConfig(collectionPath));
|
||||
} catch (err) {
|
||||
return { error: err.message };
|
||||
}
|
||||
try {
|
||||
({ brunoConfig } = loadBrunoConfig(collectionPath));
|
||||
} catch (err) {
|
||||
return { error: err.message };
|
||||
}
|
||||
|
||||
// Load spec to compare against — use compareSpec if provided, otherwise read stored spec from disk
|
||||
@@ -888,7 +910,7 @@ const registerOpenAPISyncIpc = (mainWindow) => {
|
||||
specToCompare = compareSpec;
|
||||
} else {
|
||||
const driftSourceUrl = brunoConfig?.openapi?.[0]?.sourceUrl;
|
||||
const driftSpecEntry = driftSourceUrl ? getSpecEntryForUrl(collectionPath, driftSourceUrl) : null;
|
||||
const driftSpecEntry = driftSourceUrl ? getSpecEntryForUrl(collectionPath) : null;
|
||||
const storedSpecPath = driftSpecEntry ? path.join(getSpecsDir(), driftSpecEntry.filename) : null;
|
||||
|
||||
if (!storedSpecPath || !fs.existsSync(storedSpecPath)) {
|
||||
@@ -1057,7 +1079,7 @@ const registerOpenAPISyncIpc = (mainWindow) => {
|
||||
let specToUse = newSpec;
|
||||
if (!specToUse) {
|
||||
const diffSourceUrl = brunoConfig?.openapi?.[0]?.sourceUrl;
|
||||
const diffSpecEntry = diffSourceUrl ? getSpecEntryForUrl(collectionPath, diffSourceUrl) : null;
|
||||
const diffSpecEntry = diffSourceUrl ? getSpecEntryForUrl(collectionPath) : null;
|
||||
const storedSpecPath = diffSpecEntry ? path.join(getSpecsDir(), diffSpecEntry.filename) : null;
|
||||
if (storedSpecPath && fs.existsSync(storedSpecPath)) {
|
||||
const content = fs.readFileSync(storedSpecPath, 'utf8');
|
||||
@@ -1135,9 +1157,10 @@ const registerOpenAPISyncIpc = (mainWindow) => {
|
||||
});
|
||||
|
||||
// Sync modes: 'spec-only' | 'reset' | 'sync' (default)
|
||||
ipcMain.handle('renderer:apply-openapi-sync', async (event, { collectionPath, sourceUrl, addNewRequests, removeDeletedRequests, diff, localOnlyToRemove = [], driftedToReset = [], mode = 'sync', endpointDecisions = {} }) => {
|
||||
ipcMain.handle('renderer:apply-openapi-sync', async (event, { collectionPath, addNewRequests, removeDeletedRequests, diff, localOnlyToRemove = [], driftedToReset = [], mode = 'sync', endpointDecisions = {} }) => {
|
||||
try {
|
||||
const { format, brunoConfig, collectionRoot } = loadBrunoConfig(collectionPath);
|
||||
const sourceUrl = brunoConfig?.openapi?.[0]?.sourceUrl;
|
||||
|
||||
// Mode: spec-only - Just save the spec, don't touch collection
|
||||
if (mode === 'spec-only') {
|
||||
@@ -1147,16 +1170,13 @@ const registerOpenAPISyncIpc = (mainWindow) => {
|
||||
}
|
||||
|
||||
// Update sync metadata
|
||||
const openapi = brunoConfig.openapi || [];
|
||||
const specOnlyIdx = openapi.findIndex((e) => e.sourceUrl === sourceUrl);
|
||||
if (specOnlyIdx !== -1) {
|
||||
openapi[specOnlyIdx] = {
|
||||
...openapi[specOnlyIdx],
|
||||
if (brunoConfig.openapi?.[0]) {
|
||||
brunoConfig.openapi[0] = {
|
||||
...brunoConfig.openapi[0],
|
||||
lastSyncDate: new Date().toISOString(),
|
||||
specHash: generateSpecHash(diff.newSpec)
|
||||
};
|
||||
}
|
||||
brunoConfig.openapi = openapi;
|
||||
|
||||
await saveBrunoConfig(collectionPath, format, brunoConfig, collectionRoot);
|
||||
|
||||
@@ -1165,8 +1185,7 @@ const registerOpenAPISyncIpc = (mainWindow) => {
|
||||
|
||||
// Mode: reset - Save spec and reset all endpoints to spec (preserve tests/scripts)
|
||||
if (mode === 'reset' && diff.newSpec) {
|
||||
const openapiEntryReset = (brunoConfig.openapi || []).find((e) => e.sourceUrl === sourceUrl);
|
||||
const groupBy = openapiEntryReset?.groupBy || 'tags';
|
||||
const groupBy = brunoConfig?.openapi?.[0]?.groupBy || 'tags';
|
||||
const newCollection = openApiToBruno(diff.newSpec, { groupBy });
|
||||
|
||||
// Build map of spec items by endpoint ID
|
||||
@@ -1231,16 +1250,13 @@ const registerOpenAPISyncIpc = (mainWindow) => {
|
||||
await saveOpenApiSpecFile({ collectionPath, content: specContent, sourceUrl });
|
||||
|
||||
// Update sync metadata
|
||||
const openapiReset = brunoConfig.openapi || [];
|
||||
const resetIdx = openapiReset.findIndex((e) => e.sourceUrl === sourceUrl);
|
||||
if (resetIdx !== -1) {
|
||||
openapiReset[resetIdx] = {
|
||||
...openapiReset[resetIdx],
|
||||
if (brunoConfig.openapi?.[0]) {
|
||||
brunoConfig.openapi[0] = {
|
||||
...brunoConfig.openapi[0],
|
||||
lastSyncDate: new Date().toISOString(),
|
||||
specHash: generateSpecHash(diff.newSpec)
|
||||
};
|
||||
}
|
||||
brunoConfig.openapi = openapiReset;
|
||||
|
||||
await saveBrunoConfig(collectionPath, format, brunoConfig, collectionRoot);
|
||||
|
||||
@@ -1248,8 +1264,7 @@ const registerOpenAPISyncIpc = (mainWindow) => {
|
||||
}
|
||||
|
||||
// Mode: sync (default) — compute shared values once
|
||||
const syncEntry = (brunoConfig.openapi || []).find((e) => e.sourceUrl === sourceUrl);
|
||||
const groupBy = syncEntry?.groupBy || 'tags';
|
||||
const groupBy = brunoConfig?.openapi?.[0]?.groupBy || 'tags';
|
||||
let newCollection;
|
||||
if (diff.newSpec) {
|
||||
try {
|
||||
@@ -1396,7 +1411,7 @@ const registerOpenAPISyncIpc = (mainWindow) => {
|
||||
// Reuse newCollection if available, otherwise fall back to stored spec
|
||||
let driftCollection = newCollection;
|
||||
if (!driftCollection) {
|
||||
const applySpecEntry = getSpecEntryForUrl(collectionPath, sourceUrl);
|
||||
const applySpecEntry = getSpecEntryForUrl(collectionPath);
|
||||
const storedSpecPath = applySpecEntry ? path.join(getSpecsDir(), applySpecEntry.filename) : null;
|
||||
if (storedSpecPath && fs.existsSync(storedSpecPath)) {
|
||||
try {
|
||||
@@ -1445,20 +1460,17 @@ const registerOpenAPISyncIpc = (mainWindow) => {
|
||||
await saveOpenApiSpecFile({ collectionPath, content: specContent, sourceUrl });
|
||||
}
|
||||
|
||||
const openapiSync = brunoConfig.openapi || [];
|
||||
const syncIdx = openapiSync.findIndex((e) => e.sourceUrl === sourceUrl);
|
||||
if (syncIdx !== -1) {
|
||||
if (brunoConfig.openapi?.[0]) {
|
||||
const updated = {
|
||||
...openapiSync[syncIdx],
|
||||
...brunoConfig.openapi[0],
|
||||
lastSyncDate: new Date().toISOString()
|
||||
};
|
||||
// Only update specHash when we have a valid newSpec, otherwise preserve existing hash
|
||||
if (diff.newSpec) {
|
||||
updated.specHash = generateSpecHash(diff.newSpec);
|
||||
}
|
||||
openapiSync[syncIdx] = updated;
|
||||
brunoConfig.openapi[0] = updated;
|
||||
}
|
||||
brunoConfig.openapi = openapiSync;
|
||||
|
||||
await saveBrunoConfig(collectionPath, format, brunoConfig, collectionRoot);
|
||||
|
||||
@@ -1470,7 +1482,7 @@ const registerOpenAPISyncIpc = (mainWindow) => {
|
||||
});
|
||||
|
||||
// Update OpenAPI sync configuration (e.g., source URL)
|
||||
ipcMain.handle('renderer:update-openapi-sync-config', async (event, { collectionPath, oldSourceUrl, config }) => {
|
||||
ipcMain.handle('renderer:update-openapi-sync-config', async (event, { collectionPath, config }) => {
|
||||
try {
|
||||
const { format, brunoConfig, collectionRoot } = loadBrunoConfig(collectionPath);
|
||||
|
||||
@@ -1493,37 +1505,18 @@ const registerOpenAPISyncIpc = (mainWindow) => {
|
||||
throw new Error('Invalid URL: only http and https URLs are allowed');
|
||||
}
|
||||
|
||||
// Convert absolute local file paths to collection-relative (git-shareable)
|
||||
if (path.isAbsolute(sanitizedConfig.sourceUrl)) {
|
||||
sanitizedConfig.sourceUrl = path.relative(collectionPath, sanitizedConfig.sourceUrl);
|
||||
}
|
||||
// Resolve to absolute for consistent internal handling (saveBrunoConfig converts back to relative)
|
||||
sanitizedConfig.sourceUrl = resolveSourceUrl(collectionPath, sanitizedConfig.sourceUrl);
|
||||
|
||||
// If sourceUrl is changing, remove the old entry and its metadata
|
||||
const openapi = brunoConfig.openapi || [];
|
||||
if (oldSourceUrl && oldSourceUrl !== sanitizedConfig.sourceUrl) {
|
||||
const filteredOpenapi = openapi.filter((e) => e.sourceUrl !== oldSourceUrl);
|
||||
brunoConfig.openapi = filteredOpenapi;
|
||||
// Clean up metadata entry for old sourceUrl (keep spec file for potential re-use)
|
||||
const meta = loadSpecMetadata();
|
||||
if (meta[collectionPath]) {
|
||||
meta[collectionPath] = meta[collectionPath].filter((e) => e.sourceUrl !== oldSourceUrl);
|
||||
if (meta[collectionPath].length === 0) delete meta[collectionPath];
|
||||
saveSpecMetadata(meta);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply defaults for new entries
|
||||
const updatedOpenapi = brunoConfig.openapi || [];
|
||||
const idx = updatedOpenapi.findIndex((e) => e.sourceUrl === sanitizedConfig.sourceUrl);
|
||||
const isNewEntry = idx === -1;
|
||||
if (isNewEntry) {
|
||||
// Update or create the single openapi entry
|
||||
const existingEntry = brunoConfig.openapi?.[0];
|
||||
if (existingEntry) {
|
||||
brunoConfig.openapi = [{ ...existingEntry, ...sanitizedConfig }];
|
||||
} else {
|
||||
if (!('autoCheck' in sanitizedConfig)) sanitizedConfig.autoCheck = true;
|
||||
if (!('autoCheckInterval' in sanitizedConfig)) sanitizedConfig.autoCheckInterval = 5;
|
||||
updatedOpenapi.push(sanitizedConfig);
|
||||
} else {
|
||||
updatedOpenapi[idx] = { ...updatedOpenapi[idx], ...sanitizedConfig };
|
||||
brunoConfig.openapi = [sanitizedConfig];
|
||||
}
|
||||
brunoConfig.openapi = updatedOpenapi;
|
||||
|
||||
// Save updated config
|
||||
await saveBrunoConfig(collectionPath, format, brunoConfig, collectionRoot);
|
||||
@@ -1536,9 +1529,9 @@ const registerOpenAPISyncIpc = (mainWindow) => {
|
||||
});
|
||||
|
||||
// Save OpenAPI spec file and update sync metadata (used by both connect and import flows)
|
||||
ipcMain.handle('renderer:save-openapi-spec', async (event, { collectionPath, specContent, sourceUrl }) => {
|
||||
ipcMain.handle('renderer:save-openapi-spec', async (event, { collectionPath, specContent }) => {
|
||||
try {
|
||||
await saveSpecAndUpdateMetadata({ collectionPath, specContent, sourceUrl });
|
||||
await saveSpecAndUpdateMetadata({ collectionPath, specContent });
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error saving OpenAPI spec file:', error);
|
||||
@@ -1566,9 +1559,9 @@ const registerOpenAPISyncIpc = (mainWindow) => {
|
||||
});
|
||||
|
||||
// Read stored OpenAPI spec file from AppData
|
||||
ipcMain.handle('renderer:read-openapi-spec', async (event, { collectionPath, sourceUrl }) => {
|
||||
ipcMain.handle('renderer:read-openapi-spec', async (event, { collectionPath }) => {
|
||||
try {
|
||||
const entry = getSpecEntryForUrl(collectionPath, sourceUrl);
|
||||
const entry = getSpecEntryForUrl(collectionPath);
|
||||
if (!entry) return { error: 'Spec file not found' };
|
||||
const specPath = path.join(getSpecsDir(), entry.filename);
|
||||
if (!fs.existsSync(specPath)) return { error: 'Spec file not found' };
|
||||
@@ -1579,31 +1572,22 @@ const registerOpenAPISyncIpc = (mainWindow) => {
|
||||
});
|
||||
|
||||
// Remove OpenAPI sync configuration (disconnect sync)
|
||||
ipcMain.handle('renderer:remove-openapi-sync-config', async (event, { collectionPath, sourceUrl, deleteSpecFile = false }) => {
|
||||
ipcMain.handle('renderer:remove-openapi-sync-config', async (event, { collectionPath, deleteSpecFile = false }) => {
|
||||
try {
|
||||
const { format, brunoConfig, collectionRoot } = loadBrunoConfig(collectionPath);
|
||||
|
||||
// Remove matching openapi entry from config array
|
||||
if (brunoConfig.openapi?.length) {
|
||||
brunoConfig.openapi = brunoConfig.openapi.filter((e) => e.sourceUrl !== sourceUrl);
|
||||
if (brunoConfig.openapi.length === 0) {
|
||||
delete brunoConfig.openapi;
|
||||
}
|
||||
}
|
||||
|
||||
// Save updated config
|
||||
// Remove openapi config
|
||||
delete brunoConfig.openapi;
|
||||
await saveBrunoConfig(collectionPath, format, brunoConfig, collectionRoot);
|
||||
|
||||
// Remove spec file from AppData if user opted in
|
||||
// Remove spec file and metadata for this collection
|
||||
const meta = loadSpecMetadata();
|
||||
const entries = meta[collectionPath] || [];
|
||||
const entry = entries.find((e) => e.sourceUrl === sourceUrl);
|
||||
const entry = (meta[collectionPath] || [])[0];
|
||||
if (entry && deleteSpecFile) {
|
||||
const specPath = path.join(getSpecsDir(), entry.filename);
|
||||
if (fs.existsSync(specPath)) fs.unlinkSync(specPath);
|
||||
}
|
||||
meta[collectionPath] = entries.filter((e) => e.sourceUrl !== sourceUrl);
|
||||
if (meta[collectionPath].length === 0) delete meta[collectionPath];
|
||||
delete meta[collectionPath];
|
||||
saveSpecMetadata(meta);
|
||||
|
||||
return { success: true };
|
||||
|
||||
@@ -501,8 +501,11 @@ const scanForBrunoFiles = async (dir) => {
|
||||
return brunoFolders;
|
||||
};
|
||||
|
||||
const posixifyPath = (p) => (p ? p.replace(/\\/g, '/') : p);
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_GITIGNORE,
|
||||
posixifyPath,
|
||||
isValidPathname,
|
||||
exists,
|
||||
isSymbolicLink,
|
||||
|
||||
Reference in New Issue
Block a user