mirror of
https://github.com/Kong/insomnia.git
synced 2026-10-27 17:19:42 -04:00
Improve import UX (#5869)
* add import routes and modal * fix paths * fix paths * save * remove import.ts * ui * save * fix tests * fix tests * fix tests * remove outdated test * update import tests * lint and ts fixes * add section title and fix some style issues * close modals on import success * error handling for empty content * fix api spec imports * Simplify importResources function Co-authored-by: Filipe Freire <filipe.r.freire@gmail.com> * cleanup * settings import button styles * remove comment * update tests * comment for app-command imports * fix test lint error * update comments * comment out app/import for now --------- Co-authored-by: Filipe Freire <filipe.r.freire@gmail.com>
This commit is contained in:
1 parent
d3b917a15f
commit
29acc92559
31 files changed
+3411
-3006
No files matched your search
@@ -8,8 +8,12 @@ test.describe('Cookie editor', async () => {
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
const text = await loadFixture('simple.yaml');
|
||||
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
|
||||
await page.getByRole('menuitem', { name: 'Clipboard' }).click();
|
||||
await page.click('text=Collectionsimplejust now');
|
||||
await page.getByRole('menuitem', { name: 'Import' }).click();
|
||||
await page.getByText('Clipboard').click();
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
await page.getByRole('link', { name: 'Debug' }).click();
|
||||
});
|
||||
|
||||
test('create and send a cookie', async ({ page }) => {
|
||||
|
||||
@@ -10,8 +10,12 @@ test.describe('Debug-Sidebar', async () => {
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
const text = await loadFixture('simple.yaml');
|
||||
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
|
||||
await page.getByRole('menuitem', { name: 'Clipboard' }).click();
|
||||
await page.click('text=Collectionsimplejust now');
|
||||
await page.getByRole('menuitem', { name: 'Import' }).click();
|
||||
await page.getByText('Clipboard').click();
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
await page.getByRole('link', { name: 'Debug' }).click();
|
||||
});
|
||||
|
||||
test.describe('Interact with sidebar', async () => {
|
||||
@@ -59,9 +63,9 @@ test.describe('Debug-Sidebar', async () => {
|
||||
});
|
||||
|
||||
test('Open properties of the collection', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'simple' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Collection Settings' }).click();
|
||||
await expect(page.locator('.app')).toContainText('Collection Settings wrk');
|
||||
await page.getByRole('button', { name: 'Untitled' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Document Settings' }).click();
|
||||
await page.getByText('Document Settings').click();
|
||||
});
|
||||
|
||||
test('Filter by request name', async ({ page }) => {
|
||||
|
||||
@@ -17,9 +17,10 @@ test.describe('Design interactions', async () => {
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
const text = await loadFixture('openapi3.yaml');
|
||||
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
|
||||
await page.getByRole('menuitem', { name: 'Clipboard' }).click();
|
||||
await page.click('div[role="dialog"] >> text=Design Document');
|
||||
await page.click('text=DocumentSmoke Test API server 1.0.0v1.0.0OpenAPI 3.0.0just now');
|
||||
await page.getByRole('menuitem', { name: 'Import' }).click();
|
||||
await page.getByText('Clipboard').click();
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
// Renders the spec code
|
||||
const codeEditor = page.locator('.pane-one');
|
||||
|
||||
+6
-2
@@ -8,8 +8,12 @@ test.describe('Environment Editor', async () => {
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
const text = await loadFixture('environments.yaml');
|
||||
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
|
||||
await page.getByRole('menuitem', { name: 'Clipboard' }).click();
|
||||
await page.click('text=Collectionenvironmentsjust now');
|
||||
await page.getByRole('menuitem', { name: 'Import' }).click();
|
||||
await page.getByText('Clipboard').click();
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
await page.getByRole('link', { name: 'Debug' }).click();
|
||||
});
|
||||
|
||||
test('create a new environment', async ({ page }) => {
|
||||
|
||||
@@ -17,8 +17,12 @@ test.describe('gRPC interactions', () => {
|
||||
const text = await loadFixture('grpc.yaml');
|
||||
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Clipboard' }).click();
|
||||
await page.click('text=CollectionPreRelease gRPCjust now');
|
||||
await page.getByRole('menuitem', { name: 'Import' }).click();
|
||||
await page.getByText('Clipboard').click();
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
await page.getByRole('link', { name: 'Debug' }).click();
|
||||
statusTag = page.locator('[data-testid="response-status-tag"]:visible');
|
||||
responseBody = page.locator('[data-testid="response-pane"] >> [data-testid="CodeEditor"]:visible', {
|
||||
has: page.locator('.CodeMirror-activeline'),
|
||||
|
||||
@@ -21,8 +21,12 @@ test('Check filter responses by environment preference', async ({ app, page }) =
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
const text = await loadFixture('simple.yaml');
|
||||
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
|
||||
await page.getByRole('menuitem', { name: 'Clipboard' }).click();
|
||||
await page.click('text=Collectionsimplejust now');
|
||||
await page.getByRole('menuitem', { name: 'Import' }).click();
|
||||
await page.getByText('Clipboard').click();
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
await page.getByRole('link', { name: 'Debug' }).click();
|
||||
|
||||
// Send a request
|
||||
await page.getByRole('button', { name: 'example http' }).click();
|
||||
|
||||
@@ -16,8 +16,12 @@ test('can send requests', async ({ app, page }) => {
|
||||
const text = await loadFixture('smoke-test-collection.yaml');
|
||||
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Clipboard' }).click();
|
||||
await page.click('text=CollectionSmoke testsjust now');
|
||||
await page.getByRole('menuitem', { name: 'Import' }).click();
|
||||
await page.getByText('Clipboard').click();
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
await page.getByRole('link', { name: 'Debug' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'send JSON request' }).click();
|
||||
await page.click('text=http://127.0.0.1:4010/pets/1Send >> button');
|
||||
@@ -68,8 +72,12 @@ test('can cancel requests', async ({ app, page }) => {
|
||||
const text = await loadFixture('smoke-test-collection.yaml');
|
||||
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Clipboard' }).click();
|
||||
await page.click('text=CollectionSmoke testsjust now');
|
||||
await page.getByRole('menuitem', { name: 'Import' }).click();
|
||||
await page.getByText('Clipboard').click();
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
await page.getByRole('link', { name: 'Debug' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'delayed request' }).click();
|
||||
await page.click('text=http://127.0.0.1:4010/delay/seconds/20Send >> button');
|
||||
|
||||
@@ -15,9 +15,12 @@ test('can render schema and send GraphQL requests', async ({ app, page }) => {
|
||||
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
|
||||
|
||||
// Import from clipboard
|
||||
await page.getByRole('menuitem', { name: 'Clipboard' }).click();
|
||||
// Open the new collection workspace
|
||||
await page.click('text=CollectionSmoke GraphQLjust now');
|
||||
await page.getByRole('menuitem', { name: 'Import' }).click();
|
||||
await page.getByText('Clipboard').click();
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
await page.getByRole('link', { name: 'Debug' }).click();
|
||||
// Open the graphql request
|
||||
await page.getByRole('button', { name: 'GraphQL request' }).click();
|
||||
// Assert the schema is fetched after switching to GraphQL request
|
||||
@@ -52,8 +55,12 @@ test('can send GraphQL requests after editing and prettifying query', async ({ a
|
||||
|
||||
const text = await loadFixture('graphql.yaml');
|
||||
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
|
||||
await page.getByRole('menuitem', { name: 'Clipboard' }).click();
|
||||
await page.click('text=CollectionSmoke GraphQLjust now');
|
||||
await page.getByRole('menuitem', { name: 'Import' }).click();
|
||||
await page.getByText('Clipboard').click();
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
await page.getByRole('link', { name: 'Debug' }).click();
|
||||
await page.getByRole('button', { name: 'GraphQL request' }).click();
|
||||
|
||||
// Edit and prettify query
|
||||
|
||||
@@ -16,8 +16,13 @@ test('can send gRPC requests with reflection', async ({ app, page }) => {
|
||||
const text = await loadFixture('grpc.yaml');
|
||||
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Clipboard' }).click();
|
||||
await page.click('text=CollectionPreRelease gRPCjust now');
|
||||
await page.getByRole('menuitem', { name: 'Import' }).click();
|
||||
await page.getByText('Clipboard').click();
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
await page.getByRole('link', { name: 'Debug' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Route Guide Example' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'UnaryWithOutProtoFile' }).click();
|
||||
|
||||
@@ -23,8 +23,12 @@ test('can make oauth2 requests', async ({ app, page }) => {
|
||||
const text = await loadFixture('oauth.yaml');
|
||||
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Clipboard' }).click();
|
||||
await page.locator('text=CollectionOauth Testingjust now').click();
|
||||
await page.getByRole('menuitem', { name: 'Import' }).click();
|
||||
await page.getByText('Clipboard').click();
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
await page.getByRole('link', { name: 'Debug' }).click();
|
||||
|
||||
// Authorization code
|
||||
await projectView.getByRole('button', { name: 'Authorization Code' }).click();
|
||||
|
||||
@@ -16,8 +16,12 @@ test('can make websocket connection', async ({ app, page }) => {
|
||||
const text = await loadFixture('websockets.yaml');
|
||||
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Clipboard' }).click();
|
||||
await page.click('text=CollectionWebSocketsjust now');
|
||||
await page.getByRole('menuitem', { name: 'Import' }).click();
|
||||
await page.getByText('Clipboard').click();
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
await page.getByRole('link', { name: 'Debug' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'localhost:4010' }).click();
|
||||
await expect(page.locator('.app')).toContainText('ws://localhost:4010');
|
||||
|
||||
Generated
+1736
-1841
File diff suppressed because it is too large.
Load diff
@@ -186,13 +186,13 @@
|
||||
"objectpath": "^2.0.0",
|
||||
"openapi-types": "^7.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-aria": "^3.19.0",
|
||||
"react-aria": "^3.23.1",
|
||||
"react-dnd": "^7.4.5",
|
||||
"react-dnd-html5-backend": "^7.4.4",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-router-dom": "^6.4.2",
|
||||
"react-stately": "^3.17.0",
|
||||
"react-stately": "^3.21.0",
|
||||
"react-use": "^17.4.0",
|
||||
"redux": "^4.1.2",
|
||||
"redux-mock-store": "^1.5.4",
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { describe, expect, it, jest } from '@jest/globals';
|
||||
|
||||
import * as modals from '../../ui/components/modals';
|
||||
import { askToImportIntoWorkspace, ForceToWorkspace } from '../import';
|
||||
|
||||
jest.mock('../../ui/components/modals');
|
||||
|
||||
describe('askToImportIntoWorkspace', () => {
|
||||
it('should return null if no active workspace', () => {
|
||||
const func = askToImportIntoWorkspace({ workspaceId: undefined, forceToWorkspace: ForceToWorkspace.new });
|
||||
expect(func()).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null if forcing to a new workspace', () => {
|
||||
const func = askToImportIntoWorkspace({ workspaceId: 'id', forceToWorkspace: ForceToWorkspace.new });
|
||||
expect(func()).toBeNull();
|
||||
});
|
||||
|
||||
it('should return id if forcing to a current workspace', () => {
|
||||
const currentWorkspaceId = 'currentId';
|
||||
const func = askToImportIntoWorkspace({ workspaceId: currentWorkspaceId, forceToWorkspace: ForceToWorkspace.current });
|
||||
expect(func()).toBe(currentWorkspaceId);
|
||||
});
|
||||
|
||||
it('should prompt the user if not forcing', () => {
|
||||
const currentWorkspaceId = 'current';
|
||||
const func = askToImportIntoWorkspace({ workspaceId: currentWorkspaceId });
|
||||
func();
|
||||
expect(modals.showModal).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { globalBeforeEach } from '../../__jest__/before-each';
|
||||
import { apiSpec, request, requestGroup, workspace } from '../../models';
|
||||
import { request, requestGroup, workspace } from '../../models';
|
||||
import { DEFAULT_PROJECT_ID } from '../../models/project';
|
||||
import * as importUtil from '../import';
|
||||
|
||||
@@ -37,18 +37,18 @@ describe('importRaw()', () => {
|
||||
|
||||
it('should import a curl request to a new workspace', async () => {
|
||||
const fixturePath = path.join(__dirname, '..', '__fixtures__', 'curl', 'complex-input.sh');
|
||||
const rawFixture = fs.readFileSync(fixturePath, 'utf8').toString();
|
||||
const content = fs.readFileSync(fixturePath, 'utf8').toString();
|
||||
|
||||
const importConfig: importUtil.ImportRawConfig = {
|
||||
getWorkspaceId: () => null,
|
||||
getProjectId: async () => DEFAULT_PROJECT_ID,
|
||||
getWorkspaceScope: () => 'design',
|
||||
};
|
||||
const scanResult = await importUtil.scanResources({
|
||||
content,
|
||||
});
|
||||
|
||||
const { source, error } = await importUtil.importRaw(
|
||||
rawFixture,
|
||||
importConfig
|
||||
);
|
||||
expect(scanResult.type.id).toBe('curl');
|
||||
expect(scanResult.errors.length).toBe(0);
|
||||
|
||||
await importUtil.importResources({
|
||||
projectId: DEFAULT_PROJECT_ID,
|
||||
});
|
||||
|
||||
const workspacesCount = await workspace.count();
|
||||
const projectWorkspaces = await workspace.findByParentId(
|
||||
@@ -57,9 +57,6 @@ describe('importRaw()', () => {
|
||||
|
||||
const curlRequests = await request.findByParentId(projectWorkspaces[0]._id);
|
||||
|
||||
expect(source).toBe('curl');
|
||||
expect(error).toBe(null);
|
||||
|
||||
expect(workspacesCount).toBe(1);
|
||||
|
||||
expect(curlRequests[0]).toMatchObject({
|
||||
@@ -71,30 +68,27 @@ describe('importRaw()', () => {
|
||||
|
||||
it('should import a curl request to an existing workspace', async () => {
|
||||
const fixturePath = path.join(__dirname, '..', '__fixtures__', 'curl', 'complex-input.sh');
|
||||
const rawFixture = fs.readFileSync(fixturePath, 'utf8').toString();
|
||||
const content = fs.readFileSync(fixturePath, 'utf8').toString();
|
||||
|
||||
const existingWorkspace = await workspace.create();
|
||||
|
||||
const importConfig: importUtil.ImportRawConfig = {
|
||||
getWorkspaceId: () => existingWorkspace._id,
|
||||
getProjectId: async () => DEFAULT_PROJECT_ID,
|
||||
getWorkspaceScope: () => 'design',
|
||||
};
|
||||
const scanResult = await importUtil.scanResources({
|
||||
content,
|
||||
});
|
||||
|
||||
const { source, error } = await importUtil.importRaw(
|
||||
rawFixture,
|
||||
importConfig
|
||||
);
|
||||
expect(scanResult.type?.id).toBe('curl');
|
||||
expect(scanResult.errors.length).toBe(0);
|
||||
|
||||
await importUtil.importResources({
|
||||
workspaceId: existingWorkspace._id,
|
||||
projectId: existingWorkspace.parentId,
|
||||
});
|
||||
|
||||
const workspacesCount = await workspace.count();
|
||||
expect(workspacesCount).toBe(1);
|
||||
|
||||
const curlRequests = await request.findByParentId(existingWorkspace._id);
|
||||
|
||||
expect(source).toBe('curl');
|
||||
expect(error).toBe(null);
|
||||
|
||||
expect(workspacesCount).toBe(1);
|
||||
|
||||
expect(curlRequests[0]).toMatchObject({
|
||||
body: {
|
||||
'text': '{\"email_id\": \"tem_123\"}',
|
||||
@@ -104,18 +98,18 @@ describe('importRaw()', () => {
|
||||
|
||||
it('should import a postman collection to a new workspace', async () => {
|
||||
const fixturePath = path.join(__dirname, '..', '__fixtures__', 'postman', 'aws-signature-auth-v2_0-input.json');
|
||||
const rawFixture = fs.readFileSync(fixturePath, 'utf8').toString();
|
||||
const content = fs.readFileSync(fixturePath, 'utf8').toString();
|
||||
|
||||
const importConfig: importUtil.ImportRawConfig = {
|
||||
getWorkspaceId: () => null,
|
||||
getProjectId: async () => DEFAULT_PROJECT_ID,
|
||||
getWorkspaceScope: () => 'design',
|
||||
};
|
||||
const scanResult = await importUtil.scanResources({
|
||||
content,
|
||||
});
|
||||
|
||||
const { source, error } = await importUtil.importRaw(
|
||||
rawFixture,
|
||||
importConfig
|
||||
);
|
||||
expect(scanResult.type.id).toBe('postman');
|
||||
expect(scanResult.errors.length).toBe(0);
|
||||
|
||||
await importUtil.importResources({
|
||||
projectId: DEFAULT_PROJECT_ID,
|
||||
});
|
||||
|
||||
const workspacesCount = await workspace.count();
|
||||
const projectWorkspaces = await workspace.findByParentId(
|
||||
@@ -125,9 +119,6 @@ describe('importRaw()', () => {
|
||||
const requestGroups = await requestGroup.findByParentId(projectWorkspaces[0]._id);
|
||||
const requests = await request.findByParentId(requestGroups[0]._id);
|
||||
|
||||
expect(source).toBe('postman');
|
||||
expect(error).toBe(null);
|
||||
|
||||
expect(workspacesCount).toBe(1);
|
||||
|
||||
expect(requests[0]).toMatchObject({
|
||||
@@ -137,29 +128,27 @@ describe('importRaw()', () => {
|
||||
|
||||
it('should import a postman collection to an existing workspace', async () => {
|
||||
const fixturePath = path.join(__dirname, '..', '__fixtures__', 'postman', 'aws-signature-auth-v2_0-input.json');
|
||||
const rawFixture = fs.readFileSync(fixturePath, 'utf8').toString();
|
||||
const content = fs.readFileSync(fixturePath, 'utf8').toString();
|
||||
|
||||
const existingWorkspace = await workspace.create();
|
||||
|
||||
const importConfig: importUtil.ImportRawConfig = {
|
||||
getWorkspaceId: () => existingWorkspace._id,
|
||||
getProjectId: async () => DEFAULT_PROJECT_ID,
|
||||
getWorkspaceScope: () => 'design',
|
||||
};
|
||||
const scanResult = await importUtil.scanResources({
|
||||
content,
|
||||
});
|
||||
|
||||
const { source, error } = await importUtil.importRaw(
|
||||
rawFixture,
|
||||
importConfig
|
||||
);
|
||||
expect(scanResult.type?.id).toBe('postman');
|
||||
expect(scanResult.errors.length).toBe(0);
|
||||
|
||||
await importUtil.importResources({
|
||||
workspaceId: existingWorkspace._id,
|
||||
projectId: existingWorkspace.parentId,
|
||||
});
|
||||
|
||||
const workspacesCount = await workspace.count();
|
||||
|
||||
const requestGroups = await requestGroup.findByParentId(existingWorkspace._id);
|
||||
const requests = await request.findByParentId(requestGroups[0]._id);
|
||||
|
||||
expect(source).toBe('postman');
|
||||
expect(error).toBe(null);
|
||||
|
||||
expect(workspacesCount).toBe(1);
|
||||
|
||||
expect(requests[0]).toMatchObject({
|
||||
@@ -169,23 +158,21 @@ describe('importRaw()', () => {
|
||||
|
||||
it('should import an openapi collection to an existing workspace with scope design', async () => {
|
||||
const fixturePath = path.join(__dirname, '..', '__fixtures__', 'openapi', 'endpoint-security-input.yaml');
|
||||
const rawFixture = fs.readFileSync(fixturePath, 'utf8').toString();
|
||||
const content = fs.readFileSync(fixturePath, 'utf8').toString();
|
||||
|
||||
const existingWorkspace = await workspace.create({ scope: 'design' });
|
||||
|
||||
const importConfig: importUtil.ImportRawConfig = {
|
||||
getWorkspaceId: () => existingWorkspace._id,
|
||||
getProjectId: async () => DEFAULT_PROJECT_ID,
|
||||
getWorkspaceScope: () => 'design',
|
||||
};
|
||||
const scanResult = await importUtil.scanResources({
|
||||
content,
|
||||
});
|
||||
|
||||
const { source, error } = await importUtil.importRaw(
|
||||
rawFixture,
|
||||
importConfig
|
||||
);
|
||||
expect(scanResult.type?.id).toBe('openapi3');
|
||||
expect(scanResult.errors.length).toBe(0);
|
||||
|
||||
expect(source).toBe('openapi3');
|
||||
expect(error).toBe(null);
|
||||
await importUtil.importResources({
|
||||
workspaceId: existingWorkspace._id,
|
||||
projectId: existingWorkspace.parentId,
|
||||
});
|
||||
|
||||
const workspacesCount = await workspace.count();
|
||||
|
||||
@@ -193,10 +180,6 @@ describe('importRaw()', () => {
|
||||
|
||||
const requests = await request.findByParentId(existingWorkspace._id);
|
||||
expect(requests.length).toBe(12);
|
||||
|
||||
const createdApiSpec = await apiSpec.getByParentId(existingWorkspace._id);
|
||||
|
||||
expect(createdApiSpec?.contents).toContain('openapi: \'3.0.2\'');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,54 +1,43 @@
|
||||
import fs from 'fs';
|
||||
import { readFile } from 'fs/promises';
|
||||
|
||||
import type { ApiSpec } from '../models/api-spec';
|
||||
import type { BaseModel } from '../models/index';
|
||||
import { ApiSpec, isApiSpec } from '../models/api-spec';
|
||||
import { CookieJar, isCookieJar } from '../models/cookie-jar';
|
||||
import { BaseEnvironment, isEnvironment } from '../models/environment';
|
||||
import { GrpcRequest, isGrpcRequest } from '../models/grpc-request';
|
||||
import { BaseModel, getModel } from '../models/index';
|
||||
import * as models from '../models/index';
|
||||
import { Project } from '../models/project';
|
||||
import { isRequest } from '../models/request';
|
||||
import { isWorkspace, Workspace, WorkspaceScope, WorkspaceScopeKeys } from '../models/workspace';
|
||||
import { SegmentEvent, trackSegmentEvent } from '../ui/analytics';
|
||||
import { AlertModal } from '../ui/components/modals/alert-modal';
|
||||
import { AskModal } from '../ui/components/modals/ask-modal';
|
||||
import { showError, showModal } from '../ui/components/modals/index';
|
||||
import { showSelectModal } from '../ui/components/modals/select-modal';
|
||||
import { convert, ConvertResultType } from '../utils/importers/convert';
|
||||
import { isRequest, Request } from '../models/request';
|
||||
import { isUnitTest, UnitTest } from '../models/unit-test';
|
||||
import { isUnitTestSuite, UnitTestSuite } from '../models/unit-test-suite';
|
||||
import {
|
||||
BASE_ENVIRONMENT_ID_KEY,
|
||||
CONTENT_TYPE_GRAPHQL,
|
||||
EXPORT_TYPE_WORKSPACE,
|
||||
WORKSPACE_ID_KEY,
|
||||
} from './constants';
|
||||
isWebSocketRequest,
|
||||
WebSocketRequest,
|
||||
} from '../models/websocket-request';
|
||||
import { isWorkspace, Workspace } from '../models/workspace';
|
||||
// import { SegmentEvent, trackSegmentEvent } from '../ui/analytics';
|
||||
import { convert, ConvertResultType } from '../utils/importers/convert';
|
||||
import { invariant } from '../utils/invariant';
|
||||
import { database as db } from './database';
|
||||
import { diffPatchObj, fnOrString, generateId } from './misc';
|
||||
import { strings } from './strings';
|
||||
import { generateId } from './misc';
|
||||
|
||||
export interface ImportResult {
|
||||
source: string;
|
||||
error: Error | null;
|
||||
summary: Record<string, BaseModel[]>;
|
||||
export interface ExportedModel extends BaseModel {
|
||||
_type: string;
|
||||
}
|
||||
|
||||
interface ConvertResult {
|
||||
type: ConvertResultType;
|
||||
data: {
|
||||
resources: Record<string, any>[];
|
||||
resources: ExportedModel[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ImportRawConfig {
|
||||
getWorkspaceId: ImportToWorkspacePrompt;
|
||||
getProjectId?: () => Promise<string>;
|
||||
getWorkspaceScope?: SetWorkspaceScopePrompt;
|
||||
enableDiffBasedPatching?: boolean;
|
||||
enableDiffDeep?: boolean;
|
||||
bypassDiffProps?: {
|
||||
url: boolean;
|
||||
};
|
||||
}
|
||||
export const isApiSpecImport = ({ id }: Pick<ConvertResultType, 'id'>) =>
|
||||
id === 'openapi3' || id === 'swagger2';
|
||||
|
||||
export async function importUri(uri: string, importConfig: ImportRawConfig) {
|
||||
let rawText;
|
||||
// If GH preview, force raw
|
||||
export const isInsomniaV4Import = ({ id }: Pick<ConvertResultType, 'id'>) =>
|
||||
id === 'insomnia-4';
|
||||
|
||||
export async function fetchImportContentFromURI({ uri }: { uri: string }) {
|
||||
const url = new URL(uri);
|
||||
|
||||
if (url.origin === 'https://github.com') {
|
||||
@@ -58,406 +47,242 @@ export async function importUri(uri: string, importConfig: ImportRawConfig) {
|
||||
}
|
||||
|
||||
if (uri.match(/^(http|https):\/\//)) {
|
||||
const response = await window.fetch(uri);
|
||||
rawText = await response.text();
|
||||
const response = await fetch(uri);
|
||||
const content = await response.text();
|
||||
|
||||
return content;
|
||||
} else if (uri.match(/^(file):\/\//)) {
|
||||
const path = uri.replace(/^(file):\/\//, '');
|
||||
rawText = fs.readFileSync(path, 'utf8');
|
||||
const content = await readFile(path, 'utf8');
|
||||
|
||||
return content;
|
||||
} else {
|
||||
// Treat everything else as raw text
|
||||
rawText = decodeURIComponent(uri);
|
||||
const content = decodeURIComponent(uri);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
const result = await importRaw(rawText, importConfig);
|
||||
const { summary, error } = result;
|
||||
|
||||
if (error) {
|
||||
showError({
|
||||
title: 'Failed to import',
|
||||
error,
|
||||
message: 'Import failed',
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
const statements = Object.keys(summary)
|
||||
.map(type => {
|
||||
const count = summary[type].length;
|
||||
const name = models.getModelName(type, count);
|
||||
return count === 0 ? null : `${count} ${name}`;
|
||||
})
|
||||
.filter(s => s !== null);
|
||||
let message;
|
||||
|
||||
if (statements.length === 0) {
|
||||
message = 'Nothing was found to import.';
|
||||
} else {
|
||||
message = `You imported ${statements.join(', ')}!`;
|
||||
}
|
||||
|
||||
showModal(AlertModal, {
|
||||
title: 'Import Succeeded',
|
||||
message,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// If we come across an ID of this form, we will replace it with a new one
|
||||
const REPLACE_ID_REGEX = /__\w+_\d+__/g;
|
||||
export interface ScanResult {
|
||||
requests?: (Request | WebSocketRequest | GrpcRequest)[];
|
||||
workspace?: Workspace;
|
||||
environments?: BaseEnvironment[];
|
||||
apiSpec?: ApiSpec;
|
||||
cookieJar?: CookieJar;
|
||||
unitTests?: UnitTest[];
|
||||
unitTestSuites?: UnitTestSuite[];
|
||||
type?: ConvertResultType;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export async function importRaw(
|
||||
rawContent: string,
|
||||
{
|
||||
getWorkspaceId,
|
||||
getWorkspaceScope,
|
||||
getProjectId,
|
||||
enableDiffBasedPatching,
|
||||
enableDiffDeep,
|
||||
bypassDiffProps,
|
||||
}: ImportRawConfig,
|
||||
) {
|
||||
let results: ConvertResult;
|
||||
let ResourceCache: {
|
||||
content: string;
|
||||
resources: BaseModel[];
|
||||
type: ConvertResultType;
|
||||
} | null = null;
|
||||
|
||||
export async function scanResources({
|
||||
content,
|
||||
}: {
|
||||
content: string;
|
||||
}): Promise<ScanResult> {
|
||||
let results: ConvertResult | null = null;
|
||||
|
||||
try {
|
||||
results = (await convert(rawContent)) as unknown as ConvertResult;
|
||||
} catch (err) {
|
||||
const importResult: ImportResult = {
|
||||
source: 'not found',
|
||||
error: err,
|
||||
summary: {},
|
||||
results = (await convert(content)) as unknown as ConvertResult;
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
return {
|
||||
errors: [err.message],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!results) {
|
||||
return {
|
||||
errors: ['No resources found to import.'],
|
||||
};
|
||||
return importResult;
|
||||
}
|
||||
|
||||
const { data, type: resultsType } = results;
|
||||
// Generate all the ids we may need
|
||||
const generatedIds: Record<string, string | ((...args: any[]) => any)> = {};
|
||||
const { type, data } = results;
|
||||
|
||||
for (const r of data.resources) {
|
||||
for (const key of r._id.match(REPLACE_ID_REGEX) || []) {
|
||||
generatedIds[key] = generateId(models.MODELS_BY_EXPORT_TYPE[r._type].prefix);
|
||||
}
|
||||
}
|
||||
|
||||
// Contains the ID of the workspace to be used with the import
|
||||
generatedIds[WORKSPACE_ID_KEY] = async () => {
|
||||
const workspaceId = await getWorkspaceId();
|
||||
// First try getting the workspace to overwrite
|
||||
const workspace = await models.workspace.getById(workspaceId || 'n/a');
|
||||
// Update this fn so it doesn't run again
|
||||
const idToUse = workspace?._id || generateId(models.workspace.prefix);
|
||||
generatedIds[WORKSPACE_ID_KEY] = idToUse;
|
||||
return idToUse;
|
||||
};
|
||||
|
||||
// Contains the ID of the base environment to be used with the import
|
||||
generatedIds[BASE_ENVIRONMENT_ID_KEY] = async () => {
|
||||
const parentId = await fnOrString(generatedIds[WORKSPACE_ID_KEY]);
|
||||
const baseEnvironment = await models.environment.getOrCreateForParentId(parentId);
|
||||
// Update this fn so it doesn't run again
|
||||
generatedIds[BASE_ENVIRONMENT_ID_KEY] = baseEnvironment._id;
|
||||
return baseEnvironment._id;
|
||||
};
|
||||
|
||||
// NOTE: Although the order of the imported resources is not guaranteed,
|
||||
// all current importers will produce resources in this order:
|
||||
// Workspace > Environment > RequestGroup > Request
|
||||
// Import everything backwards so they get inserted in the correct order
|
||||
data.resources.reverse();
|
||||
const importedDocs: Record<string, any[]> = {};
|
||||
|
||||
for (const model of models.all()) {
|
||||
importedDocs[model.type] = [];
|
||||
}
|
||||
|
||||
// Add a workspace to the resources if it doesn't exist
|
||||
// NOTE: The workspace should be the last item of the resources
|
||||
if (!data.resources.some(resource => resource._type === EXPORT_TYPE_WORKSPACE)) {
|
||||
data.resources.push({
|
||||
_id: WORKSPACE_ID_KEY,
|
||||
_type: EXPORT_TYPE_WORKSPACE,
|
||||
const resources = data.resources
|
||||
.filter(r => r._type)
|
||||
.map(r => {
|
||||
const { _type, ...model } = r;
|
||||
return { ...model, type: models.MODELS_BY_EXPORT_TYPE[_type].type };
|
||||
});
|
||||
}
|
||||
|
||||
for (const resource of data.resources) {
|
||||
// Buffer DB changes
|
||||
// NOTE: Doing it inside here so it's more "scalable"
|
||||
await db.bufferChanges(100);
|
||||
ResourceCache = {
|
||||
resources,
|
||||
type,
|
||||
content,
|
||||
};
|
||||
|
||||
// Replace null parentIds with current workspace
|
||||
if (!resource.parentId && resource._type !== EXPORT_TYPE_WORKSPACE) {
|
||||
resource.parentId = WORKSPACE_ID_KEY;
|
||||
}
|
||||
const requests = resources.filter(isRequest);
|
||||
const websocketRequests = resources.filter(isWebSocketRequest);
|
||||
const grpcRequests = resources.filter(isGrpcRequest);
|
||||
const environments = resources.filter(isEnvironment);
|
||||
const unitTests = resources.filter(isUnitTest);
|
||||
const unitTestSuites = resources.filter(isUnitTestSuite);
|
||||
const apiSpec = resources.find(isApiSpec);
|
||||
const workspace = resources.find(isWorkspace);
|
||||
const cookieJar = resources.find(isCookieJar);
|
||||
|
||||
// Replace ID placeholders (eg. __WORKSPACE_ID__) with generated values
|
||||
for (const key of Object.keys(generatedIds)) {
|
||||
const { parentId, _id } = resource;
|
||||
return {
|
||||
type,
|
||||
unitTests,
|
||||
unitTestSuites,
|
||||
requests: [...requests, ...websocketRequests, ...grpcRequests],
|
||||
workspace,
|
||||
environments,
|
||||
apiSpec,
|
||||
cookieJar,
|
||||
errors: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (parentId?.includes(key)) {
|
||||
resource.parentId = parentId.replace(key, await fnOrString(generatedIds[key]));
|
||||
}
|
||||
export async function importResources({
|
||||
workspaceId,
|
||||
projectId,
|
||||
workspaceName,
|
||||
}: {
|
||||
workspaceId?: string;
|
||||
workspaceName?: string;
|
||||
projectId: string;
|
||||
}) {
|
||||
invariant(ResourceCache, 'No resources to import');
|
||||
const bufferId = await db.bufferChanges();
|
||||
|
||||
if (_id && _id.includes(key)) {
|
||||
resource._id = _id.replace(key, await fnOrString(generatedIds[key]));
|
||||
const resources = ResourceCache.resources;
|
||||
const workspace = resources.find(isWorkspace);
|
||||
|
||||
const ResourceIdMap = new Map();
|
||||
// If we're importing into an existing workspace
|
||||
if (workspaceId && workspaceId !== 'create-new-workspace-id') {
|
||||
const existingWorkspace = await models.workspace.getById(workspaceId);
|
||||
|
||||
invariant(
|
||||
existingWorkspace,
|
||||
`Could not find workspace with id ${workspaceId}`
|
||||
);
|
||||
|
||||
// If we're importing into a new workspace
|
||||
// Map new IDs
|
||||
ResourceIdMap.set(workspaceId, existingWorkspace._id);
|
||||
ResourceIdMap.set('__WORKSPACE_ID__', existingWorkspace._id);
|
||||
workspace && ResourceIdMap.set(workspace._id, existingWorkspace._id);
|
||||
|
||||
const filteredResources = resources.filter(
|
||||
resource =>
|
||||
!isWorkspace(resource) &&
|
||||
!isApiSpec(resource) &&
|
||||
!isCookieJar(resource) &&
|
||||
!isEnvironment(resource)
|
||||
);
|
||||
|
||||
for (const resource of filteredResources) {
|
||||
const model = getModel(resource.type);
|
||||
if (model) {
|
||||
ResourceIdMap.set(resource._id, generateId(model.prefix));
|
||||
} else {
|
||||
console.log(
|
||||
'[Import Scan] Could not find model for type',
|
||||
resource.type
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const model = models.MODELS_BY_EXPORT_TYPE[resource._type];
|
||||
for (const resource of filteredResources) {
|
||||
const model = getModel(resource.type);
|
||||
|
||||
if (!model) {
|
||||
console.warn('Unknown doc type for import', resource._type);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Hack to switch to GraphQL based on finding `graphql` in the URL path
|
||||
// TODO: Support this in a better way
|
||||
if (
|
||||
isRequest(model) &&
|
||||
resource.body &&
|
||||
typeof resource.body.text === 'string' &&
|
||||
typeof resource.url === 'string' &&
|
||||
resource.body.text.includes('"query"') &&
|
||||
resource.url.includes('graphql')
|
||||
) {
|
||||
resource.body.mimeType = CONTENT_TYPE_GRAPHQL;
|
||||
}
|
||||
|
||||
// Try adding Content-Type JSON if no Content-Type exists
|
||||
if (
|
||||
isRequest(model) &&
|
||||
resource.body &&
|
||||
typeof resource.body.text === 'string' &&
|
||||
Array.isArray(resource.headers) &&
|
||||
!resource.headers.find(h => h.name.toLowerCase() === 'content-type')
|
||||
) {
|
||||
try {
|
||||
JSON.parse(resource.body.text);
|
||||
resource.headers.push({
|
||||
name: 'Content-Type',
|
||||
value: 'application/json',
|
||||
});
|
||||
} catch (err) {
|
||||
// Not JSON
|
||||
}
|
||||
}
|
||||
|
||||
const existingDoc = await db.get(model.type, resource._id);
|
||||
let newDoc: BaseModel;
|
||||
|
||||
if (existingDoc) {
|
||||
let updateDoc = resource;
|
||||
|
||||
// Do differential patching when enabled
|
||||
if (enableDiffBasedPatching) {
|
||||
updateDoc = diffPatchObj(resource, existingDoc, enableDiffDeep);
|
||||
}
|
||||
|
||||
// Bypass differential update for urls when enabled
|
||||
if (bypassDiffProps?.url && updateDoc.url) {
|
||||
updateDoc.url = resource.url;
|
||||
}
|
||||
|
||||
// If workspace preserve the scope and parentId of the existing workspace while importing
|
||||
if (isWorkspace(model)) {
|
||||
(updateDoc as Workspace).scope = (existingDoc as Workspace).scope;
|
||||
(updateDoc as Workspace).parentId = (existingDoc as Workspace).parentId;
|
||||
}
|
||||
|
||||
newDoc = await db.docUpdate(existingDoc, updateDoc);
|
||||
} else {
|
||||
// If workspace, check and set the scope and parentId while importing a new workspace
|
||||
if (isWorkspace(model)) {
|
||||
// Set the workspace scope if creating a new workspace during import
|
||||
// IF is creating a new workspace
|
||||
// AND imported resource has no preset scope property OR scope is null
|
||||
// AND we have a function to get scope
|
||||
if ((!resource.hasOwnProperty('scope') || resource.scope === null) && getWorkspaceScope) {
|
||||
const workspaceName = resource.name;
|
||||
let specName;
|
||||
|
||||
// If is from insomnia v4 and the spec has contents, add to the name when prompting
|
||||
if (isInsomniaV4Import(resultsType)) {
|
||||
const spec: ApiSpec | null = await models.apiSpec.getByParentId(resource._id);
|
||||
|
||||
if (spec && spec.contents.trim()) {
|
||||
specName = spec.fileName;
|
||||
}
|
||||
}
|
||||
|
||||
const nameToPrompt = specName ? `${specName} / ${workspaceName}` : workspaceName;
|
||||
resource.scope = await getWorkspaceScope(nameToPrompt);
|
||||
}
|
||||
// If the workspace doesn't have a name, update the default name based on it's scope
|
||||
if (!resource.name) {
|
||||
const name =
|
||||
(resource as Workspace).scope === 'collection'
|
||||
? `My ${strings.collection.singular}`
|
||||
: `My ${strings.document.singular}`;
|
||||
|
||||
resource.name = name;
|
||||
}
|
||||
|
||||
if (getProjectId) {
|
||||
// Set the workspace parent if creating a new workspace during import
|
||||
resource.parentId = await getProjectId();
|
||||
}
|
||||
}
|
||||
|
||||
newDoc = await db.docCreate(model.type, resource);
|
||||
|
||||
// Mark as not seen if we created a new workspace from sync
|
||||
if (isWorkspace(newDoc)) {
|
||||
const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(newDoc._id);
|
||||
await models.workspaceMeta.update(workspaceMeta, {
|
||||
hasSeen: false,
|
||||
if (model) {
|
||||
await db.docCreate(model.type, {
|
||||
...resource,
|
||||
_id: ResourceIdMap.get(resource._id),
|
||||
parentId: ResourceIdMap.get(resource.parentId),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
importedDocs[newDoc.type].push(newDoc);
|
||||
}
|
||||
await db.flushChanges(bufferId);
|
||||
|
||||
// Store spec under workspace if it's OpenAPI
|
||||
for (const workspace of importedDocs[models.workspace.type]) {
|
||||
if (isApiSpecImport(resultsType)) {
|
||||
const spec = await models.apiSpec.updateOrCreateForParentId(workspace._id, {
|
||||
contents: rawContent,
|
||||
return {
|
||||
resources,
|
||||
workspace: existingWorkspace,
|
||||
};
|
||||
} else {
|
||||
const scope =
|
||||
isApiSpecImport(ResourceCache?.type) ||
|
||||
Boolean(resources.find(r => r.type === 'ApiSpec'))
|
||||
? 'design'
|
||||
: 'collection';
|
||||
const newWorkspace = await models.workspace.create({
|
||||
name: workspaceName || workspace?.name,
|
||||
scope,
|
||||
parentId: projectId,
|
||||
});
|
||||
|
||||
const apiSpec = resources.find(isApiSpec);
|
||||
|
||||
if (apiSpec) {
|
||||
await models.apiSpec.updateOrCreateForParentId(newWorkspace._id, {
|
||||
...apiSpec,
|
||||
fileName: workspaceName || workspace?.name,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
isApiSpecImport(ResourceCache?.type) &&
|
||||
newWorkspace.scope === 'design'
|
||||
) {
|
||||
await models.apiSpec.updateOrCreateForParentId(newWorkspace._id, {
|
||||
fileName: workspaceName || workspace?.name,
|
||||
contents: ResourceCache.content,
|
||||
contentType: 'yaml',
|
||||
});
|
||||
importedDocs[spec.type].push(spec);
|
||||
}
|
||||
|
||||
// Set active environment when none is currently selected and one exists
|
||||
const meta = await models.workspaceMeta.getOrCreateByParentId(workspace._id);
|
||||
const envs = importedDocs[models.environment.type];
|
||||
// If we're importing into a new workspace
|
||||
// Map new IDs
|
||||
ResourceIdMap.set(workspaceId, newWorkspace._id);
|
||||
ResourceIdMap.set('__WORKSPACE_ID__', newWorkspace._id);
|
||||
workspace && ResourceIdMap.set(workspace._id, newWorkspace._id);
|
||||
|
||||
if (!meta.activeEnvironmentId && envs.length > 0) {
|
||||
meta.activeEnvironmentId = envs[0]._id;
|
||||
await models.workspaceMeta.update(meta);
|
||||
const resourcesWithoutWorkspaceAndApiSpec = resources.filter(
|
||||
resource => !isWorkspace(resource) && !isApiSpec(resource)
|
||||
);
|
||||
|
||||
for (const resource of resourcesWithoutWorkspaceAndApiSpec) {
|
||||
const model = getModel(resource.type);
|
||||
if (model) {
|
||||
ResourceIdMap.set(resource._id, generateId(model.prefix));
|
||||
} else {
|
||||
console.log(
|
||||
'[Import Scan] Could not find model for type',
|
||||
resource.type
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const resource of resourcesWithoutWorkspaceAndApiSpec) {
|
||||
const model = getModel(resource.type);
|
||||
|
||||
if (model) {
|
||||
await db.docCreate(model.type, {
|
||||
...resource,
|
||||
_id: ResourceIdMap.get(resource._id),
|
||||
parentId: ResourceIdMap.get(resource.parentId),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await db.flushChanges(bufferId);
|
||||
|
||||
return {
|
||||
resources,
|
||||
workspace: newWorkspace,
|
||||
};
|
||||
}
|
||||
|
||||
await db.flushChanges();
|
||||
trackSegmentEvent(SegmentEvent.dataImport, { type: resultsType.id });
|
||||
const importRequest: ImportResult = {
|
||||
source: resultsType && typeof resultsType.id === 'string' ? resultsType.id : 'unknown',
|
||||
summary: importedDocs,
|
||||
error: null,
|
||||
};
|
||||
return importRequest;
|
||||
}
|
||||
|
||||
export const isApiSpecImport = ({ id }: Pick<ConvertResultType, 'id'>) => (
|
||||
id === 'openapi3' || id === 'swagger2'
|
||||
);
|
||||
|
||||
export const isInsomniaV4Import = ({ id }: Pick<ConvertResultType, 'id'>) => (
|
||||
id === 'insomnia-4'
|
||||
);
|
||||
|
||||
export enum ForceToWorkspace {
|
||||
new = 'new',
|
||||
current = 'current',
|
||||
existing = 'existing'
|
||||
}
|
||||
|
||||
export type SelectExistingWorkspacePrompt = Promise<string | null>;
|
||||
|
||||
// Returning null instead of a string will create a new workspace
|
||||
export type ImportToWorkspacePrompt = () => null | string | Promise<null | string>;
|
||||
export function askToImportIntoWorkspace({ workspaceId, forceToWorkspace, activeProjectWorkspaces }: { workspaceId?: string; forceToWorkspace?: ForceToWorkspace; activeProjectWorkspaces?: Workspace[] }): ImportToWorkspacePrompt {
|
||||
return function() {
|
||||
switch (forceToWorkspace) {
|
||||
case ForceToWorkspace.new: {
|
||||
return null;
|
||||
}
|
||||
|
||||
case ForceToWorkspace.current: {
|
||||
if (!workspaceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return workspaceId;
|
||||
}
|
||||
|
||||
case ForceToWorkspace.existing: {
|
||||
// Return null if there are no available workspaces to chose from.
|
||||
if (activeProjectWorkspaces?.length) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
default: {
|
||||
if (!workspaceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
showModal(AskModal, {
|
||||
title: 'Import',
|
||||
message: 'Do you want to import into the current workspace or a new one?',
|
||||
yesText: 'Current',
|
||||
noText: 'New Workspace',
|
||||
onDone: async (yes: boolean) => {
|
||||
resolve(yes ? workspaceId : null);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type SetWorkspaceScopePrompt = (name?: string) => WorkspaceScope | Promise<WorkspaceScope>;
|
||||
export function askToSetWorkspaceScope(scope?: WorkspaceScope): SetWorkspaceScopePrompt {
|
||||
return name => {
|
||||
switch (scope) {
|
||||
case WorkspaceScopeKeys.collection:
|
||||
case WorkspaceScopeKeys.design:
|
||||
return scope;
|
||||
|
||||
default:
|
||||
return new Promise(resolve => {
|
||||
const message = name
|
||||
? `How would you like to import "${name}"?`
|
||||
: 'Do you want to import as a Request Collection or a Design Document?';
|
||||
|
||||
showModal(AskModal, {
|
||||
title: 'Import As',
|
||||
message,
|
||||
noText: 'Request Collection',
|
||||
yesText: 'Design Document',
|
||||
onDone: async (yes: boolean) => {
|
||||
resolve(yes ? WorkspaceScopeKeys.design : WorkspaceScopeKeys.collection);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type SetProjectIdPrompt = () => Promise<string>;
|
||||
export function askToImportIntoProject({ projects, activeProject }: { projects?: Project[]; activeProject?: Project }): SetProjectIdPrompt {
|
||||
return function() {
|
||||
return new Promise(resolve => {
|
||||
// If only one project exists, return that
|
||||
if (projects?.length === 1) {
|
||||
return resolve(projects[0]._id);
|
||||
}
|
||||
|
||||
const options = projects?.map(project => ({ name: project.name, value: project._id })) || [];
|
||||
const defaultValue = activeProject?._id || null;
|
||||
|
||||
showSelectModal({
|
||||
title: 'Import',
|
||||
message: `Select a ${strings.project.singular.toLowerCase()} to import into`,
|
||||
options,
|
||||
value: defaultValue,
|
||||
onDone: selectedProjectId => {
|
||||
// @ts-expect-error onDone can send null as an argument; why/how?
|
||||
resolve(selectedProjectId);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -45,12 +45,9 @@ describe('app.import.*', () => {
|
||||
const filename = path.resolve(__dirname, '../__fixtures__/basic-import.json');
|
||||
await data.import.uri(`file://${filename}`);
|
||||
const allWorkspaces = await db.all(models.workspace.type);
|
||||
expect(allWorkspaces).toEqual([
|
||||
expect(allWorkspaces).toMatchObject([
|
||||
workspace,
|
||||
{
|
||||
_id: 'wrk_imported_1',
|
||||
created: 888,
|
||||
modified: 999,
|
||||
description: '',
|
||||
name: 'New',
|
||||
parentId: project._id,
|
||||
@@ -58,21 +55,23 @@ describe('app.import.*', () => {
|
||||
scope: WorkspaceScopeKeys.collection,
|
||||
},
|
||||
]);
|
||||
expect(await db.all(models.request.type)).toEqual([
|
||||
|
||||
const importedWorkspace = allWorkspaces.find(w => w.name === 'New');
|
||||
|
||||
expect(importedWorkspace).toBeDefined();
|
||||
|
||||
expect(await db.all(models.request.type)).toMatchObject([
|
||||
{
|
||||
_id: 'req_imported_1',
|
||||
isPrivate: false,
|
||||
authentication: {},
|
||||
body: {},
|
||||
created: 111,
|
||||
description: '',
|
||||
headers: [],
|
||||
metaSortKey: 0,
|
||||
method: 'GET',
|
||||
modified: 222,
|
||||
name: 'Test',
|
||||
parameters: [],
|
||||
parentId: 'wrk_imported_1',
|
||||
parentId: importedWorkspace._id,
|
||||
settingDisableRenderRequestBody: false,
|
||||
settingEncodeUrl: true,
|
||||
settingSendCookies: true,
|
||||
@@ -93,12 +92,9 @@ describe('app.import.*', () => {
|
||||
const filename = path.resolve(__dirname, '../__fixtures__/basic-import.json');
|
||||
await data.import.raw(fs.readFileSync(filename, 'utf8'));
|
||||
const allWorkspaces = await db.all(models.workspace.type);
|
||||
expect(allWorkspaces).toEqual([
|
||||
expect(allWorkspaces).toMatchObject([
|
||||
workspace,
|
||||
{
|
||||
_id: 'wrk_imported_1',
|
||||
created: 888,
|
||||
modified: 999,
|
||||
description: '',
|
||||
name: 'New',
|
||||
parentId: project._id,
|
||||
@@ -106,21 +102,21 @@ describe('app.import.*', () => {
|
||||
scope: WorkspaceScopeKeys.collection,
|
||||
},
|
||||
]);
|
||||
expect(await db.all(models.request.type)).toEqual([
|
||||
|
||||
const importedWorkspace = allWorkspaces.find(w => w.name === 'New');
|
||||
expect(importedWorkspace).toBeDefined();
|
||||
expect(await db.all(models.request.type)).toMatchObject([
|
||||
{
|
||||
_id: 'req_imported_1',
|
||||
isPrivate: false,
|
||||
authentication: {},
|
||||
body: {},
|
||||
created: 111,
|
||||
description: '',
|
||||
headers: [],
|
||||
metaSortKey: 0,
|
||||
method: 'GET',
|
||||
modified: 222,
|
||||
name: 'Test',
|
||||
parameters: [],
|
||||
parentId: 'wrk_imported_1',
|
||||
parentId: importedWorkspace._id,
|
||||
settingDisableRenderRequestBody: false,
|
||||
settingEncodeUrl: true,
|
||||
settingSendCookies: true,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { exportWorkspacesData, exportWorkspacesHAR } from '../../common/export';
|
||||
import type { ImportRawConfig } from '../../common/import';
|
||||
import { importRaw, importUri } from '../../common/import';
|
||||
import { fetchImportContentFromURI, importResources, scanResources } from '../../common/import';
|
||||
import * as models from '../../models';
|
||||
import { DEFAULT_PROJECT_ID } from '../../models/project';
|
||||
import type { Workspace, WorkspaceScope } from '../../models/workspace';
|
||||
|
||||
interface PluginImportOptions {
|
||||
@@ -18,14 +16,6 @@ interface InsomniaExport {
|
||||
|
||||
type HarExport = Omit<InsomniaExport, 'format'>;
|
||||
|
||||
const buildImportRawConfig = (options: PluginImportOptions, activeProjectId: string): ImportRawConfig => ({
|
||||
getWorkspaceId: () => Promise.resolve(options.workspaceId || null),
|
||||
getWorkspaceScope: options.scope && (() => (
|
||||
Promise.resolve<WorkspaceScope>(options.scope as WorkspaceScope))
|
||||
),
|
||||
getProjectId: () => Promise.resolve(activeProjectId),
|
||||
});
|
||||
|
||||
const getWorkspaces = (activeProjectId?: string) => {
|
||||
if (activeProjectId) {
|
||||
return models.workspace.findByParentId(activeProjectId);
|
||||
@@ -42,10 +32,35 @@ export const init = (activeProjectId?: string) => ({
|
||||
data: {
|
||||
import: {
|
||||
uri: async (uri: string, options: PluginImportOptions = {}) => {
|
||||
await importUri(uri, buildImportRawConfig(options, activeProjectId || DEFAULT_PROJECT_ID));
|
||||
if (!activeProjectId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await fetchImportContentFromURI({
|
||||
uri,
|
||||
});
|
||||
|
||||
await scanResources({
|
||||
content,
|
||||
});
|
||||
|
||||
await importResources({
|
||||
projectId: activeProjectId,
|
||||
workspaceId: options.workspaceId,
|
||||
});
|
||||
},
|
||||
raw: async (text: string, options: PluginImportOptions = {}) => {
|
||||
await importRaw(text, buildImportRawConfig(options, activeProjectId || DEFAULT_PROJECT_ID));
|
||||
raw: async (content: string, options: PluginImportOptions = {}) => {
|
||||
if (!activeProjectId) {
|
||||
return;
|
||||
}
|
||||
await scanResources({
|
||||
content,
|
||||
});
|
||||
|
||||
await importResources({
|
||||
projectId: activeProjectId,
|
||||
workspaceId: options.workspaceId,
|
||||
});
|
||||
},
|
||||
},
|
||||
export: {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { AriaPopoverProps, DismissButton, Overlay, usePopover } from '@react-aria/overlays';
|
||||
import { StyleProps } from '@react-types/shared';
|
||||
import React, { FC, ReactNode, useRef } from 'react';
|
||||
import { useOverlayTrigger } from 'react-aria';
|
||||
import { AriaPopoverProps, DismissButton, Overlay, useOverlayTrigger, usePopover } from 'react-aria';
|
||||
import { OverlayTriggerState } from 'react-stately';
|
||||
|
||||
interface Props extends Omit<AriaPopoverProps, 'popoverRef' | 'maxHeight'>, StyleProps {
|
||||
interface Props extends Omit<AriaPopoverProps, 'popoverRef' | 'maxHeight'> {
|
||||
children: ReactNode;
|
||||
state: OverlayTriggerState;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,936 @@
|
||||
import React, {
|
||||
FC,
|
||||
Fragment,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useId,
|
||||
useRef,
|
||||
useState,
|
||||
useTransition,
|
||||
} from 'react';
|
||||
import { OverlayContainer, useDrop } from 'react-aria';
|
||||
import { useFetcher } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { strings } from '../../../common/strings';
|
||||
import { isDefaultProject, isLocalProject } from '../../../models/project';
|
||||
import { Workspace } from '../../../models/workspace';
|
||||
import {
|
||||
ImportResourcesActionResult,
|
||||
ScanForResourcesActionResult,
|
||||
} from '../../routes/import';
|
||||
import { ProjectLoaderData } from '../../routes/project';
|
||||
import { Modal, ModalHandle, ModalProps } from '../base/modal';
|
||||
import { ModalHeader } from '../base/modal-header';
|
||||
import { Button } from '../themed-button';
|
||||
import { hideAllModals } from '.';
|
||||
|
||||
const Pill = styled.div({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--padding-xs)',
|
||||
padding: 'var(--padding-sm)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
fontSize: 'var(--font-size-xs)',
|
||||
});
|
||||
|
||||
const RadioGroup = styled.div({
|
||||
display: 'flex',
|
||||
padding: 'var(--padding-xs)',
|
||||
border: '1px solid var(--hl-md)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
backgroundColor: 'var(--hl-xs)',
|
||||
'input[type="radio"]:checked ~ label': {
|
||||
backgroundColor: 'var(--color-bg)',
|
||||
boxShadow: '0 0 5px 1px var(--hl-xs)',
|
||||
},
|
||||
});
|
||||
|
||||
const RadioLabel = styled.label({
|
||||
padding: 'var(--padding-sm)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--padding-sm)',
|
||||
});
|
||||
|
||||
const RadioInput = styled.input({
|
||||
position: 'absolute',
|
||||
width: '1px',
|
||||
height: '1px',
|
||||
padding: '0',
|
||||
margin: '-1px',
|
||||
overflow: 'hidden',
|
||||
clip: 'rect(0,0,0,0)',
|
||||
whiteSpace: 'nowrap',
|
||||
borderWidth: '0',
|
||||
});
|
||||
|
||||
const Radio: FC<{
|
||||
name: string;
|
||||
value: string;
|
||||
children: ReactNode;
|
||||
checked?: boolean;
|
||||
defaultChecked?: boolean;
|
||||
onChange?: React.ChangeEventHandler<HTMLInputElement>;
|
||||
}> = ({ name, value, onChange, children, checked, defaultChecked }) => {
|
||||
const id = useId();
|
||||
return (
|
||||
<div>
|
||||
<RadioInput
|
||||
id={id}
|
||||
type="radio"
|
||||
name={name}
|
||||
checked={checked}
|
||||
value={value}
|
||||
defaultChecked={defaultChecked}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<RadioLabel htmlFor={id}>{children}</RadioLabel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Fieldset = styled.fieldset({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 'var(--padding-md)',
|
||||
legend: {
|
||||
marginBottom: 'var(--padding-xs)',
|
||||
},
|
||||
});
|
||||
|
||||
const FileInput = styled.input({
|
||||
display: 'none',
|
||||
});
|
||||
|
||||
const FileInputLabel = styled.label({
|
||||
padding: 'var(--padding-sm)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--padding-sm)',
|
||||
border: '1px solid var(--hl-md)',
|
||||
backgroundColor: 'var(--hl-xs)',
|
||||
flexWrap: 'wrap',
|
||||
});
|
||||
|
||||
const FileView = styled.div({
|
||||
backgroundColor: 'var(--color-bg)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 'var(--padding-md)',
|
||||
gap: 'var(--padding-sm)',
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
const FileField: FC = () => {
|
||||
const id = useId();
|
||||
const dropRef = useRef<HTMLLabelElement>(null);
|
||||
const [selectedFile, setSelectedFile] = useState<File | undefined>();
|
||||
const { isDropTarget, dropProps } = useDrop({
|
||||
ref: dropRef,
|
||||
onDrop: async event => {
|
||||
const file =
|
||||
event.items[0].kind === 'file'
|
||||
? await event.items[0].getFile()
|
||||
: undefined;
|
||||
|
||||
if (file) {
|
||||
setSelectedFile(file);
|
||||
}
|
||||
},
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<FileInput
|
||||
onChange={e => setSelectedFile(e.currentTarget.files?.[0])}
|
||||
accept={[
|
||||
'',
|
||||
'sh',
|
||||
'txt',
|
||||
'json',
|
||||
'har',
|
||||
'curl',
|
||||
'bash',
|
||||
'shell',
|
||||
'yaml',
|
||||
'yml',
|
||||
'wsdl',
|
||||
].join(',')}
|
||||
id={id}
|
||||
type="file"
|
||||
/>
|
||||
<FileInputLabel
|
||||
{...dropProps}
|
||||
style={{
|
||||
border: isDropTarget ? '1px solid var(--color-surprise)' : undefined,
|
||||
}}
|
||||
htmlFor={id}
|
||||
>
|
||||
{!selectedFile && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 'var(--padding-md)',
|
||||
gap: 'var(--padding-sm)',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<i className="fa fa-upload fa-xl" />
|
||||
</div>
|
||||
<div>
|
||||
Drag and Drop or{' '}
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--color-surprise)',
|
||||
}}
|
||||
>
|
||||
Choose a File
|
||||
</span>{' '}
|
||||
to import
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{selectedFile && (
|
||||
<Fragment key={selectedFile.name}>
|
||||
<FileView key={selectedFile.path}>
|
||||
<div>
|
||||
<i className="fa fa-file fa-xl" />
|
||||
</div>
|
||||
{selectedFile.name}
|
||||
</FileView>
|
||||
<input type="hidden" name="filePath" value={selectedFile.path} />
|
||||
</Fragment>
|
||||
)}
|
||||
</FileInputLabel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const InsomniaIcon = (props: React.SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={18}
|
||||
height={18}
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M16 31.186c8.387 0 15.186-6.799 15.186-15.186S24.387.814 16 .814.814 7.613.814 16 7.613 31.186 16 31.186z"
|
||||
fill="var(--color-bg)"
|
||||
/>
|
||||
<path
|
||||
d="M16 0C7.163 0 0 7.163 0 16s7.163 16 16 16 16-7.163 16-16S24.837 0 16 0zm0 1.627c7.938 0 14.373 6.435 14.373 14.373S23.938 30.373 16 30.373 1.627 23.938 1.627 16 8.062 1.627 16 1.627z"
|
||||
fill="var(--color-font)"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M16.18 4.61c6.291 0 11.39 5.1 11.39 11.39 0 6.29-5.099 11.39-11.39 11.39-6.29 0-11.39-5.1-11.39-11.39 0-1.537.305-3.004.858-4.342a4.43 4.43 0 106.192-6.192 11.357 11.357 0 014.34-.856z"
|
||||
fill="var(--color-font)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear"
|
||||
x1={16.1807}
|
||||
y1={27.3898}
|
||||
x2={16.1807}
|
||||
y2={4.61017}
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#7400E1" />
|
||||
<stop offset={1} stopColor="#4000BF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const PostmanIcon = (props: React.SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
width={18}
|
||||
height={18}
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M18.038.13a16 16 0 10-4.076 31.74A16 16 0 0018.038.13z"
|
||||
fill="var(--color-font)"
|
||||
/>
|
||||
<path
|
||||
d="M11.567 17.011a.06.06 0 00.07.032l2.56-.552-1.076-1.091-1.534 1.534a.06.06 0 00-.02.077zM23.555 6.02a2.386 2.386 0 101.005 4.55l-1.623-1.623a.2.2 0 010-.283l2.12-2.118a2.386 2.386 0 00-1.502-.527z"
|
||||
fill="var(--color-bg)"
|
||||
/>
|
||||
<path
|
||||
d="M25.348 6.82L23.361 8.8l1.558 1.558a2.4 2.4 0 00.429-3.538zM21.372 10.474h-.035a.621.621 0 00-.123.01h-.015a.938.938 0 00-.13.04l-.034.015a.627.627 0 00-.093.048l-.035.023a.833.833 0 00-.11.09l-5.892 5.894.73.73 6.24-5.478a.727.727 0 00.096-.102l.027-.035a.87.87 0 00.11-.234c0-.019.011-.038.016-.057a.934.934 0 00.016-.12v-.053-.086c0-.03 0-.039-.008-.058a.778.778 0 00-.61-.613h-.03a.835.835 0 00-.12-.014zM13.396 15.117l1.21 1.203 5.909-5.909c.192-.188.442-.305.71-.331-1.045-.8-2.184-.59-7.829 5.037zM22.207 12.077l-.072.07-6.24 5.475 1.061 1.06c2.63-2.488 4.965-4.858 5.252-6.605zM6.643 24.904a.058.058 0 00.051.041l2.72.188-1.525-1.525-1.233 1.232a.061.061 0 00-.013.064zM8.174 23.325l1.608 1.608a.122.122 0 00.152.02.12.12 0 00.062-.139l-.27-1.155a.346.346 0 01.177-.386c2.82-1.412 5.093-2.867 6.762-4.32l-1.12-1.12-2.4.517-4.971 4.975zM15.201 17.494l-.601-.601-.832.83a.04.04 0 000 .051.038.038 0 00.046.021l1.387-.3z"
|
||||
fill="var(--color-bg)"
|
||||
/>
|
||||
<path
|
||||
d="M25.404 8.11a.185.185 0 10-.33.16.557.557 0 01-.07.602.185.185 0 00.285.237.926.926 0 00.115-.998z"
|
||||
fill="var(--color-font)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const SwaggerIcon = (props: React.SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18px"
|
||||
height="18px"
|
||||
viewBox="0 0 256 256"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="var(--color-font)"
|
||||
d="M127.999 249.895c-67.215 0-121.9-54.68-121.9-121.896C6.1 60.782 60.785 6.102 128 6.102c67.214 0 121.899 54.685 121.899 121.9 0 67.214-54.685 121.893-121.9 121.893z"
|
||||
/>
|
||||
<path
|
||||
fill="var(--color-bg)"
|
||||
d="M127.999 12.202c63.954 0 115.797 51.842 115.797 115.797 0 63.952-51.843 115.797-115.797 115.797-63.952 0-115.797-51.845-115.797-115.797S64.047 12.202 127.999 12.202m0-12.202C57.419 0 0 57.42 0 127.999s57.42 127.998 127.999 127.998S256 198.577 256 128C256 57.419 198.578 0 127.999 0z"
|
||||
/>
|
||||
<path
|
||||
fill="var(--color-bg)"
|
||||
d="M80.598 86.619c-.394 4.38.146 8.909-.146 13.338-.345 4.431-.887 8.811-1.773 13.192-1.23 6.25-5.12 10.976-10.482 14.914 10.436 6.793 11.616 17.324 12.304 28.006.345 5.76.197 11.567.788 17.276.443 4.429 2.165 5.562 6.745 5.708 1.87.048 3.786 0 5.956 0v13.683c-13.535 2.313-24.708-1.525-27.467-12.992-.887-4.184-1.478-8.467-1.673-12.798-.297-4.578.195-9.155-.148-13.732-.985-12.553-2.61-16.785-14.618-17.376v-15.602a23.714 23.714 0 012.608-.443c6.596-.345 9.4-2.364 10.828-8.86.69-3.641 1.084-7.333 1.23-11.074.494-7.136.297-14.42 1.525-21.507C67.997 68.163 74.3 63.24 84.785 62.65c2.952-.149 5.955 0 9.35 0v13.98c-1.427.1-2.658.294-3.937.294-8.515-.297-8.96 2.607-9.6 9.695zm16.39 32.386h-.196c-4.923-.245-9.155 3.593-9.403 8.515-.246 4.972 3.592 9.206 8.515 9.45h.59c4.875.296 9.056-3.447 9.352-8.319v-.491c.1-4.971-3.886-9.055-8.857-9.155zm30.862 0c-4.774-.148-8.763 3.593-8.909 8.318 0 .297 0 .543.051.837 0 5.365 3.641 8.812 9.155 8.812 5.414 0 8.812-3.544 8.812-9.106-.051-5.366-3.646-8.91-9.109-8.86zm31.602 0c-5.02-.1-9.206 3.89-9.352 8.91a9.03 9.03 0 009.055 9.054h.1c4.528.788 9.106-3.592 9.402-8.858.243-4.874-4.186-9.106-9.205-9.106zm43.363.737c-5.711-.245-8.567-2.164-9.992-7.581a54.874 54.874 0 01-1.624-10.582c-.395-6.596-.346-13.241-.789-19.837-1.033-15.651-12.352-21.114-28.794-18.41V76.92c2.607 0 4.626 0 6.645.049 3.495.048 6.153 1.379 6.496 5.268.345 3.543.345 7.136.69 10.73.692 7.139 1.083 14.372 2.314 21.41 1.085 5.809 5.07 10.14 10.04 13.684-8.71 5.857-11.27 14.223-11.714 23.626-.245 6.448-.394 12.944-.736 19.443-.297 5.905-2.362 7.824-8.318 7.972-1.674.05-3.298.198-5.169.297v13.93c3.495 0 6.694.196 9.892 0 9.942-.592 15.947-5.415 17.918-15.063a125.582 125.582 0 001.476-16.045c.343-4.923.297-9.894.788-14.766.737-7.63 4.232-10.78 11.862-11.27.739-.1 1.427-.246 2.118-.492v-15.604c-1.282-.149-2.17-.295-3.103-.346z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const OpenAPIIcon = (props: React.SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={17}
|
||||
height={17}
|
||||
viewBox="0 0 512 512"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M.204 294.376l135.073-.002a91.735 91.735 0 008.452 30.17L27.94 394.298C12.75 368.103 3.166 334.783.204 294.376zm131.802 196.803l51.407-124.841a91.475 91.475 0 01-14.938-9.955l-95.47 95.472c19.49 17.557 39.562 30.71 59.001 39.324zm-65.604-45.647l95.284-95.286c-4.933-4.963-9.482-10.85-13.667-17.616L32.512 402.213c10.491 17.069 22.148 31.847 33.89 43.319zm313.997 6.327l-95.53-95.525a91.813 91.813 0 01-3.184 2.466l69.71 115.72c9.73-6.055 19.39-13.823 29.004-22.66zm-36.74 27.532l-69.565-115.479c-25.964 14.5-53.318 17.553-82.406 6.337l-51.301 124.583c70.2 28.467 142.961 20.313 203.272-15.441zM171.713 211.67L102.005 95.95c-10.164 6.523-19.819 14.106-29.006 22.66l95.527 95.527a135.39 135.39 0 013.187-2.467zM0 285.236l134.762-.002c.291-23.838 8.71-45.682 26.928-65.006l-95.287-95.287C22.416 171.553.512 225.056 0 285.236zm226.694-91.938L226.7 58.54c-44.728.274-84.005 12.326-116.96 32.539l69.563 115.48c13.077-7.677 28.664-12.98 47.39-13.26zm186.404-37.118l-99.449 99.452a93.73 93.73 0 014.453 20.46h135.09c-.883-40.98-14.404-80.939-40.094-119.912zm40.28 129.052H318.631c-.468 25.24-9.9 48.244-26.924 65.014l95.29 95.286c43.82-43.123 65.122-96.948 66.381-160.3zM235.84 58.74l-.006 135.087c7.082.802 13.883 2.342 20.466 4.455l99.415-99.415c-35.132-24.575-76.54-38.362-119.875-40.127zM430.95 2.597c-39.165 11.457-55.533 55.183-38.782 88.672L254.808 228.63c-32.407-16.157-74.902-1.57-87.795 36.034-15.716 45.84 24.243 91.802 71.754 82.533 42.01-8.196 62.31-54.188 44.466-90.01L420.722 119.7c34.66 17.226 79.533-1.065 89.396-41.654 11.43-47.037-32.658-89.054-79.168-75.449z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const CurlIcon = (props: React.SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={18}
|
||||
height={18}
|
||||
viewBox="0 0 123.184 102.926"
|
||||
{...props}
|
||||
>
|
||||
<defs>
|
||||
<symbol overflow="visible">
|
||||
<path
|
||||
d="M73.36 0v-84.172H13.233V0zm-9.626-78.156L43.297-47.5 22.844-78.156zm3.61 66.734L46.89-42.094 67.344-72.75zm-44.5 5.406l20.453-30.656L63.734-6.016zm-3.61-66.734l20.453 30.656-20.453 30.672zm0 0"
|
||||
stroke="none"
|
||||
/>
|
||||
</symbol>
|
||||
<symbol overflow="visible">
|
||||
<path
|
||||
d="M15.031-26.813c0 7.094 1.328 10.579 6.86 15.141l8.53 7.11C35.829-.125 38.595 0 45.329 0H63.61c3.844 0 7.938-.844 7.938-5.766 0-5.062-4.563-5.78-7.938-5.78H43.297c-2.297 0-4.578-1.204-6.625-2.892l-6.969-5.89c-1.812-1.438-3.125-2.64-3.125-4.922v-14.313c0-2.28 1.313-3.484 3.125-4.937l6.969-5.89c2.047-1.672 4.328-2.876 6.625-2.876h20.312c3.375 0 7.938-.734 7.938-5.78 0-4.923-4.094-5.767-7.938-5.767H45.33c-6.735 0-9.5.126-14.907 4.563l-8.531 7.094c-5.532 4.578-6.86 8.062-6.86 15.156zm0 0"
|
||||
stroke="none"
|
||||
/>
|
||||
</symbol>
|
||||
<symbol overflow="visible">
|
||||
<path
|
||||
d="M71.547-56.875c0-3.969-.844-7.938-5.766-7.938-4.937 0-5.781 3.97-5.781 7.938v33.547L43.047-12.625c-1.078.719-2.281 1.078-3.61 1.078h-3.843c-2.766 0-5.766-.719-6.735-2.406-.968-1.672-1.078-4.813-1.203-7.453l-1.203-35.469c-.125-3.734-.719-7.938-5.656-7.938-5.281 0-5.89 4.454-5.766 8.657l1.203 36.312c.235 6.25.125 11.188 5.297 15.64C25.375-.968 28.984 0 32.234 0h8.532c3.125 0 6.25-1.438 9.984-3.844l9.375-6.14v1.921c0 3.97.844 8.063 5.766 8.063 6.14 0 5.656-5.89 5.656-8.531zm0 0"
|
||||
stroke="none"
|
||||
/>
|
||||
</symbol>
|
||||
<symbol overflow="visible">
|
||||
<path
|
||||
d="M15.031-7.938c0 3.97.844 7.938 5.766 7.938 4.937 0 5.781-3.969 5.781-7.938v-29.218L42.688-50.5c2.046-1.688 3.609-2.766 5.765-2.766h3.375c2.766 0 5.531 0 6.969 2.047C60-49.547 60-48.094 60-46.172c0 3.719 1.688 7.094 5.89 7.094 4.938 0 5.657-4.094 5.657-8.063 0-5.28-1.328-9.25-5.172-13.109-3.125-3.125-6.844-4.563-11.297-4.563h-9.266c-3.609 0-6.968 2.282-10.218 4.922l-9.016 7.47v-4.704c0-3.844-1.203-7.688-5.781-7.688-5.281 0-5.766 4.329-5.766 8.532zm0 0"
|
||||
stroke="none"
|
||||
/>
|
||||
</symbol>
|
||||
<symbol overflow="visible">
|
||||
<path
|
||||
d="M49.297-80.563c0-6.859-1.922-8.062-8.531-8.062H28.625c-3.86 0-7.938.844-7.938 5.766 0 5.062 4.563 5.78 7.938 5.78h9.14v65.532h-9.14c-3.86 0-7.938.844-7.938 5.781C20.688-.719 25.25 0 28.625 0h29.328c3.375 0 7.938-.719 7.938-5.766 0-4.937-4.079-5.78-7.938-5.78h-8.656zm0 0"
|
||||
stroke="none"
|
||||
/>
|
||||
</symbol>
|
||||
</defs>
|
||||
<path
|
||||
d="M114.102 14.043a4.96 4.96 0 01-4.961-4.961 4.958 4.958 0 014.96-4.96 4.96 4.96 0 110 9.921M64.833 98.805a4.96 4.96 0 01-4.96-4.961 4.958 4.958 0 014.96-4.961 4.957 4.957 0 014.957 4.96 4.96 4.96 0 01-4.957 4.962M114.102 0a9.082 9.082 0 00-9.082 9.082c0 1.07.27 2.066.609 3.02L63.023 85.125c-4.117.863-7.273 4.344-7.273 8.719a9.082 9.082 0 009.082 9.082c5.012 0 9.078-4.067 9.078-9.082 0-1.008-.27-1.93-.57-2.836l42.82-73.262c3.992-.957 7.024-4.379 7.024-8.664A9.082 9.082 0 00114.102 0"
|
||||
fill="currentColor"
|
||||
fillOpacity={1}
|
||||
fillRule="nonzero"
|
||||
stroke="none"
|
||||
/>
|
||||
<path
|
||||
d="M76.941 14.043a4.96 4.96 0 01-4.96-4.961 4.958 4.958 0 014.96-4.96 4.957 4.957 0 014.957 4.96 4.96 4.96 0 01-4.957 4.961M27.668 98.805a4.96 4.96 0 110-9.922 4.958 4.958 0 014.96 4.96 4.96 4.96 0 01-4.96 4.962M76.941 0a9.08 9.08 0 00-9.082 9.082c0 1.07.27 2.066.61 3.02L25.863 85.125c-4.12.863-7.277 4.344-7.277 8.719a9.082 9.082 0 0018.164 0c0-1.008-.27-1.93-.57-2.836L79 17.746c3.992-.957 7.023-4.379 7.023-8.664 0-5.016-4.07-9.082-9.082-9.082M9.082 29.227a4.963 4.963 0 014.961 4.96 4.96 4.96 0 11-4.96-4.96M9.081 43.27a9.082 9.082 0 009.082-9.082c0-1.004-.273-1.93-.574-2.836-1.203-3.606-4.5-6.247-8.508-6.247-.64 0-1.203.239-1.809.368C3.156 26.332 0 29.813 0 34.188a9.082 9.082 0 009.082 9.082M4.121 65.922a4.96 4.96 0 119.922 0 4.96 4.96 0 01-4.961 4.957 4.96 4.96 0 01-4.96-4.957m14.042 0c0-1.008-.273-1.93-.574-2.836-1.203-3.606-4.496-6.246-8.508-6.246-.64 0-1.203.238-1.809.363C3.156 58.066 0 61.547 0 65.922c0 5.012 4.066 9.082 9.082 9.082 5.016 0 9.082-4.07 9.082-9.082"
|
||||
fill="currentColor"
|
||||
fillOpacity={1}
|
||||
fillRule="nonzero"
|
||||
stroke="none"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
interface ImportModalProps extends ModalProps {
|
||||
organizationId: string;
|
||||
defaultProjectId: string;
|
||||
defaultWorkspaceId?: string;
|
||||
from: 'file' | 'uri' | 'clipboard';
|
||||
}
|
||||
|
||||
export const ImportModal: FC<ImportModalProps> = ({
|
||||
defaultProjectId,
|
||||
defaultWorkspaceId,
|
||||
organizationId,
|
||||
from,
|
||||
...modalProps
|
||||
}) => {
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
const scanResourcesFetcher = useFetcher<ScanForResourcesActionResult>();
|
||||
const importFetcher = useFetcher<ImportResourcesActionResult>();
|
||||
|
||||
useEffect(() => {
|
||||
modalRef.current?.show();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (importFetcher.state === 'loading') {
|
||||
hideAllModals();
|
||||
}
|
||||
}, [importFetcher.state]);
|
||||
|
||||
return (
|
||||
<OverlayContainer>
|
||||
<Modal {...modalProps} ref={modalRef}>
|
||||
<ModalHeader>Import to Insomnia</ModalHeader>
|
||||
{scanResourcesFetcher.data && scanResourcesFetcher.data.errors.length === 0 ? (
|
||||
<ImportResourcesForm
|
||||
organizationId={organizationId}
|
||||
defaultProjectId={defaultProjectId}
|
||||
defaultWorkspaceId={defaultWorkspaceId}
|
||||
scanResult={scanResourcesFetcher.data}
|
||||
errors={importFetcher.data?.errors}
|
||||
onSubmit={e => {
|
||||
importFetcher.submit(e.currentTarget, {
|
||||
method: 'post',
|
||||
action: '/import/resources',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ScanResourcesForm
|
||||
from={from}
|
||||
errors={scanResourcesFetcher.data?.errors}
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
scanResourcesFetcher.submit(e.currentTarget, {
|
||||
method: 'post',
|
||||
action: '/import/scan',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</OverlayContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const ScanResourcesForm = ({
|
||||
onSubmit,
|
||||
from,
|
||||
errors,
|
||||
}: {
|
||||
onSubmit?: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||
from?: 'file' | 'uri' | 'clipboard';
|
||||
errors?: string[];
|
||||
}) => {
|
||||
const id = useId();
|
||||
const [importFrom, setImportFrom] = useState(from || 'uri');
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<form
|
||||
id={id}
|
||||
onSubmit={onSubmit}
|
||||
method="post"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 'var(--padding-sm)',
|
||||
}}
|
||||
>
|
||||
<Fieldset>
|
||||
<RadioGroup>
|
||||
<Radio
|
||||
onChange={() => setImportFrom('file')}
|
||||
name="importFrom"
|
||||
value="file"
|
||||
checked={importFrom === 'file'}
|
||||
>
|
||||
<i className="fa fa-plus" />
|
||||
File
|
||||
</Radio>
|
||||
<Radio
|
||||
onChange={() => setImportFrom('uri')}
|
||||
name="importFrom"
|
||||
value="uri"
|
||||
checked={importFrom === 'uri'}
|
||||
>
|
||||
<i className="fa fa-link" />
|
||||
Url
|
||||
</Radio>
|
||||
<Radio
|
||||
onChange={() => setImportFrom('clipboard')}
|
||||
name="importFrom"
|
||||
value="clipboard"
|
||||
checked={importFrom === 'clipboard'}
|
||||
>
|
||||
<i className="fa fa-clipboard" />
|
||||
Clipboard
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</Fieldset>
|
||||
{importFrom === 'file' && <FileField />}
|
||||
{importFrom === 'uri' && (
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Url:
|
||||
<input
|
||||
type="text"
|
||||
name="uri"
|
||||
placeholder="https://website.com/insomnia-import.json"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
<div>
|
||||
{errors && errors.length > 0 && (
|
||||
<div className="notice error margin-top-sm">
|
||||
<p>
|
||||
<strong>Error while scanning for resources to import:</strong>
|
||||
{errors[0]}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 'var(--padding-sm)',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
paddingBottom: 'var(--padding-sm)',
|
||||
}}
|
||||
>
|
||||
Supported Formats
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 'var(--padding-sm)',
|
||||
}}
|
||||
>
|
||||
<Pill>
|
||||
<InsomniaIcon />
|
||||
Insomnia
|
||||
</Pill>
|
||||
<Pill>
|
||||
<PostmanIcon />
|
||||
Postman
|
||||
</Pill>
|
||||
<Pill>
|
||||
<SwaggerIcon />
|
||||
Swagger
|
||||
</Pill>
|
||||
<Pill>
|
||||
<OpenAPIIcon />
|
||||
OpenAPI
|
||||
</Pill>
|
||||
<Pill>
|
||||
<CurlIcon />
|
||||
cURL
|
||||
</Pill>
|
||||
<Pill>
|
||||
<i className="fa-regular fa-file fa-lg" />
|
||||
HAR
|
||||
</Pill>
|
||||
<Pill>
|
||||
<i className="fa-regular fa-file fa-lg" />
|
||||
WSDL
|
||||
</Pill>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
bg="surprise"
|
||||
type="submit"
|
||||
style={{
|
||||
height: '40px',
|
||||
gap: 'var(--padding-sm)',
|
||||
}}
|
||||
form={id}
|
||||
className="btn"
|
||||
>
|
||||
<i className="fa fa-file-import" /> Scan
|
||||
</Button>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const ImportTypeTitle = styled.div({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--padding-sm)',
|
||||
});
|
||||
|
||||
const ImportResourcesForm = ({
|
||||
scanResult,
|
||||
defaultProjectId,
|
||||
defaultWorkspaceId,
|
||||
organizationId,
|
||||
onSubmit,
|
||||
errors,
|
||||
}: {
|
||||
scanResult: ScanForResourcesActionResult;
|
||||
organizationId: string;
|
||||
defaultProjectId?: string;
|
||||
defaultWorkspaceId?: string;
|
||||
errors?: string[];
|
||||
onSubmit?: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||
}) => {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const projectFetcher = useFetcher<ProjectLoaderData>();
|
||||
|
||||
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState(
|
||||
defaultWorkspaceId || 'create-new-workspace-id'
|
||||
);
|
||||
|
||||
const isCreatingNewWorkspace =
|
||||
selectedWorkspaceId === 'create-new-workspace-id';
|
||||
|
||||
useEffect(() => {
|
||||
if (projectFetcher.state === 'idle' && !projectFetcher.data) {
|
||||
projectFetcher.load(
|
||||
`/organization/${organizationId}/project/${defaultProjectId}`
|
||||
);
|
||||
}
|
||||
}, [defaultProjectId, organizationId, selectedWorkspaceId, projectFetcher]);
|
||||
|
||||
const workspaces: Partial<Workspace>[] = [
|
||||
...(projectFetcher?.data?.workspaces || []),
|
||||
{
|
||||
_id: 'create-new-workspace-id',
|
||||
name: '+ Create New File',
|
||||
},
|
||||
];
|
||||
|
||||
const projects = projectFetcher?.data?.projects || [];
|
||||
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 'var(--padding-md)',
|
||||
}}
|
||||
>
|
||||
<form
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
startTransition(() => {
|
||||
onSubmit?.(e);
|
||||
});
|
||||
}}
|
||||
method="post"
|
||||
action="/import/resources"
|
||||
id={id}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 'var(--font-size-sm)',
|
||||
fontWeight: '500',
|
||||
paddingBottom: 'var(--padding-sm)',
|
||||
}}
|
||||
>
|
||||
Import to:
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid var(--hl-sm)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
margin: 0,
|
||||
}}
|
||||
className="form-control form-control--outlined"
|
||||
>
|
||||
<select
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: 0,
|
||||
margin: 0,
|
||||
}}
|
||||
onChange={e =>
|
||||
projectFetcher.load(
|
||||
`/organization/${organizationId}/project/${e.currentTarget.value}`
|
||||
)
|
||||
}
|
||||
defaultValue={defaultProjectId}
|
||||
name="projectId"
|
||||
>
|
||||
{projects.map(project => (
|
||||
<option key={project._id} value={project._id}>
|
||||
{project.name} (
|
||||
{isDefaultProject(project)
|
||||
? strings.defaultProject.singular
|
||||
: isLocalProject(project)
|
||||
? strings.localProject.singular
|
||||
: strings.remoteProject.singular}
|
||||
)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
margin: 0,
|
||||
}}
|
||||
className="form-control form-control--outlined"
|
||||
>
|
||||
<select
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: 0,
|
||||
margin: 0,
|
||||
}}
|
||||
onChange={e => {
|
||||
setSelectedWorkspaceId(e.target.value);
|
||||
}}
|
||||
value={selectedWorkspaceId}
|
||||
name="workspaceId"
|
||||
>
|
||||
{workspaces.map(workspace => (
|
||||
<option key={workspace._id} value={workspace._id}>
|
||||
{workspace.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{isCreatingNewWorkspace && (
|
||||
<Fragment>
|
||||
<div
|
||||
style={{
|
||||
margin: 0,
|
||||
}}
|
||||
className="form-control form-control--outlined"
|
||||
>
|
||||
<input
|
||||
style={{
|
||||
border: 'none',
|
||||
margin: 0,
|
||||
}}
|
||||
autoFocus
|
||||
type="text"
|
||||
name="name"
|
||||
defaultValue="Untitled"
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
<input hidden name="organizationId" readOnly value={organizationId} />
|
||||
</form>
|
||||
<table className="table--fancy table--outlined margin-top-sm">
|
||||
<thead>
|
||||
<tr className="table--no-outline-row">
|
||||
<th>
|
||||
<ImportTypeTitle>
|
||||
{scanResult.type?.id.includes('insomnia') && (
|
||||
<Fragment>
|
||||
<InsomniaIcon width={24} height={24} />
|
||||
Insomnia
|
||||
</Fragment>
|
||||
)}
|
||||
{scanResult.type?.id.includes('postman') && (
|
||||
<Fragment>
|
||||
<PostmanIcon width={24} height={24} />
|
||||
Postman
|
||||
</Fragment>
|
||||
)}
|
||||
{scanResult.type?.id.includes('swagger') && (
|
||||
<Fragment>
|
||||
<SwaggerIcon width={24} height={24} />
|
||||
Swagger
|
||||
</Fragment>
|
||||
)}
|
||||
{scanResult.type?.id.includes('openapi') && (
|
||||
<Fragment>
|
||||
<OpenAPIIcon width={24} height={24} />
|
||||
OpenAPI
|
||||
</Fragment>
|
||||
)}
|
||||
{scanResult.type?.id.includes('wsdl') && (
|
||||
<Fragment>
|
||||
<i className="fa-regular fa-file fa-lg" />
|
||||
WSDL
|
||||
</Fragment>
|
||||
)}
|
||||
{scanResult.type?.id.includes('har') && (
|
||||
<Fragment>
|
||||
<i className="fa-regular fa-file fa-lg" />
|
||||
HAR
|
||||
</Fragment>
|
||||
)}
|
||||
{scanResult.type?.id.includes('curl') && (
|
||||
<Fragment>
|
||||
<CurlIcon width={24} height={24} />
|
||||
cURL
|
||||
</Fragment>
|
||||
)}{' '}
|
||||
resources to be imported:
|
||||
</ImportTypeTitle>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{scanResult.requests && scanResult.requests?.length > 0 && (
|
||||
<tr
|
||||
key={scanResult.requests[0]._id}
|
||||
className="table--no-outline-row"
|
||||
>
|
||||
<td>
|
||||
{scanResult.requests.length}{' '}
|
||||
{scanResult.requests.length === 1 ? 'Request' : 'Requests'}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{isCreatingNewWorkspace && scanResult.apiSpec && (
|
||||
<tr
|
||||
key={scanResult.apiSpec._id}
|
||||
className="table--no-outline-row"
|
||||
>
|
||||
<td>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--padding-md)',
|
||||
}}
|
||||
>
|
||||
OpenAPI Spec:{' '}
|
||||
{scanResult.apiSpec.name || scanResult.apiSpec.fileName}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{isCreatingNewWorkspace &&
|
||||
scanResult.environments &&
|
||||
scanResult.environments.length > 0 && (
|
||||
<tr className="table--no-outline-row">
|
||||
<td>
|
||||
{scanResult.environments.length}{' '}
|
||||
{scanResult.environments.length === 1
|
||||
? 'Environment'
|
||||
: 'Environments'}
|
||||
{scanResult.cookieJar ? ' and a Cookie Jar' : ''}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{scanResult.unitTestSuites &&
|
||||
scanResult.unitTestSuites?.length > 0 && (
|
||||
<tr className="table--no-outline-row">
|
||||
<td>
|
||||
{scanResult.unitTestSuites.length}{' '}
|
||||
{scanResult.unitTestSuites.length === 1
|
||||
? 'Test Suite'
|
||||
: 'Test Suites'}
|
||||
{scanResult.unitTests?.length}
|
||||
{' with'}
|
||||
{scanResult.unitTests?.length === 1 ? 'Test' : 'Tests'}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{errors && errors.length > 0 && (
|
||||
<div className="notice error margin-top-sm">
|
||||
<p>
|
||||
<strong>Error while importing to Insomnia:</strong>
|
||||
{errors[0]}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 'var(--padding-sm)',
|
||||
width: '100%',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
bg="surprise"
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
style={{
|
||||
height: '40px',
|
||||
gap: 'var(--padding-sm)',
|
||||
}}
|
||||
form={id}
|
||||
className="btn"
|
||||
>
|
||||
{isPending ? (
|
||||
<div>
|
||||
<i className="fa fa-spinner fa-spin" /> Importing
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<i className="fa fa-file-import" /> Import
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { FC } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Dropdown, DropdownButton, DropdownItem, ItemContent } from '../base/dropdown';
|
||||
import { Button } from '../themed-button';
|
||||
|
||||
const Wrapper = styled.div({
|
||||
@@ -48,67 +47,56 @@ const Title = styled.div({
|
||||
interface Props {
|
||||
createRequestCollection: () => void;
|
||||
createDesignDocument: () => void;
|
||||
importFromFile: () => void;
|
||||
importFromURL: () => void;
|
||||
importFromClipboard: () => void;
|
||||
importFromGit: () => void;
|
||||
importFrom: () => void;
|
||||
cloneFromGit: () => void;
|
||||
}
|
||||
|
||||
export const EmptyStatePane: FC<Props> = ({ createRequestCollection, createDesignDocument, importFromFile, importFromURL, importFromClipboard, importFromGit }) => {
|
||||
export const EmptyStatePane: FC<Props> = ({ createRequestCollection, createDesignDocument, importFrom, cloneFromGit }) => {
|
||||
return (
|
||||
<Wrapper>
|
||||
<Title>This is an empty project, to get started create your first resource:</Title>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', marginTop: 'var(--padding-md)' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginTop: 'var(--padding-md)', gap: 'var(--padding-md)' }}>
|
||||
<div style={{ width: '100%', display: 'flex', flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginTop: 'var(--padding-md)', gap: 'var(--padding-md)' }}>
|
||||
<Button
|
||||
style={{
|
||||
gap: 'var(--padding-xs)',
|
||||
flex: 1,
|
||||
}}
|
||||
onClick={createRequestCollection}
|
||||
>
|
||||
New Collection
|
||||
<i className='fa fa-bars' /> New Collection
|
||||
</Button>
|
||||
<Button
|
||||
style={{
|
||||
gap: 'var(--padding-xs)',
|
||||
flex: 1,
|
||||
}}
|
||||
onClick={createDesignDocument}
|
||||
>
|
||||
New Document
|
||||
<i className='fa fa-file-o' /> New Document
|
||||
</Button>
|
||||
</div>
|
||||
<Divider>or</Divider>
|
||||
<Dropdown
|
||||
aria-label='Import From Dropdown'
|
||||
triggerButton={
|
||||
<DropdownButton style={{ width: '100%', alignSelf: 'stretch' }}>
|
||||
Import From <i className="fa fa-caret-down pad-left-sm" />
|
||||
</DropdownButton>
|
||||
}
|
||||
>
|
||||
<DropdownItem aria-label='File'>
|
||||
<ItemContent
|
||||
icon="plus"
|
||||
label="File"
|
||||
onClick={importFromFile}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label='URL'>
|
||||
<ItemContent
|
||||
icon="link"
|
||||
label="URL"
|
||||
onClick={importFromURL}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label='Clipboard'>
|
||||
<ItemContent
|
||||
icon="clipboard"
|
||||
label="Clipboard"
|
||||
onClick={importFromClipboard}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label='Git Clone'>
|
||||
<ItemContent
|
||||
icon="code-fork"
|
||||
label="Git Clone"
|
||||
onClick={importFromGit}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
<div style={{ width: '100%', display: 'flex', flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: 'var(--padding-md)' }}>
|
||||
<Button
|
||||
style={{
|
||||
gap: 'var(--padding-xs)',
|
||||
flex: 1,
|
||||
}}
|
||||
onClick={importFrom}
|
||||
>
|
||||
<i className='fa fa-file-import' /> Import
|
||||
</Button>
|
||||
<Button
|
||||
style={{
|
||||
gap: 'var(--padding-xs)',
|
||||
flex: 1,
|
||||
}}
|
||||
onClick={cloneFromGit}
|
||||
>
|
||||
<i className='fa fa-code-fork' /> Git Clone
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import React, { FC, Fragment, useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useRevalidator } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { getProductName } from '../../../common/constants';
|
||||
import { docsImportExport } from '../../../common/documentation';
|
||||
import { exportAllToFile } from '../../../common/export';
|
||||
import { getWorkspaceLabel } from '../../../common/get-workspace-label';
|
||||
import { ForceToWorkspace } from '../../../common/import';
|
||||
import { strings } from '../../../common/strings';
|
||||
import { isRequestGroup } from '../../../models/request-group';
|
||||
import { importers } from '../../../utils/importers/importers';
|
||||
import { importClipBoard, importFile, importUri } from '../../import';
|
||||
import { selectActiveProject, selectActiveProjectName, selectActiveWorkspace, selectActiveWorkspaceName, selectProjects, selectWorkspaceRequestsAndRequestGroups, selectWorkspacesForActiveProject, selectWorkspacesWithResolvedNameForActiveProject } from '../../redux/selectors';
|
||||
import { selectActiveProjectName, selectActiveWorkspace, selectActiveWorkspaceName, selectWorkspaceRequestsAndRequestGroups, selectWorkspacesForActiveProject } from '../../redux/selectors';
|
||||
import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
|
||||
import { Link } from '../base/link';
|
||||
import { AlertModal } from '../modals/alert-modal';
|
||||
import { ExportRequestsModal } from '../modals/export-requests-modal';
|
||||
import { showModal, showPrompt } from '../modals/index';
|
||||
import { ImportModal } from '../modals/import-modal';
|
||||
import { showModal } from '../modals/index';
|
||||
import { Button } from '../themed-button';
|
||||
|
||||
interface Props {
|
||||
@@ -24,37 +23,17 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
|
||||
const {
|
||||
organizationId,
|
||||
projectId,
|
||||
workspaceId,
|
||||
} = useParams() as { organizationId: string; projectId: string; workspaceId: string };
|
||||
const projectName = useSelector(selectActiveProjectName) ?? getProductName();
|
||||
const activeWorkspace = useSelector(selectActiveWorkspace);
|
||||
const activeProjectWorkspaces = useSelector(selectWorkspacesWithResolvedNameForActiveProject);
|
||||
const activeProject = useSelector(selectActiveProject);
|
||||
const projects = useSelector(selectProjects);
|
||||
const forceToWorkspace = activeWorkspace?._id ? ForceToWorkspace.current : ForceToWorkspace.existing;
|
||||
const activeWorkspaceName = useSelector(selectActiveWorkspaceName);
|
||||
const workspacesForActiveProject = useSelector(selectWorkspacesForActiveProject);
|
||||
const workspaceRequestsAndRequestGroups = useSelector(selectWorkspaceRequestsAndRequestGroups);
|
||||
const { revalidate } = useRevalidator();
|
||||
const handleImportUri = useCallback(() => {
|
||||
const lastUsedImportUri = window.localStorage.getItem('insomnia.lastUsedImportUri');
|
||||
const defaultValue = lastUsedImportUri ? { defaultValue: lastUsedImportUri } : {};
|
||||
|
||||
showPrompt({
|
||||
title: 'Import Data from URL',
|
||||
submitName: 'Fetch and Import',
|
||||
label: 'URL',
|
||||
placeholder: 'https://website.com/insomnia-import.json',
|
||||
onComplete: (uri: string) => {
|
||||
window.localStorage.setItem('insomnia.lastUsedImportUri', uri);
|
||||
importUri(uri, {
|
||||
activeProjectWorkspaces,
|
||||
activeProject,
|
||||
projects,
|
||||
workspaceId: activeWorkspace?._id,
|
||||
forceToWorkspace, onComplete: revalidate });
|
||||
hideSettingsModal();
|
||||
},
|
||||
...defaultValue,
|
||||
});
|
||||
}, [activeProjectWorkspaces, activeProject, projects, activeWorkspace?._id, forceToWorkspace, revalidate, hideSettingsModal]);
|
||||
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
|
||||
|
||||
const showExportRequestsModal = useCallback(() => {
|
||||
if (!workspaceRequestsAndRequestGroups.filter(r => !isRequestGroup(r)).length) {
|
||||
@@ -73,109 +52,74 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
|
||||
hideSettingsModal();
|
||||
}, [hideSettingsModal, projectName, workspacesForActiveProject]);
|
||||
|
||||
const handleImportFile = useCallback(() => {
|
||||
importFile({
|
||||
activeProjectWorkspaces,
|
||||
activeProject,
|
||||
projects,
|
||||
workspaceId: activeWorkspace?._id,
|
||||
forceToWorkspace, onComplete: revalidate });
|
||||
hideSettingsModal();
|
||||
}, [activeProjectWorkspaces, activeProject, projects, activeWorkspace?._id, forceToWorkspace, revalidate, hideSettingsModal]);
|
||||
|
||||
const handleImportClipBoard = useCallback(() => {
|
||||
importClipBoard({
|
||||
activeProjectWorkspaces,
|
||||
activeProject,
|
||||
projects,
|
||||
workspaceId: activeWorkspace?._id,
|
||||
forceToWorkspace, onComplete: revalidate });
|
||||
hideSettingsModal();
|
||||
}, [activeProjectWorkspaces, activeProject, projects, activeWorkspace?._id, forceToWorkspace, revalidate, hideSettingsModal]);
|
||||
|
||||
const activeWorkspaceName = useSelector(selectActiveWorkspaceName);
|
||||
|
||||
return (
|
||||
<div data-testid="import-export-tab">
|
||||
<div className="no-margin-top">
|
||||
Import format will be automatically detected. Supported formats include: {importers.map(importer => importer.name).join(', ')}
|
||||
</div>
|
||||
<p>
|
||||
Your format isn't supported? <Link href={docsImportExport}>Add Your Own</Link>.
|
||||
</p>
|
||||
<div className="pad-top">
|
||||
{activeWorkspace ?
|
||||
(<Dropdown
|
||||
aria-label='Export Data Dropdown'
|
||||
triggerButton={
|
||||
<DropdownButton className="btn btn--clicky">
|
||||
Export Data <i className="fa fa-caret-down" />
|
||||
</DropdownButton>
|
||||
}
|
||||
>
|
||||
<DropdownSection
|
||||
aria-label="Choose Export Type"
|
||||
title="Choose Export Type"
|
||||
<Fragment>
|
||||
<div data-testid="import-export-tab">
|
||||
<div className="no-margin-top">
|
||||
Import format will be automatically detected. Supported formats include: {importers.map(importer => importer.name).join(', ')}
|
||||
</div>
|
||||
<p>
|
||||
Your format isn't supported? <Link href={docsImportExport}>Add Your Own</Link>.
|
||||
</p>
|
||||
<div className="pad-top">
|
||||
{activeWorkspace ?
|
||||
(<Dropdown
|
||||
aria-label='Export Data Dropdown'
|
||||
triggerButton={
|
||||
<DropdownButton className="btn btn--clicky">
|
||||
Export Data <i className="fa fa-caret-down" />
|
||||
</DropdownButton>
|
||||
}
|
||||
>
|
||||
<DropdownItem aria-label={`Export the "${activeWorkspaceName}" ${getWorkspaceLabel(activeWorkspace).singular}`}>
|
||||
<ItemContent
|
||||
icon="home"
|
||||
label={`Export the "${activeWorkspaceName}" ${getWorkspaceLabel(activeWorkspace).singular}`}
|
||||
onClick={showExportRequestsModal}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label={`All ${strings.document.plural} and ${strings.collection.plural} from the "${projectName}" ${strings.project.singular}`}>
|
||||
<ItemContent
|
||||
icon="empty"
|
||||
label={`All ${strings.document.plural} and ${strings.collection.plural} from the "${projectName}" ${strings.project.singular}`}
|
||||
onClick={handleExportAllToFile}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
</Dropdown>) : (<Button onClick={handleExportAllToFile}>Export all</Button>)
|
||||
}
|
||||
|
||||
<Dropdown
|
||||
aria-label='Import Data Dropdown'
|
||||
triggerButton={
|
||||
<DropdownButton className="btn btn--clicky">
|
||||
Import Data <i className="fa fa-caret-down" />
|
||||
</DropdownButton>
|
||||
<DropdownSection
|
||||
aria-label="Choose Export Type"
|
||||
title="Choose Export Type"
|
||||
>
|
||||
<DropdownItem aria-label={`Export the "${activeWorkspaceName}" ${getWorkspaceLabel(activeWorkspace).singular}`}>
|
||||
<ItemContent
|
||||
icon="home"
|
||||
label={`Export the "${activeWorkspaceName}" ${getWorkspaceLabel(activeWorkspace).singular}`}
|
||||
onClick={showExportRequestsModal}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label={`All ${strings.document.plural} and ${strings.collection.plural} from the "${projectName}" ${strings.project.singular}`}>
|
||||
<ItemContent
|
||||
icon="empty"
|
||||
label={`All ${strings.document.plural} and ${strings.collection.plural} from the "${projectName}" ${strings.project.singular}`}
|
||||
onClick={handleExportAllToFile}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
</Dropdown>) : (<Button onClick={handleExportAllToFile}>Export all</Button>)
|
||||
}
|
||||
>
|
||||
<DropdownSection
|
||||
aria-label="Choose Import Type"
|
||||
title="Choose Import Type"
|
||||
>
|
||||
<DropdownItem aria-label='From File'>
|
||||
<ItemContent
|
||||
icon="file-o"
|
||||
label="From File"
|
||||
onClick={handleImportFile}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label='From URL'>
|
||||
<ItemContent
|
||||
icon="link"
|
||||
label="From URL"
|
||||
onClick={handleImportUri}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label='From Clipboard'>
|
||||
<ItemContent
|
||||
icon="clipboard"
|
||||
label="From Clipboard"
|
||||
onClick={handleImportClipBoard}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
</Dropdown>
|
||||
|
||||
<Link href="https://insomnia.rest/create-run-button" className="btn btn--compact" button>
|
||||
Create Run Button
|
||||
</Link>
|
||||
<Button
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--padding-sm)',
|
||||
}}
|
||||
onClick={() => setIsImportModalOpen(true)}
|
||||
>
|
||||
<i className="fa fa-file-import" />
|
||||
Import
|
||||
</Button>
|
||||
|
||||
<Link href="https://insomnia.rest/create-run-button" className="btn btn--compact" button>
|
||||
Create Run Button
|
||||
</Link>
|
||||
</div>
|
||||
<p className="italic faint">* Tip: You can also paste Curl commands into the URL bar</p>
|
||||
</div>
|
||||
<p className="italic faint">* Tip: You can also paste Curl commands into the URL bar</p>
|
||||
</div>
|
||||
{isImportModalOpen && (
|
||||
<ImportModal
|
||||
onHide={() => setIsImportModalOpen(false)}
|
||||
from={'file'}
|
||||
organizationId={organizationId}
|
||||
defaultProjectId={projectId}
|
||||
defaultWorkspaceId={workspaceId}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,6 @@ import { ipcRenderer } from 'electron';
|
||||
import { FC, useEffect } from 'react';
|
||||
|
||||
import { useAppCommands } from '../hooks/use-app-commands';
|
||||
import { useDragAndDropImportFile } from '../hooks/use-drag-and-drop-import-file';
|
||||
import { useGlobalKeyboardShortcuts } from '../hooks/use-global-keyboard-shortcuts';
|
||||
import { useSettingsSideEffects } from '../hooks/use-settings-side-effects';
|
||||
import { useSyncMigration } from '../hooks/use-sync-migration';
|
||||
@@ -12,7 +11,6 @@ export const AppHooks: FC = () => {
|
||||
useSyncMigration();
|
||||
useSettingsSideEffects();
|
||||
useGlobalKeyboardShortcuts();
|
||||
useDragAndDropImportFile();
|
||||
useThemeChange();
|
||||
useAppCommands();
|
||||
// Give it a bit before letting the backend know it's ready
|
||||
|
||||
@@ -4,7 +4,6 @@ import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { isDevelopment } from '../../common/constants';
|
||||
import { askToImportIntoProject, askToImportIntoWorkspace, askToSetWorkspaceScope, importUri } from '../../common/import';
|
||||
import * as models from '../../models';
|
||||
import { reloadPlugins } from '../../plugins';
|
||||
import { createPlugin } from '../../plugins/create';
|
||||
@@ -53,26 +52,19 @@ export const useAppCommands = () => {
|
||||
});
|
||||
break;
|
||||
|
||||
case 'insomnia://app/import':
|
||||
showModal(AlertModal, {
|
||||
title: 'Confirm Data Import',
|
||||
message: (
|
||||
<span>
|
||||
Do you really want to import {params.name && (<><code>{params.name}</code> from</>)} <code>{params.uri}</code>?
|
||||
</span>
|
||||
),
|
||||
addCancel: true,
|
||||
onConfirm: async () => {
|
||||
const activeWorkspaceId = activeWorkspace?._id;
|
||||
importUri(params.uri, {
|
||||
getWorkspaceScope: askToSetWorkspaceScope(),
|
||||
getWorkspaceId: askToImportIntoWorkspace({ workspaceId: params.workspaceId || activeWorkspaceId, activeProjectWorkspaces }),
|
||||
// Currently, just return the active project instead of prompting for which project to import into
|
||||
getProjectId: askToImportIntoProject({ projects, activeProject }),
|
||||
});
|
||||
},
|
||||
});
|
||||
break;
|
||||
// case 'insomnia://app/import':
|
||||
// @TODO @gatzjames - FixME - Move this to a separate hook in the root of the app that opens the import modal
|
||||
// showModal(AlertModal, {
|
||||
// title: 'Confirm Data Import',
|
||||
// message: (
|
||||
// <span>
|
||||
// Do you really want to import {params.name && (<><code>{params.name}</code> from</>)} <code>{params.uri}</code>?
|
||||
// </span>
|
||||
// ),
|
||||
// addCancel: true,
|
||||
// onConfirm: async () => {},
|
||||
// });
|
||||
// break;
|
||||
|
||||
case 'insomnia://plugins/install':
|
||||
showModal(AskModal, {
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { showModal } from '../components/modals';
|
||||
import { AlertModal } from '../components/modals/alert-modal';
|
||||
import { importUri } from '../import';
|
||||
import { selectActiveProject, selectActiveWorkspace, selectProjects, selectWorkspacesWithResolvedNameForActiveProject } from '../redux/selectors';
|
||||
|
||||
export const useDragAndDropImportFile = () => {
|
||||
const activeWorkspace = useSelector(selectActiveWorkspace);
|
||||
const activeProjectWorkspaces = useSelector(selectWorkspacesWithResolvedNameForActiveProject);
|
||||
const activeProject = useSelector(selectActiveProject);
|
||||
const projects = useSelector(selectProjects);
|
||||
|
||||
// Global Drag and Drop for importing files
|
||||
useEffect(() => {
|
||||
// NOTE: This is required for "drop" event to trigger.
|
||||
document.addEventListener(
|
||||
'dragover',
|
||||
event => {
|
||||
event.preventDefault();
|
||||
},
|
||||
false,
|
||||
);
|
||||
document.addEventListener(
|
||||
'drop',
|
||||
async event => {
|
||||
event.preventDefault();
|
||||
if (!activeWorkspace) {
|
||||
return;
|
||||
}
|
||||
const files = event.dataTransfer?.files || [];
|
||||
if (files.length === 0) {
|
||||
console.log('[drag] Ignored drop event because no files present');
|
||||
return;
|
||||
}
|
||||
const file = files[0];
|
||||
if (!file?.path) {
|
||||
return;
|
||||
}
|
||||
await showModal(AlertModal, {
|
||||
title: 'Confirm Data Import',
|
||||
message: (
|
||||
<span>
|
||||
Import <code>{file.path}</code>?
|
||||
</span>
|
||||
),
|
||||
addCancel: true,
|
||||
});
|
||||
importUri(`file://${file.path}`, {
|
||||
activeProjectWorkspaces,
|
||||
activeProject,
|
||||
projects,
|
||||
workspaceId: activeWorkspace?._id,
|
||||
});
|
||||
},
|
||||
false,
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -1,192 +0,0 @@
|
||||
import electron, { OpenDialogOptions } from 'electron';
|
||||
|
||||
import {
|
||||
askToImportIntoProject,
|
||||
askToImportIntoWorkspace,
|
||||
askToSetWorkspaceScope,
|
||||
ForceToWorkspace,
|
||||
importRaw,
|
||||
importUri as _importUri,
|
||||
} from '../common/import';
|
||||
import * as models from '../models';
|
||||
import { DEFAULT_PROJECT_ID, Project } from '../models/project';
|
||||
import { Workspace, WorkspaceScope } from '../models/workspace';
|
||||
import { showError, showModal } from './components/modals';
|
||||
import { AlertModal } from './components/modals/alert-modal';
|
||||
|
||||
export interface ImportOptions {
|
||||
workspaceId?: string;
|
||||
forceToProject?: 'active' | 'prompt';
|
||||
forceToWorkspace?: ForceToWorkspace;
|
||||
forceToScope?: WorkspaceScope;
|
||||
onComplete?: () => void;
|
||||
activeProject?: Project;
|
||||
activeProjectWorkspaces?: Workspace[];
|
||||
projects?: Project[];
|
||||
}
|
||||
|
||||
export const importFile = async (
|
||||
{
|
||||
forceToScope,
|
||||
forceToWorkspace,
|
||||
workspaceId,
|
||||
forceToProject,
|
||||
activeProject,
|
||||
activeProjectWorkspaces,
|
||||
projects,
|
||||
onComplete,
|
||||
}: ImportOptions = {},
|
||||
) => {
|
||||
const openDialogOptions: OpenDialogOptions = {
|
||||
title: 'Import Insomnia Data',
|
||||
buttonLabel: 'Import',
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
// @ts-expect-error https://github.com/electron/electron/pull/29322
|
||||
{
|
||||
extensions: [
|
||||
'',
|
||||
'sh',
|
||||
'txt',
|
||||
'json',
|
||||
'har',
|
||||
'curl',
|
||||
'bash',
|
||||
'shell',
|
||||
'yaml',
|
||||
'yml',
|
||||
'wsdl',
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const { canceled, filePaths } = await window.dialog.showOpenDialog(openDialogOptions);
|
||||
|
||||
if (canceled) {
|
||||
// It was cancelled, so let's bail out
|
||||
return;
|
||||
}
|
||||
// Let's import all the files!
|
||||
for (const filePath of filePaths) {
|
||||
try {
|
||||
const uri = `file://${filePath}`;
|
||||
const config = {
|
||||
getWorkspaceScope: askToSetWorkspaceScope(forceToScope),
|
||||
getWorkspaceId: askToImportIntoWorkspace({ workspaceId, forceToWorkspace, activeProjectWorkspaces }),
|
||||
// Currently, just return the active project instead of prompting for which project to import into
|
||||
getProjectId: forceToProject === 'prompt' ? askToImportIntoProject({ projects, activeProject }) : () => Promise.resolve(activeProject?._id || DEFAULT_PROJECT_ID),
|
||||
};
|
||||
const { error, summary } = await _importUri(uri, config);
|
||||
if (!error) {
|
||||
models.stats.incrementRequestStats({ createdRequests: summary[models.request.type].length + summary[models.grpcRequest.type].length });
|
||||
}
|
||||
if (error) {
|
||||
showError({
|
||||
title: 'Import Failed',
|
||||
message: 'The file does not contain a valid specification.',
|
||||
error,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
showModal(AlertModal, {
|
||||
title: 'Import Failed',
|
||||
message: err + '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onComplete?.();
|
||||
};
|
||||
|
||||
export const importClipBoard = async ({
|
||||
forceToScope,
|
||||
forceToWorkspace,
|
||||
workspaceId,
|
||||
forceToProject,
|
||||
activeProject,
|
||||
activeProjectWorkspaces,
|
||||
projects,
|
||||
onComplete,
|
||||
}: ImportOptions = {},
|
||||
) => {
|
||||
const schema = electron.clipboard.readText();
|
||||
|
||||
if (!schema) {
|
||||
showModal(AlertModal, {
|
||||
title: 'Import Failed',
|
||||
message: 'Your clipboard appears to be empty.',
|
||||
});
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// Let's import all the paths!
|
||||
try {
|
||||
const config = {
|
||||
getWorkspaceScope: askToSetWorkspaceScope(forceToScope),
|
||||
getWorkspaceId: askToImportIntoWorkspace({ workspaceId, forceToWorkspace, activeProjectWorkspaces }),
|
||||
// Currently, just return the active project instead of prompting for which project to import into
|
||||
getProjectId: forceToProject === 'prompt' ? askToImportIntoProject({ projects, activeProject }) : () => Promise.resolve(activeProject?._id || DEFAULT_PROJECT_ID),
|
||||
};
|
||||
const { error, summary } = await importRaw(schema, config);
|
||||
if (!error) {
|
||||
models.stats.incrementRequestStats({ createdRequests: summary[models.request.type].length + summary[models.grpcRequest.type].length });
|
||||
}
|
||||
if (error) {
|
||||
showError({
|
||||
title: 'Import Failed',
|
||||
message: 'Your clipboard does not contain a valid specification.',
|
||||
error,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
showModal(AlertModal, {
|
||||
title: 'Import Failed',
|
||||
message: 'Your clipboard does not contain a valid specification.',
|
||||
});
|
||||
}
|
||||
|
||||
onComplete?.();
|
||||
};
|
||||
|
||||
export const importUri = async (
|
||||
uri: string,
|
||||
{
|
||||
forceToScope,
|
||||
forceToWorkspace,
|
||||
workspaceId,
|
||||
forceToProject,
|
||||
activeProject,
|
||||
activeProjectWorkspaces,
|
||||
projects,
|
||||
onComplete,
|
||||
}: ImportOptions = {},
|
||||
) => {
|
||||
|
||||
try {
|
||||
const config = {
|
||||
getWorkspaceScope: askToSetWorkspaceScope(forceToScope),
|
||||
getWorkspaceId: askToImportIntoWorkspace({ workspaceId, forceToWorkspace, activeProjectWorkspaces }),
|
||||
// Currently, just return the active project instead of prompting for which project to import into
|
||||
getProjectId: forceToProject === 'prompt' ? askToImportIntoProject({ projects, activeProject }) : () => Promise.resolve(activeProject?._id || DEFAULT_PROJECT_ID),
|
||||
};
|
||||
const { error, summary } = await _importUri(uri, config);
|
||||
if (!error) {
|
||||
models.stats.incrementRequestStats({ createdRequests: summary[models.request.type].length + summary[models.grpcRequest.type].length });
|
||||
}
|
||||
if (error) {
|
||||
showError({
|
||||
title: 'Import Failed',
|
||||
message: 'The URI does not contain a valid specification.',
|
||||
error,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
showModal(AlertModal, {
|
||||
title: 'Import Failed',
|
||||
message: err + '',
|
||||
});
|
||||
}
|
||||
|
||||
onComplete?.();
|
||||
};
|
||||
@@ -69,6 +69,19 @@ const router = createMemoryRouter(
|
||||
element: <Root />,
|
||||
errorElement: <ErrorRoute />,
|
||||
children: [
|
||||
{
|
||||
path: 'import',
|
||||
children: [
|
||||
{
|
||||
path: 'scan',
|
||||
action: async (...args) => (await import('./routes/import')).scanForResourcesAction(...args),
|
||||
},
|
||||
{
|
||||
path: 'resources',
|
||||
action: async (...args) => (await import('./routes/import')).importResourcesAction(...args),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'organization',
|
||||
children: [
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ActionFunction, redirect } from 'react-router-dom';
|
||||
import * as session from '../../account/session';
|
||||
import { ACTIVITY_DEBUG, ACTIVITY_SPEC } from '../../common/constants';
|
||||
import { database } from '../../common/database';
|
||||
import { importRaw } from '../../common/import';
|
||||
import { importResources, scanResources } from '../../common/import';
|
||||
import * as models from '../../models';
|
||||
import * as workspaceOperations from '../../models/helpers/workspace-operations';
|
||||
import { DEFAULT_ORGANIZATION_ID } from '../../models/organization';
|
||||
@@ -466,6 +466,7 @@ export const generateCollectionFromApiSpecAction: ActionFunction = async ({
|
||||
}) => {
|
||||
const { organizationId, projectId, workspaceId } = params;
|
||||
|
||||
invariant(typeof projectId === 'string', 'Project ID is required');
|
||||
invariant(typeof workspaceId === 'string', 'Workspace ID is required');
|
||||
|
||||
const apiSpec = await models.apiSpec.getByParentId(workspaceId);
|
||||
@@ -491,13 +492,13 @@ export const generateCollectionFromApiSpecAction: ActionFunction = async ({
|
||||
throw new Error('Error Generating Configuration');
|
||||
}
|
||||
|
||||
await importRaw(apiSpec.contents, {
|
||||
getWorkspaceId: () => Promise.resolve(workspaceId),
|
||||
enableDiffBasedPatching: true,
|
||||
enableDiffDeep: true,
|
||||
bypassDiffProps: {
|
||||
url: true,
|
||||
},
|
||||
await scanResources({
|
||||
content: apiSpec.contents,
|
||||
});
|
||||
|
||||
await importResources({
|
||||
projectId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/${ACTIVITY_DEBUG}`);
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// Import
|
||||
import { clipboard } from 'electron';
|
||||
import { ActionFunction, redirect } from 'react-router-dom';
|
||||
|
||||
import { ACTIVITY_DEBUG, ACTIVITY_SPEC } from '../../common/constants';
|
||||
import { fetchImportContentFromURI, importResources, scanResources, ScanResult } from '../../common/import';
|
||||
import * as models from '../../models';
|
||||
import { WorkspaceScope } from '../../models/workspace';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
|
||||
function guard<T>(condition: any, value: any): asserts value is T {
|
||||
if (!condition) {
|
||||
throw new Error('Guard failed');
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export interface ScanForResourcesActionResult extends ScanResult {}
|
||||
|
||||
export const scanForResourcesAction: ActionFunction = async ({ request }): Promise<ScanForResourcesActionResult> => {
|
||||
const formData = await request.formData();
|
||||
|
||||
const source = formData.get('importFrom');
|
||||
invariant(typeof source === 'string', 'Source is required.');
|
||||
guard<'file' | 'uri' | 'clipboard'>(source, ['file', 'uri', 'clipboard'].includes(source));
|
||||
|
||||
let content = '';
|
||||
if (source === 'uri') {
|
||||
const uri = formData.get('uri');
|
||||
if (typeof uri !== 'string' || uri === '') {
|
||||
return {
|
||||
errors: ['URI is required'],
|
||||
};
|
||||
}
|
||||
|
||||
content = await fetchImportContentFromURI({
|
||||
uri,
|
||||
});
|
||||
} else if (source === 'file') {
|
||||
const filePath = formData.get('filePath');
|
||||
if (typeof filePath !== 'string' || filePath === '') {
|
||||
return {
|
||||
errors: ['File is required'],
|
||||
};
|
||||
}
|
||||
const uri = `file://${filePath}`;
|
||||
|
||||
content = await fetchImportContentFromURI({
|
||||
uri,
|
||||
});
|
||||
} else {
|
||||
content = clipboard.readText();
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
return {
|
||||
errors: ['No content to import'],
|
||||
};
|
||||
}
|
||||
|
||||
const result = await scanResources({ content });
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export interface ImportResourcesActionResult {
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export const importResourcesAction: ActionFunction = async ({ request }): Promise<ImportResourcesActionResult | Response> => {
|
||||
const formData = await request.formData();
|
||||
|
||||
const organizationId = formData.get('organizationId');
|
||||
const projectId = formData.get('projectId');
|
||||
const workspaceId = formData.get('workspaceId');
|
||||
const scope = formData.get('scope') || 'design';
|
||||
const name = formData.get('name');
|
||||
|
||||
const workspaceName = typeof name === 'string' ? name : 'Untitled';
|
||||
|
||||
invariant(typeof organizationId === 'string', 'OrganizationId is required.');
|
||||
invariant(typeof projectId === 'string', 'ProjectId is required.');
|
||||
invariant(typeof workspaceId === 'string', 'WorkspaceId is required.');
|
||||
invariant(typeof scope === 'string', 'Scope is required.');
|
||||
|
||||
guard<WorkspaceScope>(scope === 'design' || scope === 'collection', scope);
|
||||
|
||||
const project = await models.project.getById(projectId);
|
||||
invariant(project, 'Project not found.');
|
||||
|
||||
const result = await importResources({
|
||||
projectId: project._id,
|
||||
workspaceId,
|
||||
workspaceName,
|
||||
});
|
||||
|
||||
return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${result.workspace._id}/${
|
||||
result.workspace.scope === 'design'
|
||||
? ACTIVITY_SPEC
|
||||
: ACTIVITY_DEBUG
|
||||
}`);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, Fragment, useCallback, useRef, useState } from 'react';
|
||||
import React, { FC, Fragment, useRef, useState } from 'react';
|
||||
import {
|
||||
AriaButtonProps,
|
||||
AriaGridListItemOptions,
|
||||
@@ -16,17 +16,11 @@ import {
|
||||
useLoaderData,
|
||||
useNavigate,
|
||||
useParams,
|
||||
useRevalidator,
|
||||
useRouteLoaderData,
|
||||
useSearchParams,
|
||||
useSubmit,
|
||||
} from 'react-router-dom';
|
||||
import {
|
||||
Item,
|
||||
ListProps,
|
||||
ListState,
|
||||
useListState,
|
||||
} from 'react-stately';
|
||||
import { Item, ListProps, ListState, useListState } from 'react-stately';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { parseApiSpec, ParsedApiSpec } from '../../common/api-specs';
|
||||
@@ -36,7 +30,6 @@ import {
|
||||
DashboardSortOrder,
|
||||
} from '../../common/constants';
|
||||
import { clickLink } from '../../common/electron-helpers';
|
||||
import { ForceToWorkspace } from '../../common/import';
|
||||
import { fuzzyMatchAll, isNotNullOrUndefined } from '../../common/misc';
|
||||
import { descendingNumberSort, sortMethodMap } from '../../common/sorting';
|
||||
import { strings } from '../../common/strings';
|
||||
@@ -68,11 +61,11 @@ import { RemoteWorkspacesDropdown } from '../components/dropdowns/remote-workspa
|
||||
import { ErrorBoundary } from '../components/error-boundary';
|
||||
import { showAlert, showPrompt } from '../components/modals';
|
||||
import { GitRepositoryCloneModal } from '../components/modals/git-repository-settings-modal/git-repo-clone-modal';
|
||||
import { ImportModal } from '../components/modals/import-modal';
|
||||
import { EmptyStatePane } from '../components/panes/project-empty-state-pane';
|
||||
import { SidebarLayout } from '../components/sidebar-layout';
|
||||
import { Button } from '../components/themed-button/button';
|
||||
import { WorkspaceCard } from '../components/workspace-card';
|
||||
import { importClipBoard, importFile, importUri } from '../import';
|
||||
import { RootLoaderData } from './root';
|
||||
|
||||
const StyledDropdownButton = styled(DropdownButton).attrs({
|
||||
@@ -308,21 +301,23 @@ const OrganizationProjectsSidebar: FC<{
|
||||
</SidebarTitle>
|
||||
<SidebarSection>
|
||||
<SidebarSectionTitle>Projects ({projects.length})</SidebarSectionTitle>
|
||||
{shouldShowSearch && <Button
|
||||
style={{
|
||||
padding: 'var(--padding-sm)',
|
||||
minWidth: 'auto',
|
||||
width: 'unset',
|
||||
flex: 0,
|
||||
}}
|
||||
variant="text"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setSearchOpen(!isSearchOpen);
|
||||
}}
|
||||
>
|
||||
<i data-testid="SearchProjectsButton" className="fa fa-search" />
|
||||
</Button>}
|
||||
{shouldShowSearch && (
|
||||
<Button
|
||||
style={{
|
||||
padding: 'var(--padding-sm)',
|
||||
minWidth: 'auto',
|
||||
width: 'unset',
|
||||
flex: 0,
|
||||
}}
|
||||
variant="text"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setSearchOpen(!isSearchOpen);
|
||||
}}
|
||||
>
|
||||
<i data-testid="SearchProjectsButton" className="fa fa-search" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
style={{
|
||||
padding: 'var(--padding-sm)',
|
||||
@@ -390,9 +385,7 @@ const OrganizationProjectsSidebar: FC<{
|
||||
</SearchFormControl>
|
||||
</SearchFormContainer>
|
||||
)}
|
||||
<ProjectListContainer
|
||||
className="sidebar__list sidebar__list-root theme--sidebar__list"
|
||||
>
|
||||
<ProjectListContainer className="sidebar__list sidebar__list-root theme--sidebar__list">
|
||||
{projects.map(proj => {
|
||||
return (
|
||||
<li key={proj._id} className="sidebar__row">
|
||||
@@ -409,12 +402,10 @@ const OrganizationProjectsSidebar: FC<{
|
||||
gap: 'var(--padding-sm)',
|
||||
}}
|
||||
onClick={() =>
|
||||
navigate(
|
||||
{
|
||||
pathname: `/organization/${organizationId}/project/${proj._id}`,
|
||||
search: searchParams.toString(),
|
||||
},
|
||||
)
|
||||
navigate({
|
||||
pathname: `/organization/${organizationId}/project/${proj._id}`,
|
||||
search: searchParams.toString(),
|
||||
})
|
||||
}
|
||||
className="wide"
|
||||
>
|
||||
@@ -510,7 +501,7 @@ const OrganizationProjectsSidebar: FC<{
|
||||
|
||||
<List
|
||||
onAction={key => {
|
||||
clickLink(key);
|
||||
clickLink(key.toString());
|
||||
}}
|
||||
>
|
||||
<Item
|
||||
@@ -588,6 +579,7 @@ const SidebarListItemTitle = ({ icon, label }: SidebarListItemTitleProps) => {
|
||||
};
|
||||
|
||||
interface WorkspaceWithMetadata {
|
||||
_id: string;
|
||||
hasUnsavedChanges: boolean;
|
||||
lastModifiedTimestamp: number;
|
||||
modifiedLocally: number;
|
||||
@@ -623,7 +615,7 @@ export const indexLoader: LoaderFunction = async ({ params }) => {
|
||||
return;
|
||||
};
|
||||
|
||||
interface LoaderData {
|
||||
export interface ProjectLoaderData {
|
||||
workspaces: WorkspaceWithMetadata[];
|
||||
allFilesCount: number;
|
||||
documentsCount: number;
|
||||
@@ -636,7 +628,7 @@ interface LoaderData {
|
||||
export const loader: LoaderFunction = async ({
|
||||
params,
|
||||
request,
|
||||
}): Promise<LoaderData> => {
|
||||
}): Promise<ProjectLoaderData> => {
|
||||
const search = new URL(request.url).searchParams;
|
||||
const { projectId, organizationId } = params;
|
||||
invariant(organizationId, 'Organization ID is required');
|
||||
@@ -704,6 +696,7 @@ export const loader: LoaderFunction = async ({
|
||||
);
|
||||
|
||||
return {
|
||||
_id: workspace._id,
|
||||
hasUnsavedChanges,
|
||||
lastModifiedTimestamp,
|
||||
modifiedLocally,
|
||||
@@ -795,7 +788,9 @@ export const loader: LoaderFunction = async ({
|
||||
? allProjects.filter(proj => !isRemoteProject(proj))
|
||||
: [project];
|
||||
|
||||
const projects = sortProjects(organizationProjects).filter(p => p.name.toLowerCase().includes(projectName.toLowerCase()));
|
||||
const projects = sortProjects(organizationProjects).filter(p =>
|
||||
p.name.toLowerCase().includes(projectName.toLowerCase())
|
||||
);
|
||||
|
||||
return {
|
||||
organization:
|
||||
@@ -827,20 +822,21 @@ const ProjectRoute: FC = () => {
|
||||
allFilesCount,
|
||||
collectionsCount,
|
||||
documentsCount,
|
||||
} = useLoaderData() as LoaderData;
|
||||
} = useLoaderData() as ProjectLoaderData;
|
||||
const { organizationId } = useParams() as { organizationId: string };
|
||||
const [searchParams] = useSearchParams();
|
||||
const [isGitRepositoryCloneModalOpen, setIsGitRepositoryCloneModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const fetcher = useFetcher();
|
||||
const { revalidate } = useRevalidator();
|
||||
const submit = useSubmit();
|
||||
const navigate = useNavigate();
|
||||
const filter = searchParams.get('filter') || '';
|
||||
const sortOrder =
|
||||
(searchParams.get('sortOrder') as DashboardSortOrder) || 'modified-desc';
|
||||
|
||||
const [isImportModalOpen, setIsImportModalOpen] = useState<
|
||||
'uri' | 'file' | 'clipboard' | false
|
||||
>(false);
|
||||
const createNewCollection = () => {
|
||||
showPrompt({
|
||||
title: 'Create New Request Collection',
|
||||
@@ -885,44 +881,6 @@ const ProjectRoute: FC = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const importFromURL = useCallback(() => {
|
||||
showPrompt({
|
||||
title: 'Import document from URL',
|
||||
submitName: 'Fetch and Import',
|
||||
label: 'URL',
|
||||
placeholder: 'https://website.com/insomnia-import.json',
|
||||
onComplete: uri => {
|
||||
importUri(uri, {
|
||||
activeProjectWorkspaces: workspaces.map(w => w.workspace),
|
||||
activeProject,
|
||||
projects,
|
||||
forceToWorkspace: ForceToWorkspace.existing,
|
||||
onComplete: revalidate,
|
||||
});
|
||||
},
|
||||
});
|
||||
}, [activeProject, projects, revalidate, workspaces]);
|
||||
|
||||
const importFromClipboard = useCallback(() => {
|
||||
importClipBoard({
|
||||
activeProjectWorkspaces: workspaces.map(w => w.workspace),
|
||||
activeProject,
|
||||
projects,
|
||||
forceToWorkspace: ForceToWorkspace.existing,
|
||||
onComplete: revalidate,
|
||||
});
|
||||
}, [activeProject, projects, revalidate, workspaces]);
|
||||
|
||||
const importFromFile = useCallback(() => {
|
||||
importFile({
|
||||
activeProjectWorkspaces: workspaces.map(w => w.workspace),
|
||||
activeProject,
|
||||
projects,
|
||||
forceToWorkspace: ForceToWorkspace.existing,
|
||||
onComplete: revalidate,
|
||||
});
|
||||
}, [activeProject, projects, revalidate, workspaces]);
|
||||
|
||||
const importFromGit = () => {
|
||||
setIsGitRepositoryCloneModalOpen(true);
|
||||
};
|
||||
@@ -949,10 +907,14 @@ const ProjectRoute: FC = () => {
|
||||
}
|
||||
renderPaneOne={
|
||||
<Pane
|
||||
style={!hasWorkspaces ? {
|
||||
gridTemplateRows: 'auto 1fr',
|
||||
gridTemplateColumns: '1fr',
|
||||
} : undefined}
|
||||
style={
|
||||
!hasWorkspaces
|
||||
? {
|
||||
gridTemplateRows: 'auto 1fr',
|
||||
gridTemplateColumns: '1fr',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<PaneHeaderToolbar>
|
||||
<SearchFormControl className="form-control form-control--outlined no-margin">
|
||||
@@ -1026,27 +988,13 @@ const ProjectRoute: FC = () => {
|
||||
</DropdownSection>
|
||||
<DropdownSection
|
||||
aria-label="Import From"
|
||||
title="Import From"
|
||||
title="Import"
|
||||
>
|
||||
<DropdownItem aria-label="File">
|
||||
<DropdownItem aria-label="Import">
|
||||
<ItemContent
|
||||
icon="plus"
|
||||
label="File"
|
||||
onClick={importFromFile}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label="URL">
|
||||
<ItemContent
|
||||
icon="link"
|
||||
label="URL"
|
||||
onClick={importFromURL}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label="Clipboard">
|
||||
<ItemContent
|
||||
icon="clipboard"
|
||||
label="Clipboard"
|
||||
onClick={importFromClipboard}
|
||||
icon="file-import"
|
||||
label="Import"
|
||||
onClick={() => setIsImportModalOpen('file')}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label="Git Clone">
|
||||
@@ -1059,28 +1007,28 @@ const ProjectRoute: FC = () => {
|
||||
</DropdownSection>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
</PaneHeaderToolbar>
|
||||
{hasWorkspaces && workspaces.map(workspace => (
|
||||
<WorkspaceCard
|
||||
{...workspace}
|
||||
projects={projects}
|
||||
key={workspace.apiSpec._id}
|
||||
activeProject={activeProject}
|
||||
onSelect={() =>
|
||||
navigate(
|
||||
`/organization/${organizationId}/project/${
|
||||
activeProject._id
|
||||
}/workspace/${workspace.workspace._id}/${
|
||||
workspace.workspace.scope === 'design'
|
||||
? ACTIVITY_SPEC
|
||||
: ACTIVITY_DEBUG
|
||||
}`
|
||||
)
|
||||
}
|
||||
filter={filter}
|
||||
/>
|
||||
))}
|
||||
{hasWorkspaces &&
|
||||
workspaces.map(workspace => (
|
||||
<WorkspaceCard
|
||||
{...workspace}
|
||||
projects={projects}
|
||||
key={workspace.apiSpec._id}
|
||||
activeProject={activeProject}
|
||||
onSelect={() =>
|
||||
navigate(
|
||||
`/organization/${organizationId}/project/${
|
||||
activeProject._id
|
||||
}/workspace/${workspace.workspace._id}/${
|
||||
workspace.workspace.scope === 'design'
|
||||
? ACTIVITY_SPEC
|
||||
: ACTIVITY_DEBUG
|
||||
}`
|
||||
)
|
||||
}
|
||||
filter={filter}
|
||||
/>
|
||||
))}
|
||||
{filter && !hasWorkspaces && (
|
||||
<div>
|
||||
<p className="notice subtle">
|
||||
@@ -1092,10 +1040,8 @@ const ProjectRoute: FC = () => {
|
||||
<EmptyStatePane
|
||||
createRequestCollection={createNewCollection}
|
||||
createDesignDocument={createNewDocument}
|
||||
importFromFile={importFromFile}
|
||||
importFromURL={importFromURL}
|
||||
importFromClipboard={importFromClipboard}
|
||||
importFromGit={importFromGit}
|
||||
importFrom={() => setIsImportModalOpen('file')}
|
||||
cloneFromGit={importFromGit}
|
||||
/>
|
||||
)}
|
||||
</Pane>
|
||||
@@ -1106,6 +1052,14 @@ const ProjectRoute: FC = () => {
|
||||
onHide={() => setIsGitRepositoryCloneModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{isImportModalOpen && (
|
||||
<ImportModal
|
||||
onHide={() => setIsImportModalOpen(false)}
|
||||
from={isImportModalOpen}
|
||||
organizationId={organizationId}
|
||||
defaultProjectId={activeProject._id}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
@@ -67,9 +67,7 @@ const Layout = styled.div({
|
||||
const Root = () => {
|
||||
const { revalidate } = useRevalidator();
|
||||
const workspaceData = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData | null;
|
||||
console.log({
|
||||
workspaceData,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onLoginLogout(() => {
|
||||
revalidate();
|
||||
|
||||
Reference in new issue
Block a user