mirror of
https://github.com/Kong/insomnia.git
synced 2026-10-06 21:14:38 -04:00
feat(INS-3641): add Export OpenAPI Spec to the document context menu (#10538)
## Summary Adds an **Export OpenAPI Spec** item right under the existing **Export** option in the sidebar workspace dropdown — the context menu for an API collection (opened via the ellipsis trigger button or by right-clicking the workspace row). Selecting it prompts for **YAML or JSON**, then a save location, and writes the spec file — the same flow as the existing Insomnia/HAR exporters (format modal → native save dialog → `writeFile`), reusing `showSaveExportedFileDialog` / `writeExportedFileToFileSystem` so the last export path is remembered. Closes INS-3641 ## Behavior details - The item shows for collection/design scope workspaces. If the spec is empty or missing, clicking it shows a `Cannot export` alert explaining there is nothing to export, rather than writing an empty file. - Same serialization as inso: matching format passes the contents through untouched; cross-format converts via `YAML.parse` + `JSON.stringify(…, 2)` / `YAML.stringify`, with an error modal if the spec is invalid. - Records `dataExport` telemetry like every sibling exporter in the file. - Spec format detection/conversion are shared with the Spec editor toolbar via new `detectApiSpecSyntax` / `convertApiSpecSyntax` helpers in `common/api-specs` (the toolbar previously inlined the same JSON-parse probe).
This commit is contained in:
1 parent
b353ed3f44
commit
2d56f154d5
12 files changed
+389
-17
No files matched your search
@@ -20,3 +20,6 @@ fixtures/inso-nedb/insomnia.OAuth2Token.db
|
||||
fixtures/inso-nedb/insomnia.PluginData.db
|
||||
fixtures/inso-nedb/insomnia.CaCertificate.db
|
||||
fixtures/inso-nedb/insomnia.CloudCredential.db
|
||||
|
||||
# Playwright junit/xml run artifacts
|
||||
test-results.xml
|
||||
@@ -11,7 +11,7 @@ export class ExportModal {
|
||||
* Handles the export type selection modal (Insomnia v5 or HAR).
|
||||
* @param format - The format to select ('yaml' for Insomnia v5, 'har' for HAR)
|
||||
*/
|
||||
async selectExportFormat(format: 'yaml' | 'har'): Promise<void> {
|
||||
async selectExportFormat(format: 'yaml' | 'har' | 'json'): Promise<void> {
|
||||
await this.page.getByText('Which format would you like to export as?').waitFor({ state: 'visible' });
|
||||
|
||||
// The modal uses a <select> element, so we need to use selectOption
|
||||
|
||||
@@ -145,7 +145,7 @@ export class NavigationSidebar {
|
||||
workspaceName: string;
|
||||
}): Promise<void> {
|
||||
await this.openWorkspaceActionsDropdown(workspaceName);
|
||||
await this.page.getByRole('menuitemradio', { name: actionName }).click();
|
||||
await this.page.getByRole('menuitemradio', { name: actionName, exact: true }).click();
|
||||
}
|
||||
|
||||
async expandWorkspace(workspaceName: string): Promise<void> {
|
||||
|
||||
@@ -363,7 +363,7 @@ export class ProjectPage extends BasePage {
|
||||
await this.workspaceList.openWorkspaceCardDropdown(workspaceName);
|
||||
|
||||
// Click Export option
|
||||
await this.page.getByRole('menuitem', { name: 'Export' }).click();
|
||||
await this.page.getByRole('menuitem', { name: 'Export', exact: true }).click();
|
||||
|
||||
// Click Export button in the export requests modal (all requests selected by default)
|
||||
await this.page.getByRole('dialog').getByRole('button', { name: 'Export' }).click();
|
||||
|
||||
@@ -12,7 +12,7 @@ test('can send requests', async ({ page, insomnia }) => {
|
||||
await insomnia.projectPage.importFixture('smoke-test-collection.yaml');
|
||||
|
||||
await insomnia.navigationSidebar.openWorkspaceActionsDropdown('Smoke tests');
|
||||
await page.getByRole('menuitemradio', { name: 'Export' }).click();
|
||||
await page.getByRole('menuitemradio', { name: 'Export', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Export' }).click();
|
||||
await page.getByText('Which format would you like to export as?').click();
|
||||
await insomnia.pressEscape();
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
import { test } from '../../playwright/test';
|
||||
import {
|
||||
cleanupExportDir,
|
||||
createTempExportDir,
|
||||
mockSaveDialogForFile,
|
||||
readExportedFile,
|
||||
waitForExportFiles,
|
||||
} from '../../playwright/utils';
|
||||
|
||||
test.describe('Export OpenAPI Spec', () => {
|
||||
test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms');
|
||||
|
||||
// Creates an API collection and fills its spec with the Pet Store example
|
||||
const createApiCollectionWithPetStoreSpec = async (page: Page) => {
|
||||
await page.getByRole('button', { name: 'Create document' }).click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Create' }).click();
|
||||
await page.click('text=Use example');
|
||||
await page.click('text=Pet Store');
|
||||
await expect.soft(page.locator('.pane-one').getByTestId('CodeEditor')).toContainText('openapi: 3.0.4');
|
||||
};
|
||||
|
||||
test('exports the spec of an API collection as YAML from the sidebar dropdown', async ({ page, app, insomnia }) => {
|
||||
await createApiCollectionWithPetStoreSpec(page);
|
||||
|
||||
const tempDir = createTempExportDir();
|
||||
try {
|
||||
const exportPath = path.join(tempDir, 'spec-export.yaml');
|
||||
await mockSaveDialogForFile(app, exportPath);
|
||||
|
||||
await insomnia.navigationSidebar.selectWorkspaceDropdownOption({
|
||||
actionName: 'Export OpenAPI Spec',
|
||||
workspaceName: 'My API Collection',
|
||||
});
|
||||
|
||||
// The format modal defaults to YAML
|
||||
await insomnia.exportModal.selectExportFormat('yaml');
|
||||
|
||||
await waitForExportFiles(tempDir, 1);
|
||||
const contents = readExportedFile(exportPath);
|
||||
expect.soft(contents).toContain('openapi: 3.0.4');
|
||||
expect.soft(contents).toContain('Pet Store');
|
||||
} finally {
|
||||
cleanupExportDir(tempDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('exports the spec of an API collection as JSON from the sidebar dropdown', async ({ page, app, insomnia }) => {
|
||||
await createApiCollectionWithPetStoreSpec(page);
|
||||
|
||||
const tempDir = createTempExportDir();
|
||||
try {
|
||||
const exportPath = path.join(tempDir, 'spec-export.json');
|
||||
await mockSaveDialogForFile(app, exportPath);
|
||||
|
||||
await insomnia.navigationSidebar.selectWorkspaceDropdownOption({
|
||||
actionName: 'Export OpenAPI Spec',
|
||||
workspaceName: 'My API Collection',
|
||||
});
|
||||
await insomnia.exportModal.selectExportFormat('json');
|
||||
|
||||
await waitForExportFiles(tempDir, 1);
|
||||
const contents = JSON.parse(readExportedFile(exportPath));
|
||||
expect.soft(contents).toMatchObject({ openapi: '3.0.4' });
|
||||
expect.soft(contents.info.title).toContain('Pet');
|
||||
} finally {
|
||||
cleanupExportDir(tempDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('offers Export OpenAPI Spec when the API collection is opened via right-click', async ({ page, insomnia }) => {
|
||||
await createApiCollectionWithPetStoreSpec(page);
|
||||
|
||||
// Right-click opens the dropdown via the controlled isOpen prop, not the trigger
|
||||
await insomnia.navigationSidebar.workspaceRow('My API Collection').click({ button: 'right' });
|
||||
await expect.soft(page.getByRole('menuitemradio', { name: 'Export OpenAPI Spec' })).toBeVisible();
|
||||
await page.keyboard.press('Escape');
|
||||
});
|
||||
|
||||
test('prompts an error while the spec is empty, then exports once it is filled in', async ({
|
||||
page,
|
||||
app,
|
||||
insomnia,
|
||||
}) => {
|
||||
await page.getByRole('button', { name: 'Create document' }).click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Empty spec: the item is offered, but clicking it explains there is nothing to export
|
||||
// instead of writing an empty file.
|
||||
await insomnia.navigationSidebar.openWorkspaceActionsDropdown('My API Collection');
|
||||
await page.getByRole('menuitemradio', { name: 'Export OpenAPI Spec' }).click();
|
||||
await expect.soft(page.getByText('does not contain an OpenAPI specification to export')).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Modal Close Button' }).click();
|
||||
|
||||
// Fill the spec in: the same entry point now exports the file
|
||||
const tempDir = createTempExportDir();
|
||||
try {
|
||||
const exportPath = path.join(tempDir, 'spec-export.yaml');
|
||||
await mockSaveDialogForFile(app, exportPath);
|
||||
|
||||
await page.click('text=Use example');
|
||||
await page.click('text=Pet Store');
|
||||
await expect.soft(page.locator('.pane-one').getByTestId('CodeEditor')).toContainText('openapi: 3.0.4');
|
||||
|
||||
await insomnia.navigationSidebar.selectWorkspaceDropdownOption({
|
||||
actionName: 'Export OpenAPI Spec',
|
||||
workspaceName: 'My API Collection',
|
||||
});
|
||||
await insomnia.exportModal.selectExportFormat('yaml');
|
||||
|
||||
await waitForExportFiles(tempDir, 1);
|
||||
expect.soft(readExportedFile(exportPath)).toContain('openapi: 3.0.4');
|
||||
} finally {
|
||||
cleanupExportDir(tempDir);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -26,7 +26,7 @@ test.describe('test hidden window handling', () => {
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
await insomnia.navigationSidebar.openWorkspaceActionsDropdown('Pre-request Scripts');
|
||||
await page.getByRole('menuitemradio', { name: 'Export' }).click();
|
||||
await page.getByRole('menuitemradio', { name: 'Export', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Export' }).click();
|
||||
await page.getByText('Which format would you like to export as?').click();
|
||||
await page.locator('.app').press('Escape');
|
||||
|
||||
@@ -39,6 +39,29 @@ export function parseApiSpec(rawDocument: string) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the serialization syntax of an API spec by probing it with JSON.parse.
|
||||
* JSON is valid YAML, so anything that fails to parse as JSON is treated as YAML.
|
||||
*/
|
||||
export function detectApiSpecSyntax(contents: string): 'json' | 'yaml' {
|
||||
try {
|
||||
JSON.parse(contents);
|
||||
return 'json';
|
||||
} catch {
|
||||
return 'yaml';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-serializes an API spec into the requested syntax. Throws if the spec cannot
|
||||
* be parsed (invalid YAML or JSON).
|
||||
*/
|
||||
export function convertApiSpecSyntax(contents: string, to: 'json' | 'yaml'): string {
|
||||
// YAML parses JSON as well
|
||||
const parsed = YAML.parse(contents);
|
||||
return to === 'json' ? JSON.stringify(parsed, null, 2) : YAML.stringify(parsed);
|
||||
}
|
||||
|
||||
export function resolveComponentSchemaRefs(spec: ParsedApiSpec, methodInfo: Record<string, any>) {
|
||||
const schemas = spec.contents?.components?.schemas;
|
||||
if (!schemas) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
exportGlobalEnvironmentToFile,
|
||||
exportMcpClientToFile,
|
||||
exportMockServerToFile,
|
||||
exportSpecificationToFile,
|
||||
} from 'insomnia/src/ui/components/settings/import-export';
|
||||
import type { MockServer, Project, Workspace } from 'insomnia-data';
|
||||
import { models, services } from 'insomnia-data';
|
||||
@@ -312,6 +313,16 @@ export const SidebarWorkspaceDropdown = ({
|
||||
return setIsExportModalOpen(true);
|
||||
},
|
||||
},
|
||||
...(isCollectionLike
|
||||
? [
|
||||
{
|
||||
id: 'ExportOpenApiSpec',
|
||||
name: 'Export OpenAPI Spec',
|
||||
icon: 'file-code' as IconName,
|
||||
action: () => exportSpecificationToFile(workspace),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'Settings',
|
||||
name: 'Settings',
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// @vitest-environment jsdom
|
||||
import { AnalyticsEvent } from 'insomnia/src/ui/analytics';
|
||||
import { showError, showModal } from 'insomnia/src/ui/components/modals';
|
||||
import { exportSpecificationToFile } from 'insomnia/src/ui/components/settings/import-export';
|
||||
import { services } from 'insomnia-data';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('~/root', () => ({
|
||||
useRootLoaderData: () => ({}),
|
||||
}));
|
||||
|
||||
vi.mock('insomnia/src/ui/components/modals', async importOriginal => {
|
||||
const actual = await importOriginal<Record<string, unknown>>();
|
||||
return {
|
||||
...actual,
|
||||
showModal: vi.fn(),
|
||||
showError: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const YAML_SPEC = ['openapi: 3.0.0', 'info:', ' title: Pet Store', ' version: 1.0.0', 'paths: {}'].join('\n');
|
||||
const JSON_SPEC = JSON.stringify({ openapi: '3.0.0', info: { title: 'Pet Store', version: '1.0.0' }, paths: {} });
|
||||
|
||||
const mockShowModal = vi.mocked(showModal);
|
||||
const mockShowError = vi.mocked(showError);
|
||||
const mockShowSaveDialog = vi.fn();
|
||||
const mockWriteFile = vi.fn();
|
||||
const mockTrackAnalyticsEvent = vi.fn();
|
||||
|
||||
/**
|
||||
* Drives the SelectModal shown by exportSpecificationToFile() as if the user had
|
||||
* picked a format and pressed Done. The modal host is mocked, so we invoke the
|
||||
* onDone callback directly.
|
||||
*/
|
||||
const selectExportFormat = async (format: 'json' | 'yaml') => {
|
||||
const selectModalCall = mockShowModal.mock.calls.find(
|
||||
([, options]) => typeof (options as { onDone?: unknown })?.onDone === 'function',
|
||||
);
|
||||
expect(selectModalCall, 'expected a format selection modal to be shown').toBeTruthy();
|
||||
const [, options] = selectModalCall! as unknown as [{ name?: string }, { onDone: (format: string | null) => Promise<void> }];
|
||||
mockShowModal.mockClear();
|
||||
await options.onDone(format);
|
||||
};
|
||||
|
||||
const writtenFiles = () => mockWriteFile.mock.calls.map(([{ path, content }]) => ({ path, content }));
|
||||
|
||||
describe('exportSpecificationToFile()', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
Object.assign(window, {
|
||||
dialog: { showSaveDialog: mockShowSaveDialog },
|
||||
main: { writeFile: mockWriteFile, trackAnalyticsEvent: mockTrackAnalyticsEvent },
|
||||
app: { getPath: () => '/desktop' },
|
||||
path: {
|
||||
join: (...args: string[]) => args.join('/'),
|
||||
dirname: (path: string) => path.slice(0, path.lastIndexOf('/')),
|
||||
},
|
||||
});
|
||||
mockShowSaveDialog.mockResolvedValue({ filePath: '/tmp/export/out.yaml', canceled: false });
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
it('shows an error modal when the API collection has no specification', async () => {
|
||||
const workspace = await services.workspace.create({ name: 'Empty API Collection', scope: 'collection' });
|
||||
|
||||
await exportSpecificationToFile(workspace);
|
||||
expect(mockShowModal).toHaveBeenCalledTimes(1);
|
||||
let [, options] = mockShowModal.mock.calls[0] as unknown as [{ name?: string }, { title: string }];
|
||||
expect(options.title).toBe('Cannot export');
|
||||
|
||||
// Same guard covers a spec record whose contents were cleared
|
||||
mockShowModal.mockClear();
|
||||
await services.apiSpec.updateOrCreateForParentId(workspace._id, { contents: '', contentType: 'yaml' });
|
||||
|
||||
await exportSpecificationToFile(workspace);
|
||||
expect(mockShowModal).toHaveBeenCalledTimes(1);
|
||||
[, options] = mockShowModal.mock.calls[0] as unknown as [{ name?: string }, { title: string }];
|
||||
expect(options.title).toBe('Cannot export');
|
||||
});
|
||||
|
||||
it('writes the spec unchanged when the requested format matches the source format', async () => {
|
||||
const workspace = await services.workspace.create({ name: 'YAML API Collection', scope: 'collection' });
|
||||
await services.apiSpec.updateOrCreateForParentId(workspace._id, { contents: YAML_SPEC, contentType: 'yaml' });
|
||||
|
||||
await exportSpecificationToFile(workspace);
|
||||
await selectExportFormat('yaml');
|
||||
|
||||
expect(mockShowError).not.toHaveBeenCalled();
|
||||
expect(writtenFiles()).toEqual([{ path: '/tmp/export/out.yaml', content: YAML_SPEC }]);
|
||||
expect(mockTrackAnalyticsEvent).toHaveBeenCalledWith({
|
||||
event: AnalyticsEvent.dataExport,
|
||||
properties: { type: 'yaml', scope: 'collection' },
|
||||
});
|
||||
});
|
||||
|
||||
it('converts a YAML spec to JSON when requested', async () => {
|
||||
const workspace = await services.workspace.create({ name: 'Converted API Collection', scope: 'collection' });
|
||||
await services.apiSpec.updateOrCreateForParentId(workspace._id, { contents: YAML_SPEC, contentType: 'yaml' });
|
||||
|
||||
await exportSpecificationToFile(workspace);
|
||||
await selectExportFormat('json');
|
||||
|
||||
expect(mockShowError).not.toHaveBeenCalled();
|
||||
const [{ path, content }] = writtenFiles();
|
||||
expect(path).toBe('/tmp/export/out.yaml');
|
||||
expect(JSON.parse(content)).toMatchObject({ openapi: '3.0.0', info: { title: 'Pet Store' } });
|
||||
});
|
||||
|
||||
it('converts a JSON spec to YAML when requested', async () => {
|
||||
const workspace = await services.workspace.create({ name: 'JSON API Collection', scope: 'collection' });
|
||||
await services.apiSpec.updateOrCreateForParentId(workspace._id, { contents: JSON_SPEC, contentType: 'json' });
|
||||
|
||||
await exportSpecificationToFile(workspace);
|
||||
await selectExportFormat('yaml');
|
||||
|
||||
expect(mockShowError).not.toHaveBeenCalled();
|
||||
const [{ content }] = writtenFiles();
|
||||
expect(content).toContain('openapi: 3.0.0');
|
||||
expect(content).toContain('title: Pet Store');
|
||||
});
|
||||
|
||||
it('reports an error instead of writing a file when the spec cannot be converted', async () => {
|
||||
const workspace = await services.workspace.create({ name: 'Invalid API Collection', scope: 'collection' });
|
||||
await services.apiSpec.updateOrCreateForParentId(workspace._id, {
|
||||
contents: 'openapi: [unclosed',
|
||||
contentType: 'yaml',
|
||||
});
|
||||
|
||||
await exportSpecificationToFile(workspace);
|
||||
await selectExportFormat('json');
|
||||
|
||||
expect(mockShowError).toHaveBeenCalledTimes(1);
|
||||
expect(mockWriteFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not write a file when the save dialog is cancelled', async () => {
|
||||
const workspace = await services.workspace.create({ name: 'Cancelled API Collection', scope: 'collection' });
|
||||
await services.apiSpec.updateOrCreateForParentId(workspace._id, { contents: YAML_SPEC, contentType: 'yaml' });
|
||||
mockShowSaveDialog.mockResolvedValue({ filePath: undefined, canceled: true });
|
||||
|
||||
await exportSpecificationToFile(workspace);
|
||||
await selectExportFormat('yaml');
|
||||
|
||||
expect(mockWriteFile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,8 @@
|
||||
import { format } from 'date-fns';
|
||||
import {
|
||||
convertApiSpecSyntax,
|
||||
detectApiSpecSyntax,
|
||||
} from 'insomnia/src/common/api-specs';
|
||||
import { getProductName } from 'insomnia/src/common/constants';
|
||||
import { getWorkspaceLabel } from 'insomnia/src/common/get-workspace-label';
|
||||
import { getInsomniaV5DataExport } from 'insomnia/src/common/insomnia-v5';
|
||||
@@ -31,6 +35,7 @@ import { useOrganizationPermissions } from '~/ui/hooks/use-organization-features
|
||||
import { usePlanData } from '~/ui/hooks/use-plan';
|
||||
|
||||
const VALUE_YAML = 'yaml';
|
||||
const VALUE_JSON = 'json';
|
||||
const VALUE_HAR = 'har';
|
||||
|
||||
export type SelectedFormat = typeof VALUE_HAR | typeof VALUE_YAML;
|
||||
@@ -90,7 +95,7 @@ const showSaveExportedFileDialog = async ({
|
||||
selectedFormat,
|
||||
}: {
|
||||
exportedFileNamePrefix: string;
|
||||
selectedFormat: SelectedFormat;
|
||||
selectedFormat: SelectedFormat | typeof VALUE_JSON;
|
||||
}) => {
|
||||
const date = format(Date.now(), 'yyyy-MM-dd-HH-mm-ss');
|
||||
const name = exportedFileNamePrefix.replace(/ /g, '-');
|
||||
@@ -359,6 +364,77 @@ export const exportRequestsToFile = (workspaceId: string, requestIds: string[])
|
||||
});
|
||||
};
|
||||
|
||||
export const exportSpecificationToFile = async (workspace: Workspace) => {
|
||||
const apiSpec = await services.apiSpec.getByParentId(workspace._id);
|
||||
if (!apiSpec?.contents) {
|
||||
showModal(AlertModal, {
|
||||
title: 'Cannot export',
|
||||
message: (
|
||||
<>
|
||||
This <strong>{getWorkspaceLabel(workspace).singular.toLowerCase()}</strong> does not contain an OpenAPI
|
||||
specification to export.
|
||||
</>
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
showModal(SelectModal, {
|
||||
title: 'Select Specification Format',
|
||||
value: VALUE_YAML,
|
||||
options: [
|
||||
{
|
||||
name: 'YAML',
|
||||
value: VALUE_YAML,
|
||||
},
|
||||
{
|
||||
name: 'JSON',
|
||||
value: VALUE_JSON,
|
||||
},
|
||||
],
|
||||
message: 'Which format would you like to export as?',
|
||||
onDone: async selectedFormat => {
|
||||
if (selectedFormat !== VALUE_YAML && selectedFormat !== VALUE_JSON) {
|
||||
return;
|
||||
}
|
||||
|
||||
let specification = apiSpec.contents;
|
||||
if (detectApiSpecSyntax(specification) !== selectedFormat) {
|
||||
try {
|
||||
specification = convertApiSpecSyntax(specification, selectedFormat);
|
||||
} catch {
|
||||
showError({
|
||||
title: 'Export Failed',
|
||||
message: `The specification is not valid, cannot convert to ${selectedFormat.toUpperCase()}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const fileName = await showSaveExportedFileDialog({
|
||||
exportedFileNamePrefix: workspace.name,
|
||||
selectedFormat,
|
||||
});
|
||||
if (!fileName) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await writeExportedFileToFileSystem(fileName, specification);
|
||||
window.main.trackAnalyticsEvent({
|
||||
event: AnalyticsEvent.dataExport,
|
||||
properties: { type: selectedFormat, scope: workspace.scope },
|
||||
});
|
||||
} catch {
|
||||
showError({
|
||||
title: 'Export Failed',
|
||||
message: 'Export failed due to an unexpected error',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const exportMcpClientToFile = async (workspace: Workspace) => {
|
||||
const fileName = await showSaveExportedFileDialog({
|
||||
exportedFileNamePrefix: workspace.name,
|
||||
|
||||
@@ -32,7 +32,7 @@ import * as reactUse from 'react-use';
|
||||
import { SwaggerUIBundle } from 'swagger-ui-dist';
|
||||
import YAML from 'yaml';
|
||||
|
||||
import { parseApiSpec } from '~/common/api-specs';
|
||||
import { convertApiSpecSyntax, detectApiSpecSyntax, parseApiSpec } from '~/common/api-specs';
|
||||
import { DEFAULT_SIDEBAR_SIZE } from '~/common/constants';
|
||||
import { debounce } from '~/common/misc';
|
||||
import { utf8ByteLength } from '~/common/utils/utf8-bytes';
|
||||
@@ -388,12 +388,7 @@ export const SpecView = ({
|
||||
if (!contents) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
JSON.parse(contents);
|
||||
return 'json';
|
||||
} catch {
|
||||
return 'yaml';
|
||||
}
|
||||
return detectApiSpecSyntax(contents);
|
||||
}, [apiSpec?.contents]);
|
||||
|
||||
const switchFormat = (to: 'json' | 'yaml') => {
|
||||
@@ -401,10 +396,9 @@ export const SpecView = ({
|
||||
if (!editorValue) {
|
||||
return;
|
||||
}
|
||||
let parsedSpec: string | undefined;
|
||||
let contents: string;
|
||||
try {
|
||||
// yaml parses json correctly
|
||||
parsedSpec = YAML.parse(editorValue);
|
||||
contents = convertApiSpecSyntax(editorValue, to);
|
||||
} catch {
|
||||
showToast({
|
||||
title: 'Failed to convert spec format',
|
||||
@@ -414,7 +408,6 @@ export const SpecView = ({
|
||||
});
|
||||
return;
|
||||
}
|
||||
const contents = to === 'json' ? JSON.stringify(parsedSpec, null, 2) : YAML.stringify(parsedSpec);
|
||||
editor.current?.setValue(contents);
|
||||
updateApiSpec({ organizationId, projectId, workspaceId, contents });
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user