Ensure a base space always exists (+ fix gRPC sub-environments) (#3819)

This commit is contained in:
Opender Singh authored and GitHub committed 2021-07-26 00:49:34 +00:00
1 parent eb67958846
commit 7abde2a017
49 files changed
+1551 -1607

No files matched your search

@@ -1,4 +1,5 @@
import { ACTIVITY_DEBUG } from '../common/constants';
import { BASE_SPACE_ID } from '../models/space';
import { RootState } from '../ui/redux/modules';
import * as entities from '../ui/redux/modules/entities';
import { GlobalState } from '../ui/redux/modules/global';
@@ -8,7 +9,7 @@ export const reduxStateForTest = async (global: Partial<GlobalState> = {}): Prom
global: {
activeWorkspaceId: null,
activeActivity: ACTIVITY_DEBUG,
activeSpaceId: null,
activeSpaceId: BASE_SPACE_ID,
isLoading: false,
isLoggedIn: false,
loadingRequestIds: {},
@@ -263,8 +263,10 @@ describe('_repairDatabase()', () => {
it('fixes duplicate environments', async () => {
// Create Workspace with no children
const space = await models.space.create();
const workspace = await models.workspace.create({
_id: 'w1',
parentId: space._id,
});
const spec = await models.apiSpec.getByParentId(workspace._id);
expect((await db.withDescendants(workspace)).length).toBe(2);
@@ -326,7 +328,7 @@ describe('_repairDatabase()', () => {
{
_id: 'w1',
data: null,
parentId: null,
parentId: workspace.parentId,
},
{
_id: 'b1',
@@ -392,7 +394,7 @@ describe('_repairDatabase()', () => {
{
_id: 'w1',
data: null,
parentId: null,
parentId: workspace.parentId,
},
{
_id: 'b1',
@@ -442,8 +444,10 @@ describe('_repairDatabase()', () => {
it('fixes duplicate cookie jars', async () => {
// Create Workspace with no children
const space = await models.space.create();
const workspace = await models.workspace.create({
_id: 'w1',
parentId: space._id,
});
const spec = await models.apiSpec.getByParentId(workspace._id);
expect((await db.withDescendants(workspace)).length).toBe(2);
@@ -496,7 +500,7 @@ describe('_repairDatabase()', () => {
{
_id: 'w1',
cookies: null,
parentId: null,
parentId: workspace.parentId,
},
{
_id: 'j1',
@@ -548,7 +552,7 @@ describe('_repairDatabase()', () => {
{
_id: 'w1',
cookies: null,
parentId: null,
parentId: workspace.parentId,
},
{
_id: 'j1',
@@ -713,7 +717,10 @@ describe('withAncestors()', () => {
beforeEach(globalBeforeEach);
it('should return itself and all parents but exclude siblings', async () => {
const wrk = await models.workspace.create();
const spc = await models.space.create();
const wrk = await models.workspace.create({
parentId: spc._id,
});
const wrkReq = await models.request.create({
parentId: wrk._id,
});
@@ -730,14 +737,14 @@ describe('withAncestors()', () => {
parentId: grp._id,
});
// Workspace child searching for ancestors
await expect(db.withAncestors(wrk)).resolves.toStrictEqual([wrk]);
await expect(db.withAncestors(wrkReq)).resolves.toStrictEqual([wrkReq, wrk]);
await expect(db.withAncestors(wrkGrpcReq)).resolves.toStrictEqual([wrkGrpcReq, wrk]);
await expect(db.withAncestors(wrk)).resolves.toStrictEqual([wrk, spc]);
await expect(db.withAncestors(wrkReq)).resolves.toStrictEqual([wrkReq, wrk, spc]);
await expect(db.withAncestors(wrkGrpcReq)).resolves.toStrictEqual([wrkGrpcReq, wrk, spc]);
// Group searching for ancestors
await expect(db.withAncestors(grp)).resolves.toStrictEqual([grp, wrk]);
await expect(db.withAncestors(grp)).resolves.toStrictEqual([grp, wrk, spc]);
// Group child searching for ancestors
await expect(db.withAncestors(grpReq)).resolves.toStrictEqual([grpReq, grp, wrk]);
await expect(db.withAncestors(grpGrpcReq)).resolves.toStrictEqual([grpGrpcReq, grp, wrk]);
await expect(db.withAncestors(grpReq)).resolves.toStrictEqual([grpReq, grp, wrk, spc]);
await expect(db.withAncestors(grpGrpcReq)).resolves.toStrictEqual([grpGrpcReq, grp, wrk, spc]);
// Group child searching for ancestors with filters
await expect(db.withAncestors(grpGrpcReq, [models.requestGroup.type])).resolves.toStrictEqual([
grpGrpcReq,
@@ -6,7 +6,10 @@ import { getAppVersion } from '../constants';
import { exportRequestsData, exportRequestsHAR, exportWorkspacesData, exportWorkspacesHAR } from '../export';
describe('exportWorkspacesHAR() and exportRequestsHAR()', () => {
beforeEach(globalBeforeEach);
beforeEach(async () => {
await globalBeforeEach();
await models.space.all();
});
it('exports a single workspace and some requests only as an HTTP Archive', async () => {
const wrk1 = await models.workspace.create({
@@ -172,7 +175,7 @@ describe('exportWorkspacesHAR() and exportRequestsHAR()', () => {
activeEnvironmentId: env2Private._id,
});
const includePrivateDocs = false;
const json = await exportWorkspacesHAR([], includePrivateDocs);
const json = await exportWorkspacesHAR([wrk1, wrk2], includePrivateDocs);
const data = JSON.parse(json);
expect(data).toMatchObject({
log: {
@@ -260,8 +263,8 @@ describe('export', () => {
parentId: eBase._id,
});
// Test export whole workspace.
const exportedWorkspacesJson = await exportWorkspacesData([], false, 'json');
const exportedWorkspacesYaml = await exportWorkspacesData([], false, 'yaml');
const exportedWorkspacesJson = await exportWorkspacesData([w], false, 'json');
const exportedWorkspacesYaml = await exportWorkspacesData([w], false, 'yaml');
const exportWorkspacesDataJson = JSON.parse(exportedWorkspacesJson);
const exportWorkspacesDataYaml = YAML.parse(exportedWorkspacesYaml);
// Ensure JSON is the same as YAML
@@ -276,7 +279,7 @@ describe('export', () => {
_id: w._id,
}),
expect.objectContaining({
_id: spec._id,
_id: spec?._id,
}),
expect.objectContaining({
_id: eBase._id,
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -549,7 +549,7 @@ export const database = {
let docsToReturn: T[] = doc ? [doc] : [];
async function next(docs: T[]) {
async function next(docs: T[]): Promise<T[]> {
const foundDocs: T[] = [];
for (const d of docs) {
+2 -6
View File
@@ -48,9 +48,7 @@ export async function exportWorkspacesHAR(
workspaces: Workspace[],
includePrivateDocs = false,
) {
// regarding `[null]`, see the comment here in `exportWorkspacesData`
const rootDocs = workspaces.length === 0 ? [null] : workspaces;
const promises = rootDocs.map(getDocWithDescendants(includePrivateDocs));
const promises = workspaces.map(getDocWithDescendants(includePrivateDocs));
const docs = (await Promise.all(promises)).flat();
const requests = docs.filter(isRequest);
return exportRequestsHAR(requests, includePrivateDocs);
@@ -124,9 +122,7 @@ export async function exportWorkspacesData(
includePrivateDocs: boolean,
format: 'json' | 'yaml',
) {
// Semantically, if an empty array is passed, then nothing will be returned. What an empty array really signifies is "no parent", which, at the database layer is the same as "parentId === null", hence we add null in ourselves.
const rootDocs = workspaces.length === 0 ? [null] : workspaces;
const promises = rootDocs.map(getDocWithDescendants(includePrivateDocs));
const promises = workspaces.map(getDocWithDescendants(includePrivateDocs));
const docs = (await Promise.all(promises)).flat();
const requests = docs.filter(doc => isRequest(doc) || isGrpcRequest(doc));
return exportRequestsData(requests, includePrivateDocs, format);
+3 -3
View File
@@ -186,7 +186,7 @@ export async function exportHar(exportRequests: ExportRequest[]) {
continue;
}
const harRequest = await exportHarWithRequest(request, exportRequest.environmentId);
const harRequest = await exportHarWithRequest(request, exportRequest.environmentId || undefined);
if (!harRequest) {
continue;
@@ -283,11 +283,11 @@ export async function exportHarRequest(
export async function exportHarWithRequest(
request: Request,
environmentId: string | null,
environmentId?: string,
addContentLength = false,
) {
try {
const renderResult = await getRenderedRequestAndContext(request, environmentId);
const renderResult = await getRenderedRequestAndContext({ request, environmentId });
const renderedRequest = await _applyRequestPluginHooks(
renderResult.request,
renderResult.context,
+2 -3
View File
@@ -40,7 +40,7 @@ interface ConvertResult {
export interface ImportRawConfig {
getWorkspaceId: ImportToWorkspacePrompt;
getSpaceId?: () => Promise<string | null>;
getSpaceId?: () => Promise<string>;
getWorkspaceScope?: SetWorkspaceScopePrompt;
enableDiffBasedPatching?: boolean;
enableDiffDeep?: boolean;
@@ -336,11 +336,10 @@ async function updateWorkspaceScope(
async function createWorkspaceInSpace(
resource: Workspace,
getSpaceId?: () => Promise<string | null>,
getSpaceId?: () => Promise<string>,
) {
if (getSpaceId) {
// Set the workspace parent if creating a new workspace during import
// @ts-expect-error workspace parent can be null or string
resource.parentId = await getSpaceId();
}
}
+72 -53
View File
@@ -6,10 +6,10 @@ import * as models from '../models';
import type { CookieJar } from '../models/cookie-jar';
import type { Environment } from '../models/environment';
import type { GrpcRequest, GrpcRequestBody } from '../models/grpc-request';
import type { BaseModel } from '../models/index';
import type { Request } from '../models/request';
import { isRequestGroup } from '../models/request-group';
import { isWorkspace } from '../models/workspace';
import { isRequestGroup, RequestGroup } from '../models/request-group';
import { isSpace, Space } from '../models/space';
import { isWorkspace, Workspace } from '../models/workspace';
import * as templating from '../templating';
import * as templatingUtils from '../templating/utils';
import { CONTENT_TYPE_GRAPHQL, JSON_ORDER_SEPARATOR } from './constants';
@@ -54,10 +54,17 @@ export type HandleGetRenderContext = () => Promise<RenderContextAndKeys>;
export type HandleRender = <T>(object: T, contextCacheKey?: string | null) => Promise<T>;
export async function buildRenderContext(
ancestors: BaseModel[] | null,
rootEnvironment: Environment | null,
subEnvironment: Environment | null,
baseContext: Record<string, any> = {},
{
ancestors,
rootEnvironment,
subEnvironment,
baseContext = {},
}: {
ancestors?: RenderContextAncestor[];
rootEnvironment?: Environment;
subEnvironment?: Environment;
baseContext?: Record<string, any>;
},
) {
const envObjects: Record<string, any>[] = [];
@@ -280,17 +287,31 @@ export async function render<T>(
return next<T>(newObj, name, true);
}
export async function getRenderContext(
request: Request | GrpcRequest | null,
environmentId: string | null,
ancestors: BaseModel[] | null = null,
purpose: RenderPurpose | null = null,
extraInfo: ExtraRenderInfo | null = null,
): Promise<Record<string, any>> {
if (!ancestors) {
ancestors = await _getRequestAncestors(request);
}
interface RenderRequest<T extends Request | GrpcRequest> {
request: T;
}
interface BaseRenderContextOptions {
environmentId?: string;
purpose?: RenderPurpose;
extraInfo?: ExtraRenderInfo;
}
interface RenderContextOptions extends BaseRenderContextOptions, Partial<RenderRequest<Request | GrpcRequest>> {
ancestors?: RenderContextAncestor[];
}
export async function getRenderContext(
{
request,
environmentId,
ancestors: _ancestors,
purpose,
extraInfo,
}: RenderContextOptions,
): Promise<Record<string, any>> {
const ancestors = _ancestors || await getRenderContextAncestors(request);
const space = ancestors.find(isSpace);
const workspace = ancestors.find(isWorkspace);
if (!workspace) {
@@ -373,24 +394,26 @@ export async function getRenderContext(
baseContext.getEnvironmentId = () => environmentId;
// It is possible for a space to not exist because this code path can be reached via Inso/insomnia-send-request which has no concept of a space.
baseContext.getSpaceId = () => space?._id;
// Generate the context we need to render
return buildRenderContext(ancestors, rootEnvironment, subEnvironment, baseContext);
return buildRenderContext({ ancestors, rootEnvironment, subEnvironment: subEnvironment || undefined, baseContext });
}
interface RenderGrpcRequestOptions extends BaseRenderContextOptions, RenderRequest<GrpcRequest> {
skipBody?: boolean;
}
export async function getRenderedGrpcRequest(
request: GrpcRequest,
environmentId: string | null,
purpose?: RenderPurpose,
extraInfo?: ExtraRenderInfo,
skipBody?: boolean,
) {
const renderContext = await getRenderContext(
{
purpose,
extraInfo,
request,
environmentId,
null,
purpose,
extraInfo || null,
);
skipBody,
}: RenderGrpcRequestOptions,
) {
const renderContext = await getRenderContext({ request, environmentId, purpose, extraInfo });
const description = request.description;
// Render description separately because it's lower priority
request.description = '';
@@ -406,41 +429,35 @@ export async function getRenderedGrpcRequest(
return renderedRequest;
}
type RenderGrpcRequestMessageOptions = BaseRenderContextOptions & RenderRequest<GrpcRequest>;
export async function getRenderedGrpcRequestMessage(
request: GrpcRequest,
environmentId: string | null,
purpose?: RenderPurpose,
extraInfo?: ExtraRenderInfo,
) {
const renderContext = await getRenderContext(
request,
{
environmentId,
null,
request,
extraInfo,
purpose,
extraInfo || null,
);
}: RenderGrpcRequestMessageOptions,
) {
const renderContext = await getRenderContext({ request, environmentId, purpose, extraInfo });
// Render request body
const renderedBody: RenderedGrpcRequestBody = await render(request.body, renderContext);
return renderedBody;
}
type RenderRequestOptions = BaseRenderContextOptions & RenderRequest<Request>;
export async function getRenderedRequestAndContext(
request: Request,
environmentId?: string | null,
purpose?: RenderPurpose,
extraInfo?: ExtraRenderInfo,
{
request,
environmentId,
extraInfo,
purpose,
}: RenderRequestOptions,
) {
const ancestors = await _getRequestAncestors(request);
const ancestors = await getRenderContextAncestors(request);
const workspace = ancestors.find(isWorkspace);
const parentId = workspace ? workspace._id : 'n/a';
const cookieJar = await models.cookieJar.getOrCreateForParentId(parentId);
const renderContext = await getRenderContext(
request,
environmentId || null,
ancestors,
purpose,
extraInfo || null,
);
const renderContext = await getRenderContext({ request, environmentId, ancestors, purpose, extraInfo });
// HACK: Switch '#}' to '# }' to prevent Nunjucks from barfing
// https://github.com/kong/insomnia/issues/895
@@ -539,11 +556,13 @@ function _getOrderedEnvironmentKeys(finalRenderContext: Record<string, any>): st
});
}
async function _getRequestAncestors(request: Request | GrpcRequest | null): Promise<BaseModel[]> {
return await db.withAncestors(request, [
type RenderContextAncestor = Request | GrpcRequest | RequestGroup | Workspace | Space;
export async function getRenderContextAncestors(base?: Request | GrpcRequest | Workspace): Promise<RenderContextAncestor[]> {
return await db.withAncestors<RenderContextAncestor>(base || null, [
models.request.type,
models.grpcRequest.type,
models.requestGroup.type,
models.workspace.type,
models.space.type,
]);
}
@@ -1,7 +1,8 @@
import { Schema } from '@develohpanda/fluent-builder';
import clone from 'clone';
import { BaseModel, gitRepository, grpcRequest, request, requestGroup, workspace } from '..';
import { BaseModel, environment, gitRepository, grpcRequest, request, requestGroup, workspace } from '..';
import { Environment } from '../environment';
import { GitRepository } from '../git-repository';
import { GrpcRequest } from '../grpc-request';
import { Request } from '../request';
@@ -64,3 +65,9 @@ export const gitRepositorySchema: Schema<GitRepository> = {
needsFullClone: () => false,
uriNeedsMigration: () => true,
};
export const environmentModelSchema: Schema<Environment> = {
...baseModelSchema,
...toSchema(environment.init()),
type: () => environment.type,
};
+16 -10
View File
@@ -1,3 +1,4 @@
import { getAppName } from '../common/constants';
import { database as db } from '../common/database';
import { generateId } from '../common/misc';
import type { BaseModel } from './index';
@@ -8,15 +9,13 @@ export const prefix = 'sp';
export const canDuplicate = false;
export const canSync = false;
// A base space represents "no" space, when viewing this base space fetch all workspaces with no parent.
// Using this instead of null.
export const BASE_SPACE_ID = 'base-space';
export const BASE_SPACE_ID = `${prefix}_base-space`;
export const isBaseSpace = ({ _id }: Pick<Space, '_id'>) => _id === BASE_SPACE_ID;
export const isNotBaseSpace = (space: Pick<Space, '_id'>) => !isBaseSpace(space);
export const isLocalSpace = ({ remoteId }: Pick<Space, 'remoteId'>) => remoteId === null;
export const isRemoteSpace = (space: Pick<Space, 'remoteId'>) => !isLocalSpace(space);
export const spaceHasSettings = (space: Pick<Space, 'remoteId' | '_id'>) => isLocalSpace(space) && !isBaseSpace(space);
export const isBaseSpace = ({ _id }: Space) => _id === BASE_SPACE_ID;
export const isNotBaseSpace = (space: Space) => !isBaseSpace(space);
export const isLocalSpace = ({ remoteId }: Space) => remoteId === null;
export const isRemoteSpace = (space: Space) => !isLocalSpace(space);
export const spaceHasSettings = (space: Space) => isLocalSpace(space) && !isBaseSpace(space);
interface BaseSpace {
name: string;
@@ -64,6 +63,13 @@ export function update(space: Space, patch: Partial<Space>) {
return db.docUpdate(space, patch);
}
export function all() {
return db.all<Space>(type);
export async function all() {
const spaces = await db.all<Space>(type);
if (!spaces.find(c => c._id === BASE_SPACE_ID)) {
await create({ _id: BASE_SPACE_ID, name: getAppName(), remoteId: null });
return db.all<Space>(type);
}
return spaces;
}
+14 -1
View File
@@ -4,7 +4,7 @@ import { database as db } from '../common/database';
import { strings } from '../common/strings';
import type { BaseModel } from './index';
import * as models from './index';
import { isSpaceId } from './space';
import { BASE_SPACE_ID, isSpaceId } from './space';
export const name = 'Workspace';
export const type = 'Workspace';
@@ -57,6 +57,7 @@ export async function migrate(doc: Workspace) {
fileName: doc.name,
});
doc = _migrateScope(doc);
doc = _migrateIntoBaseSpace(doc);
return doc;
}
@@ -64,6 +65,10 @@ export function getById(id?: string) {
return db.get<Workspace>(type, id);
}
export function findByParentId(parentId: string) {
return db.find<Workspace>(type, { parentId });
}
export async function create(patch: Partial<Workspace> = {}) {
expectParentToBeSpace(patch.parentId);
return db.docCreate<Workspace>(type, patch);
@@ -153,6 +158,14 @@ function _migrateScope(workspace: MigrationWorkspace) {
return workspace as Workspace;
}
function _migrateIntoBaseSpace(workspace: Workspace) {
if (!workspace.parentId) {
workspace.parentId = BASE_SPACE_ID;
}
return workspace;
}
export async function ensureChildren({ _id }: Workspace) {
await models.environment.getOrCreateForParentId(_id);
await models.cookieJar.getOrCreateForParentId(_id);
@@ -19,10 +19,13 @@ import { DEFAULT_BOUNDARY } from '../multipart';
import * as networkUtils from '../network';
const CONTEXT = {};
const getRenderedRequest = async (...args) => (await getRenderedRequestAndContext(...args)).request;
const getRenderedRequest = async (args: Parameters<typeof getRenderedRequestAndContext>[0]) => (await getRenderedRequestAndContext(args)).request;
describe('actuallySend()', () => {
beforeEach(globalBeforeEach);
beforeEach(async () => {
await globalBeforeEach();
await models.space.all();
});
it('sends a generic request', async () => {
const workspace = await models.workspace.create();
@@ -90,7 +93,7 @@ describe('actuallySend()', () => {
password: 'pass',
},
});
const renderedRequest = await getRenderedRequest(request);
const renderedRequest = await getRenderedRequest({ request });
const response = await networkUtils._actuallySend(
renderedRequest,
CONTEXT,
@@ -165,7 +168,7 @@ describe('actuallySend()', () => {
},
url: 'http://localhost',
});
const renderedRequest = await getRenderedRequest(request);
const renderedRequest = await getRenderedRequest({ request });
const response = await networkUtils._actuallySend(
renderedRequest,
CONTEXT,
@@ -265,7 +268,7 @@ describe('actuallySend()', () => {
settingStoreCookies: false,
settingSendCookies: false,
});
const renderedRequest = await getRenderedRequest(request);
const renderedRequest = await getRenderedRequest({ request });
const response = await networkUtils._actuallySend(
renderedRequest,
CONTEXT,
@@ -325,7 +328,7 @@ describe('actuallySend()', () => {
fileName,
},
});
const renderedRequest = await getRenderedRequest(request);
const renderedRequest = await getRenderedRequest({ request });
const response = await networkUtils._actuallySend(
renderedRequest,
CONTEXT,
@@ -405,7 +408,7 @@ describe('actuallySend()', () => {
],
},
});
const renderedRequest = await getRenderedRequest(request);
const renderedRequest = await getRenderedRequest({ request });
const response = await networkUtils._actuallySend(
renderedRequest,
CONTEXT,
@@ -466,7 +469,7 @@ describe('actuallySend()', () => {
url: 'http://unix:/my/socket:/my/path',
method: 'GET',
});
const renderedRequest = await getRenderedRequest(request);
const renderedRequest = await getRenderedRequest({ request });
const response = await networkUtils._actuallySend(
renderedRequest,
CONTEXT,
@@ -506,7 +509,7 @@ describe('actuallySend()', () => {
url: 'http://localhost:3000/foo/bar',
method: 'HEAD',
});
const renderedRequest = await getRenderedRequest(request);
const renderedRequest = await getRenderedRequest({ request });
const response = await networkUtils._actuallySend(
renderedRequest,
CONTEXT,
@@ -545,7 +548,7 @@ describe('actuallySend()', () => {
url: 'http://unix:3000/my/path',
method: 'GET',
});
const renderedRequest = await getRenderedRequest(request);
const renderedRequest = await getRenderedRequest({ request });
const response = await networkUtils._actuallySend(
renderedRequest,
CONTEXT,
@@ -585,7 +588,7 @@ describe('actuallySend()', () => {
type: AUTH_NETRC,
},
});
const renderedRequest = await getRenderedRequest(request);
const renderedRequest = await getRenderedRequest({ request });
const response = await networkUtils._actuallySend(
renderedRequest,
CONTEXT,
@@ -623,7 +626,7 @@ describe('actuallySend()', () => {
_id: 'req_123',
parentId: workspace._id,
});
const renderedRequest = await getRenderedRequest(request);
const renderedRequest = await getRenderedRequest({ request });
const responseV1 = await networkUtils._actuallySend(renderedRequest, CONTEXT, workspace, {
...settings,
preferredHttpVersion: HttpVersions.V1_0,
@@ -674,8 +677,8 @@ describe('actuallySend()', () => {
url: 'http://unix:3000/requestB',
method: 'GET',
});
const renderedRequest1 = await getRenderedRequest(request1);
const renderedRequest2 = await getRenderedRequest(request2);
const renderedRequest1 = await getRenderedRequest({ request: request1 });
const renderedRequest2 = await getRenderedRequest({ request: request2 });
// WHEN
const response1Promise = networkUtils._actuallySend(
@@ -1,7 +1,9 @@
import { mocked } from 'ts-jest/utils';
import { globalBeforeEach } from '../../../__jest__/before-each';
import {
getRenderedGrpcRequest,
getRenderedGrpcRequestMessage,
getRenderedGrpcRequest as _getRenderedGrpcRequest,
getRenderedGrpcRequestMessage as _getRenderedGrpcRequestMessage,
RENDER_PURPOSE_SEND,
} from '../../../common/render';
import * as models from '../../../models';
@@ -10,6 +12,9 @@ import { prepareGrpcMessage, prepareGrpcRequest } from '../prepare';
jest.mock('../../../common/render');
const getRenderedGrpcRequest = mocked(_getRenderedGrpcRequest);
const getRenderedGrpcRequestMessage = mocked(_getRenderedGrpcRequestMessage);
describe('prepareGrpcRequest', () => {
beforeEach(globalBeforeEach);
@@ -26,11 +31,12 @@ describe('prepareGrpcRequest', () => {
getRenderedGrpcRequest.mockResolvedValue(gr);
const result = await prepareGrpcRequest(gr._id, env._id, methodType);
expect(getRenderedGrpcRequest).toHaveBeenLastCalledWith(
gr,
env,
RENDER_PURPOSE_SEND,
{},
false,
{
request: gr,
environmentId: env._id,
purpose: RENDER_PURPOSE_SEND,
skipBody: false,
},
);
expect(result).toEqual({
request: gr,
@@ -51,11 +57,12 @@ describe('prepareGrpcRequest', () => {
getRenderedGrpcRequest.mockResolvedValue(gr);
const result = await prepareGrpcRequest(gr._id, env._id, methodType);
expect(getRenderedGrpcRequest).toHaveBeenLastCalledWith(
gr,
env,
RENDER_PURPOSE_SEND,
{},
true,
{
request: gr,
environmentId: env._id,
purpose: RENDER_PURPOSE_SEND,
skipBody: true,
},
);
expect(result).toEqual({
request: gr,
@@ -78,10 +85,11 @@ describe('prepareGrpcMessage', () => {
getRenderedGrpcRequestMessage.mockResolvedValue(gr.body);
const result = await prepareGrpcMessage(gr._id, env._id);
expect(getRenderedGrpcRequestMessage).toHaveBeenLastCalledWith(
gr,
env,
RENDER_PURPOSE_SEND,
{},
{
request: gr,
environmentId: env._id,
purpose: RENDER_PURPOSE_SEND,
},
);
expect(result).toEqual({
body: gr.body,
@@ -23,15 +23,16 @@ export const prepareGrpcRequest = async (
methodType: GrpcMethodType,
): Promise<GrpcIpcRequestParams> => {
const req = await models.grpcRequest.getById(requestId);
const environment = await models.environment.getById(environmentId || 'n/a');
const request = await getRenderedGrpcRequest(
// @ts-expect-error -- TSCONVERSION req can be null but should not try to render if it is null
req,
environment,
RENDER_PURPOSE_SEND,
{},
canClientStream(methodType),
{
// @ts-expect-error -- TSCONVERSION req can be null but should not try to render if it is null
request: req,
environmentId,
purpose: RENDER_PURPOSE_SEND,
skipBody: canClientStream(methodType),
},
);
return {
request,
};
@@ -42,13 +43,12 @@ export const prepareGrpcMessage = async (
environmentId: string,
): Promise<GrpcIpcMessageParams> => {
const req = await models.grpcRequest.getById(requestId);
const environment = await models.environment.getById(environmentId || 'n/a');
const requestBody = await getRenderedGrpcRequestMessage(
const requestBody = await getRenderedGrpcRequestMessage({
// @ts-expect-error -- TSCONVERSION req can be null but should not try to render if it is null
req,
environment,
RENDER_PURPOSE_SEND,
{},
request: req,
environmentId,
purpose: RENDER_PURPOSE_SEND,
},
);
return {
body: requestBody,
+9 -7
View File
@@ -895,7 +895,7 @@ export async function sendWithSettings(
};
try {
renderResult = await getRenderedRequestAndContext(newRequest, environmentId);
renderResult = await getRenderedRequestAndContext({ request: newRequest, environmentId });
} catch (err) {
throw new Error(`Failed to render request: ${requestId}`);
}
@@ -946,10 +946,12 @@ export async function send(
const environment: Environment | null = await models.environment.getById(environmentId || 'n/a');
const renderResult = await getRenderedRequestAndContext(
request,
environmentId || null,
RENDER_PURPOSE_SEND,
extraInfo,
{
request,
environmentId,
purpose: RENDER_PURPOSE_SEND,
extraInfo,
},
);
const renderedRequestBeforePlugins = renderResult.request;
const renderedContextBeforePlugins = renderResult.context;
@@ -1004,7 +1006,7 @@ async function _applyRequestPluginHooks(
for (const { plugin, hook } of await plugins.getRequestHooks()) {
const context = {
...(pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER) as Record<string, any>),
...pluginContexts.data.init(),
...pluginContexts.data.init(renderedContext.getSpaceId()),
...(pluginContexts.store.init(plugin) as Record<string, any>),
...(pluginContexts.request.init(newRenderedRequest, renderedContext) as Record<string, any>),
...(pluginContexts.network.init(renderedContext.getEnvironmentId()) as Record<string, any>),
@@ -1032,7 +1034,7 @@ async function _applyResponsePluginHooks(
for (const { plugin, hook } of await plugins.getResponseHooks()) {
const context = {
...(pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER) as Record<string, any>),
...pluginContexts.data.init(),
...pluginContexts.data.init(renderedContext.getSpaceId()),
...(pluginContexts.store.init(plugin) as Record<string, any>),
...(pluginContexts.response.init(newResponse) as Record<string, any>),
...(pluginContexts.request.init(newRequest, renderedContext, true) as Record<string, any>),
@@ -5,6 +5,7 @@ import { globalBeforeEach } from '../../../__jest__/before-each';
import { getAppVersion } from '../../../common/constants';
import { database as db } from '../../../common/database';
import * as models from '../../../models/index';
import { BASE_SPACE_ID, Space } from '../../../models/space';
import { WorkspaceScopeKeys } from '../../../models/workspace';
import * as plugin from '../data';
@@ -14,7 +15,7 @@ describe('init()', () => {
beforeEach(globalBeforeEach);
it('initializes correctly', async () => {
const { data } = plugin.init();
const { data } = plugin.init(BASE_SPACE_ID);
expect(Object.keys(data)).toEqual(['import', 'export']);
expect(Object.keys(data.export).sort()).toEqual(['har', 'insomnia']);
expect(Object.keys(data.import).sort()).toEqual(['raw', 'uri']);
@@ -22,12 +23,16 @@ describe('init()', () => {
});
describe('app.import.*', () => {
let space: Space;
beforeEach(async () => {
await globalBeforeEach();
space = await models.space.create();
await models.workspace.create({
_id: 'wrk_1',
created: 111,
modified: 222,
parentId: space._id,
});
});
@@ -35,7 +40,7 @@ describe('app.import.*', () => {
const workspace = await models.workspace.getById('wrk_1');
expect(await db.all(models.workspace.type)).toEqual([workspace]);
expect(await db.count(models.request.type)).toBe(0);
const { data } = plugin.init();
const { data } = plugin.init(space._id);
const filename = path.resolve(__dirname, '../__fixtures__/basic-import.json');
await data.import.uri(`file://${filename}`);
const allWorkspaces = await db.all(models.workspace.type);
@@ -47,7 +52,7 @@ describe('app.import.*', () => {
modified: 999,
description: '',
name: 'New',
parentId: null,
parentId: space._id,
type: 'Workspace',
scope: WorkspaceScopeKeys.collection,
},
@@ -83,7 +88,7 @@ describe('app.import.*', () => {
const workspace = await models.workspace.getById('wrk_1');
expect(await db.all(models.workspace.type)).toEqual([workspace]);
expect(await db.count(models.request.type)).toBe(0);
const { data } = plugin.init();
const { data } = plugin.init(space._id);
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);
@@ -95,7 +100,7 @@ describe('app.import.*', () => {
modified: 999,
description: '',
name: 'New',
parentId: null,
parentId: space._id,
type: 'Workspace',
scope: WorkspaceScopeKeys.collection,
},
@@ -129,12 +134,15 @@ describe('app.import.*', () => {
});
describe('app.export.*', () => {
let space: Space;
beforeEach(async () => {
await globalBeforeEach();
space = await models.space.create();
await models.workspace.create({
_id: 'wrk_1',
created: 111,
modified: 222,
parentId: space._id,
});
await models.request.create({
_id: 'req_1',
@@ -153,7 +161,7 @@ describe('app.export.*', () => {
});
it('insomnia', async () => {
const { data } = plugin.init();
const { data } = plugin.init(space._id);
const exported = await data.export.insomnia();
const exportedData = JSON.parse(exported);
expect(typeof exportedData.__export_date).toBe('string');
@@ -202,7 +210,7 @@ describe('app.export.*', () => {
});
it('har', async () => {
const { data } = plugin.init();
const { data } = plugin.init(space._id);
const exported = await data.export.har();
const exportedData = JSON.parse(exported);
exportedData.log.entries[0].startedDateTime = '2017-11-24T18:12:12.849Z';
@@ -1,6 +1,8 @@
import { exportWorkspacesData, exportWorkspacesHAR } from '../../common/export';
import type { ImportRawConfig } from '../../common/import';
import { importRaw, importUri } from '../../common/import';
import * as models from '../../models';
import { BASE_SPACE_ID } from '../../models/space';
import type { Workspace, WorkspaceScope } from '../../models/workspace';
interface PluginImportOptions {
@@ -16,21 +18,29 @@ interface InsomniaExport {
type HarExport = Omit<InsomniaExport, 'format'>;
const buildImportRawConfig = (options: PluginImportOptions): ImportRawConfig => ({
const buildImportRawConfig = (options: PluginImportOptions, activeSpaceId: string): ImportRawConfig => ({
getWorkspaceId: () => Promise.resolve(options.workspaceId || null),
getWorkspaceScope: options.scope && (() => (
Promise.resolve<WorkspaceScope>(options.scope as WorkspaceScope))
),
getSpaceId: () => Promise.resolve(activeSpaceId),
});
export const init = () => ({
// TODO: add metrics here to track how frequently this fallback is being used
const getWorkspaces = (activeSpaceId?: string) =>
activeSpaceId
? models.workspace.findByParentId(activeSpaceId)
: models.workspace.all();
// Only in the case of running unit tests from Inso via send-request can activeSpaceId be undefined. This is because the concept of a space doesn't exist in git/insomnia sync or an export file
export const init = (activeSpaceId?: string) => ({
data: {
import: {
uri: async (uri: string, options: PluginImportOptions = {}) => {
await importUri(uri, buildImportRawConfig(options));
await importUri(uri, buildImportRawConfig(options, activeSpaceId || BASE_SPACE_ID));
},
raw: async (text: string, options: PluginImportOptions = {}) => {
await importRaw(text, buildImportRawConfig(options));
await importRaw(text, buildImportRawConfig(options, activeSpaceId || BASE_SPACE_ID));
},
},
export: {
@@ -39,7 +49,7 @@ export const init = () => ({
includePrivate,
format,
}: InsomniaExport = {}) => exportWorkspacesData(
workspace ? [workspace] : [],
workspace ? [workspace] : await getWorkspaces(activeSpaceId),
Boolean(includePrivate),
format || 'json',
),
@@ -48,7 +58,7 @@ export const init = () => ({
workspace,
includePrivate,
}: HarExport = {}) => exportWorkspacesHAR(
workspace ? [workspace] : [],
workspace ? [workspace] : await getWorkspaces(activeSpaceId),
Boolean(includePrivate),
),
},
@@ -6,6 +6,7 @@ import { globalBeforeEach } from '../../../__jest__/before-each';
import { database as db } from '../../../common/database';
import * as models from '../../../models';
import { workspaceModelSchema } from '../../../models/__schemas__/model-schemas';
import { BASE_SPACE_ID } from '../../../models/space';
import { GIT_CLONE_DIR, GIT_INSOMNIA_DIR, GIT_INSOMNIA_DIR_NAME } from '../git-vcs';
import { NeDBClient } from '../ne-db-client';
import { assertAsyncError, setupDateMocks } from './util';
@@ -19,6 +20,7 @@ describe('NeDBClient', () => {
workspaceBuilder.reset();
setupDateMocks();
// Create some sample models
await models.space.all();
await models.workspace.create({
_id: 'wrk_1',
});
@@ -40,7 +42,7 @@ describe('NeDBClient', () => {
describe('readdir()', () => {
it('reads model IDs from model type folders', async () => {
const neDbClient = new NeDBClient('wrk_1');
const neDbClient = new NeDBClient('wrk_1', BASE_SPACE_ID);
const reqDir = path.join(GIT_INSOMNIA_DIR, models.request.type);
const wrkDir = path.join(GIT_INSOMNIA_DIR, models.workspace.type);
expect(await neDbClient.readdir(GIT_CLONE_DIR)).toEqual([GIT_INSOMNIA_DIR_NAME]);
@@ -66,11 +68,11 @@ describe('NeDBClient', () => {
const wrk1Yml = path.join(GIT_INSOMNIA_DIR, models.workspace.type, 'wrk_1.yml');
const req1Yml = path.join(GIT_INSOMNIA_DIR, models.request.type, 'req_1.yml');
const reqXYml = path.join(GIT_INSOMNIA_DIR, models.request.type, 'req_x.yml');
const pNeDB = new NeDBClient('wrk_1');
const pNeDB = new NeDBClient('wrk_1', BASE_SPACE_ID);
expect(YAML.parse((await pNeDB.readFile(wrk1Yml, 'utf8')).toString())).toEqual(
expect.objectContaining({
_id: 'wrk_1',
parentId: null,
parentId: null, // should be reset to default value instead of the active space id
}),
);
expect(YAML.parse((await pNeDB.readFile(req1Yml, 'utf8')).toString())).toEqual(
@@ -95,7 +97,7 @@ describe('NeDBClient', () => {
type: 'file',
});
// Act
const neDbClient = new NeDBClient('wrk_1');
const neDbClient = new NeDBClient('wrk_1', BASE_SPACE_ID);
// Assert
expect(await neDbClient.stat(GIT_CLONE_DIR)).toEqual(dirType);
expect(await neDbClient.stat(GIT_INSOMNIA_DIR)).toEqual(dirType);
@@ -110,7 +112,7 @@ describe('NeDBClient', () => {
// Arrange
const upsertSpy = jest.spyOn(db, 'upsert');
const workspaceId = 'wrk_1';
const neDbClient = new NeDBClient(workspaceId);
const neDbClient = new NeDBClient(workspaceId, BASE_SPACE_ID);
const env = {
_id: 'env_1',
type: models.environment.type,
@@ -128,7 +130,7 @@ describe('NeDBClient', () => {
it('should write files in GIT_INSOMNIA_DIR directory to db', async () => {
// Arrange
const workspaceId = 'wrk_1';
const neDbClient = new NeDBClient(workspaceId);
const neDbClient = new NeDBClient(workspaceId, BASE_SPACE_ID);
const upsertSpy = jest.spyOn(db, 'upsert');
const env = {
_id: 'env_1',
@@ -202,7 +204,7 @@ describe('NeDBClient', () => {
it('should throw error if id does not match', async () => {
// Arrange
const workspaceId = 'wrk_1';
const neDbClient = new NeDBClient(workspaceId);
const neDbClient = new NeDBClient(workspaceId, BASE_SPACE_ID);
const env = {
_id: 'env_1',
type: models.environment.type,
@@ -220,7 +222,7 @@ describe('NeDBClient', () => {
it('should throw error if type does not match', async () => {
// Arrange
const workspaceId = 'wrk_1';
const neDbClient = new NeDBClient(workspaceId);
const neDbClient = new NeDBClient(workspaceId, BASE_SPACE_ID);
const env = {
_id: 'env_1',
type: models.environment.type,
@@ -239,7 +241,7 @@ describe('NeDBClient', () => {
describe('mkdir()', () => {
it('should throw error', async () => {
const workspaceId = 'wrk_1';
const neDbClient = new NeDBClient(workspaceId);
const neDbClient = new NeDBClient(workspaceId, BASE_SPACE_ID);
const promiseResult = neDbClient.mkdir();
await expect(promiseResult).rejects.toThrowError('NeDBClient is not writable');
});
@@ -16,18 +16,18 @@ import { BufferEncoding } from './utils';
export class NeDBClient {
_workspaceId: string;
_spaceId: string | null;
_spaceId: string;
constructor(workspaceId: string, spaceId?: string) {
constructor(workspaceId: string, spaceId: string) {
if (!workspaceId) {
throw new Error('Cannot use NeDBClient without workspace ID');
}
this._workspaceId = workspaceId;
this._spaceId = spaceId || null;
this._spaceId = spaceId;
}
static createClient(workspaceId: string, spaceId?: string): PromiseFsClient {
static createClient(workspaceId: string, spaceId: string): PromiseFsClient {
return {
promises: new NeDBClient(workspaceId, spaceId),
};
@@ -104,7 +104,6 @@ export class NeDBClient {
// Whenever we write a workspace into nedb we should set the parentId to be that of the current space
// This is because the parentId (or a space) is not synced into git, so it will be cleared whenever git writes the workspace into the db, thereby removing it from the space on the client
// In order to reproduce this bug, comment out the following line, then clone a repository into a local space, then open the workspace, you'll notice it will have moved into the base space
// @ts-expect-error parentId can be string or null for a workspace
doc.parentId = this._spaceId;
}
@@ -39,25 +39,6 @@ describe('pullProject()', () => {
expect(vcs.pull).not.toHaveBeenCalledWith([], expect.anything());
});
it('should insert a workspace with no parent', async () => {
// Arrange
const space = undefined;
// Act
await pullProject({ vcs, project, space });
// Assert
const workspaces = await models.workspace.all();
expect(workspaces).toHaveLength(1);
const workspace = workspaces[0];
expect(workspace).toStrictEqual(expect.objectContaining<Partial<Workspace>>({
_id: project.rootDocumentId,
name: project.name,
// @ts-expect-error parent id is optional for workspaces
parentId: null,
}));
});
it('should insert a workspace with parent', async () => {
// Arrange
const space = await models.space.create();
@@ -104,36 +85,6 @@ describe('pullProject()', () => {
afterEach(() => {
});
it('should overwrite the parentId only for a workspace with null', async () => {
// Arrange
const space = undefined;
const existingWrk = await models.workspace.create({ _id: project.rootDocumentId, name: project.name });
const existingReq = await models.request.create({ parentId: existingWrk._id });
vcs.allDocuments.mockResolvedValue([existingWrk, existingReq]);
// Act
await pullProject({ vcs, project, space });
// Assert
const workspaces = await models.workspace.all();
expect(workspaces).toHaveLength(1);
const workspace = workspaces[0];
expect(workspace).toStrictEqual(expect.objectContaining<Partial<Workspace>>({
_id: project.rootDocumentId,
name: project.name,
// @ts-expect-error parent id is optional for workspaces
parentId: null,
}));
const requests = await models.request.all();
expect(requests).toHaveLength(1);
const request = requests[0];
expect(request).toStrictEqual(existingReq);
expect(vcs.pull).toHaveBeenCalledWith([], undefined);
});
it('should overwrite the parentId only for a workspace with the space id', async () => {
// Arrange
const space = await models.space.create({ remoteId: 'abc' });
@@ -943,7 +943,7 @@ describe('util', () => {
it('shouldnt change the hash of a workspace after a parent id is added and ignored', () => {
// Arrange
// @ts-expect-error parent id should be nullable
// @ts-expect-error parentId is intentionally null, because that's what the data was originally
const originalSyncedWorkspace = workspaceModelBuilder.parentId(null).build();
// Existing synced workspaces do not have a modified field
@@ -10,7 +10,7 @@ import { VCS } from './vcs';
interface Options {
vcs: VCS;
project: Project;
space?: Space;
space: Space;
}
export const pullProject = async ({ vcs, project, space }: Options) => {
@@ -20,7 +20,7 @@ export const pullProject = async ({ vcs, project, space }: Options) => {
const remoteBranches = await vcs.getRemoteBranches();
const defaultBranchMissing = !remoteBranches.includes(DEFAULT_BRANCH_NAME);
const workspaceParentId = space?._id || null;
const workspaceParentId = space._id;
// The default branch does not exist, so we create it and the workspace locally
if (defaultBranchMissing) {
@@ -31,14 +31,13 @@ export const pullProject = async ({ vcs, project, space }: Options) => {
});
await database.upsert(workspace);
} else {
await vcs.pull([], space?.remoteId); // There won't be any existing docs since it's a new pull
await vcs.pull([], space.remoteId); // There won't be any existing docs since it's a new pull
const flushId = await database.bufferChanges();
// @ts-expect-error -- TSCONVERSION
for (const doc of (await vcs.allDocuments() || [])) {
if (isWorkspace(doc)) {
// @ts-expect-error parent id is optional for workspaces
doc.parentId = workspaceParentId;
}
await database.upsert(doc);
@@ -12,6 +12,7 @@ import { GrpcRequest } from '../../../models/grpc-request';
import * as requestOperations from '../../../models/helpers/request-operations';
import { isRequest, Request } from '../../../models/request';
import type { RequestGroup } from '../../../models/request-group';
import { Space } from '../../../models/space';
import { incrementDeletedRequests } from '../../../models/stats';
// Plugin action related imports
import type { RequestAction } from '../../../plugins';
@@ -38,6 +39,7 @@ interface Props extends Partial<DropdownProps> {
hotKeyRegistry: HotKeyRegistry;
handleSetRequestPinned: Function;
activeEnvironment?: Environment | null;
activeSpace: Space;
}
// Setup state for plugin actions
@@ -101,11 +103,11 @@ class RequestActionsDropdown extends PureComponent<Props, State> {
}));
try {
const { activeEnvironment, request, requestGroup } = this.props;
const { activeEnvironment, activeSpace, request, requestGroup } = this.props;
const activeEnvironmentId = activeEnvironment ? activeEnvironment._id : null;
const context = {
...(pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER) as Record<string, any>),
...pluginContexts.data.init(),
...pluginContexts.data.init(activeSpace._id),
...(pluginContexts.store.init(p.plugin) as Record<string, any>),
...(pluginContexts.network.init(activeEnvironmentId) as Record<string, any>),
};
@@ -9,6 +9,7 @@ import { RENDER_PURPOSE_NO_RENDER } from '../../../common/render';
import * as models from '../../../models';
import type { Environment } from '../../../models/environment';
import type { RequestGroup } from '../../../models/request-group';
import { Space } from '../../../models/space';
import type { Workspace } from '../../../models/workspace';
import type { RequestGroupAction } from '../../../plugins';
import { getRequestGroupActions } from '../../../plugins';
@@ -25,6 +26,7 @@ import { showError, showModal } from '../modals';
import EnvironmentEditModal from '../modals/environment-edit-modal';
interface Props {
space: Space;
workspace: Workspace;
requestGroup: RequestGroup;
hotKeyRegistry: HotKeyRegistry;
@@ -91,11 +93,11 @@ class RequestGroupActionsDropdown extends PureComponent<Props, State> {
}));
try {
const { activeEnvironment, requestGroup } = this.props;
const { activeEnvironment, requestGroup, space } = this.props;
const activeEnvironmentId = activeEnvironment ? activeEnvironment._id : null;
const context = {
...(pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER) as Record<string, any>),
...pluginContexts.data.init(),
...pluginContexts.data.init(space._id),
...(pluginContexts.store.init(p.plugin) as Record<string, any>),
...(pluginContexts.network.init(activeEnvironmentId) as Record<string, any>),
};
@@ -3,9 +3,8 @@ import React, { FC, useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { getAppName } from '../../../common/constants';
import { strings } from '../../../common/strings';
import { BASE_SPACE_ID, isBaseSpace, isRemoteSpace, Space, spaceHasSettings } from '../../../models/space';
import { isBaseSpace, isNotBaseSpace, isRemoteSpace, Space, spaceHasSettings } from '../../../models/space';
import { VCS } from '../../../sync/vcs/vcs';
import { useRemoteSpaces } from '../../hooks/space';
import { setActiveSpace } from '../../redux/modules/global';
@@ -14,14 +13,6 @@ import { selectActiveSpace, selectSpaces } from '../../redux/selectors';
import { showModal } from '../modals';
import SpaceSettingsModal from '../modals/space-settings-modal';
type SpaceSubset = Pick<Space, '_id' | 'name' | 'remoteId'>;
const baseSpace: SpaceSubset = {
_id: BASE_SPACE_ID,
name: getAppName(),
remoteId: null,
};
const svgPlacementHack = {
// This is a bit of a hack/workaround to avoid some larger changes that we'd need to do with dropdown item icons and tooltips.
// Without this, the icon is too high with respect to the text because of Tooltip introducing some changes to the placement of the icon.
@@ -67,7 +58,7 @@ interface Props {
}
const SpaceDropdownItem: FC<{
space: SpaceSubset;
space: Space;
isActive: boolean;
setActive: (spaceId: string) => void;
}> = ({ isActive, space, setActive }) => {
@@ -92,10 +83,9 @@ SpaceDropdownItem.displayName = DropdownItem.name; // This is required because t
export const SpaceDropdown: FC<Props> = ({ vcs }) => {
const { loading, refresh } = useRemoteSpaces(vcs);
// get list of spaces (which doesn't include the base space)
const spaces = useSelector(selectSpaces);
const activeSpace = useSelector(selectActiveSpace) || baseSpace;
const activeSpace = useSelector(selectActiveSpace);
const dispatch = useDispatch();
const setActive = useCallback((spaceId: string) => dispatch(setActiveSpace(spaceId)), [dispatch]);
const createNew = useCallback(() => dispatch(createSpace()), [dispatch]);
@@ -109,25 +99,20 @@ export const SpaceDropdown: FC<Props> = ({ vcs }) => {
</button>
), [activeSpace]);
const renderSpace = useCallback((space: Space) => (
<SpaceDropdownItem
key={space._id}
isActive={space._id === activeSpace._id}
setActive={setActive}
space={space}
/>
), [setActive, activeSpace._id]);
return (
<Dropdown renderButton={button} onOpen={refresh}>
<SpaceDropdownItem
isActive={baseSpace._id === activeSpace._id}
setActive={setActive}
space={baseSpace}
/>
{spaces.filter(isBaseSpace).map(renderSpace)}
<DropdownDivider>All spaces{' '}{loading && spinner}</DropdownDivider>
{spaces.map(space => (
<SpaceDropdownItem
isActive={space._id === activeSpace._id}
key={space._id}
setActive={setActive}
space={space}
/>
))}
{spaces.filter(isNotBaseSpace).map(renderSpace)}
{spaceHasSettings(activeSpace) && <>
<DropdownDivider />
<DropdownItem icon={<StyledSvgIcon icon="gear" />} onClick={showSettings}>
@@ -142,3 +127,4 @@ export const SpaceDropdown: FC<Props> = ({ vcs }) => {
</Dropdown>
);
};
@@ -9,6 +9,7 @@ import * as models from '../../../models';
import type { ApiSpec } from '../../../models/api-spec';
import getWorkspaceName from '../../../models/helpers/get-workspace-name';
import * as workspaceOperations from '../../../models/helpers/workspace-operations';
import { Space } from '../../../models/space';
import type { Workspace } from '../../../models/workspace';
import { WorkspaceScopeKeys } from '../../../models/workspace';
import type { DocumentAction } from '../../../plugins';
@@ -23,11 +24,12 @@ import AskModal from '../modals/ask-modal';
interface Props {
workspace: Workspace;
apiSpec: ApiSpec;
space: Space;
}
const spinner = <i className="fa fa-refresh fa-spin" />;
const useWorkspaceHandlers = ({ workspace, apiSpec }: { workspace: Workspace; apiSpec: ApiSpec; }) => {
const useWorkspaceHandlers = ({ workspace, apiSpec }: Props) => {
const dispatch = useDispatch();
const handleDuplicate = useCallback(() => {
@@ -78,7 +80,7 @@ const useWorkspaceHandlers = ({ workspace, apiSpec }: { workspace: Workspace; ap
return { handleDelete, handleDuplicate, handleRename };
};
const useDocumentActionPlugins = ({ workspace, apiSpec }: { workspace: Workspace; apiSpec: ApiSpec; }) => {
const useDocumentActionPlugins = ({ workspace, apiSpec, space }: Props) => {
const [actionPlugins, setActionPlugins] = useState<DocumentAction[]>([]);
const { startLoading, stopLoading, isLoading } = useLoadingRecord();
@@ -95,7 +97,7 @@ const useDocumentActionPlugins = ({ workspace, apiSpec }: { workspace: Workspace
try {
const context = {
...pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER),
...pluginContexts.data.init(),
...pluginContexts.data.init(space._id),
...pluginContexts.store.init(p.plugin),
};
// @ts-expect-error -- TSCONVERSION
@@ -108,7 +110,7 @@ const useDocumentActionPlugins = ({ workspace, apiSpec }: { workspace: Workspace
} finally {
stopLoading(p.label);
}
}, [apiSpec.contents, startLoading, stopLoading]);
}, [apiSpec.contents, space._id, startLoading, stopLoading]);
const renderPluginDropdownItems = useCallback(() => actionPlugins.map(p => (
<DropdownItem
@@ -13,6 +13,7 @@ import { ApiSpec } from '../../../models/api-spec';
import type { Environment } from '../../../models/environment';
import { isRequest } from '../../../models/request';
import { isRequestGroup } from '../../../models/request-group';
import { Space } from '../../../models/space';
import { isDesign, Workspace } from '../../../models/workspace';
import type { WorkspaceAction } from '../../../plugins';
import { ConfigGenerator, getConfigGenerators, getWorkspaceActions } from '../../../plugins';
@@ -33,6 +34,7 @@ interface Props {
activeEnvironment: Environment | null;
activeWorkspace: Workspace;
activeApiSpec: ApiSpec;
activeSpace: Space;
hotKeyRegistry: HotKeyRegistry;
isLoading: boolean;
className?: string;
@@ -57,13 +59,13 @@ class WorkspaceDropdown extends PureComponent<Props, State> {
this.setState(state => ({
loadingActions: { ...state.loadingActions, [p.label]: true },
}));
const { activeEnvironment, activeWorkspace } = this.props;
const { activeEnvironment, activeWorkspace, activeSpace } = this.props;
try {
const activeEnvironmentId = activeEnvironment ? activeEnvironment._id : null;
const context = {
...(pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER) as Record<string, any>),
...pluginContexts.data.init(),
...pluginContexts.data.init(activeSpace._id),
...(pluginContexts.store.init(p.plugin) as Record<string, any>),
...(pluginContexts.network.init(activeEnvironmentId) as Record<string, any>),
};
@@ -1,5 +1,5 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import React, { PureComponent } from 'react';
import React, { PureComponent, ReactNode } from 'react';
import { AUTOBIND_CFG } from '../../../common/constants';
import Modal from '../base/modal';
@@ -7,13 +7,16 @@ import ModalBody from '../base/modal-body';
import ModalFooter from '../base/modal-footer';
import ModalHeader from '../base/modal-header';
interface State {
title: string;
message: string;
addCancel: boolean;
okLabel: string;
export interface AlertModalOptions {
title?: string;
message?: ReactNode;
addCancel?: boolean;
okLabel?: string;
onConfirm?: () => void | Promise<void>;
}
type State = Omit<AlertModalOptions, 'onConfirm'>;
@autoBindMethodsForReact(AUTOBIND_CFG)
class AlertModal extends PureComponent<{}, State> {
state: State = {
@@ -26,8 +29,9 @@ class AlertModal extends PureComponent<{}, State> {
modal: Modal | null = null;
_cancel: HTMLButtonElement | null = null;
_ok: HTMLButtonElement | null = null;
_okCallback: Function | null = null;
_okCallback2: Function | null = null;
_okCallback: (value: void | PromiseLike<void>) => void;
_okCallback2: AlertModalOptions['onConfirm'];
_setModalRef(m: Modal) {
this.modal = m;
@@ -36,7 +40,7 @@ class AlertModal extends PureComponent<{}, State> {
_handleOk() {
this.hide();
this._okCallback?.();
this._okCallback();
if (typeof this._okCallback2 === 'function') {
this._okCallback2();
@@ -55,9 +59,7 @@ class AlertModal extends PureComponent<{}, State> {
this._ok = n;
}
show(options = {}) {
// @ts-expect-error -- TSCONVERSION
const { title, message, addCancel, onConfirm, okLabel } = options;
show({ title, message, addCancel, onConfirm, okLabel }: AlertModalOptions) {
this.setState({
title,
message,
@@ -70,7 +72,7 @@ class AlertModal extends PureComponent<{}, State> {
this._cancel && this._cancel.focus();
}, 100);
this._okCallback2 = onConfirm;
return new Promise(resolve => {
return new Promise<void>(resolve => {
this._okCallback = resolve;
});
}
@@ -1,7 +1,6 @@
import { trackEvent } from '../../../common/analytics';
import AlertModal from './alert-modal';
import type { ErrorModalOptions } from './error-modal';
import ErrorModal from './error-modal';
import AlertModal, { AlertModalOptions } from './alert-modal';
import ErrorModal, { ErrorModalOptions } from './error-modal';
import PromptModal, { PromptModalOptions } from './prompt-modal';
const modals = {};
@@ -24,7 +23,7 @@ export function showPrompt(config: PromptModalOptions) {
return showModal(PromptModal, config);
}
export function showAlert(config) {
export function showAlert(config: AlertModalOptions) {
return showModal(AlertModal, config);
}
@@ -6,7 +6,8 @@ import { bindActionCreators } from 'redux';
import { AUTOBIND_CFG } from '../../../common/constants';
import { strings } from '../../../common/strings';
import * as models from '../../../models/index';
import { Space } from '../../../models/space';
import { isBaseSpace, isRemoteSpace } from '../../../models/space';
import { RootState } from '../../redux/modules';
import * as spaceActions from '../../redux/modules/space';
import { selectActiveSpace } from '../../redux/selectors';
import DebouncedInput from '../base/debounced-input';
@@ -15,15 +16,9 @@ import ModalBody from '../base/modal-body';
import ModalHeader from '../base/modal-header';
import PromptButton from '../base/prompt-button';
interface ReduxStateProps {
space?: Space;
}
export type ReduxProps = ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>;
interface ReduxDispatchProps {
handleRemoveSpace: (space: Space) => void;
}
interface Props extends ReduxStateProps, ReduxDispatchProps { }
type Props = ReduxProps
@autoBindMethodsForReact(AUTOBIND_CFG)
class SpaceSettingsModal extends PureComponent<Props> {
@@ -34,32 +29,26 @@ class SpaceSettingsModal extends PureComponent<Props> {
}
_handleRemoveSpace() {
if (this.props.space) {
this.props.handleRemoveSpace(this.props.space);
}
this.props.handleRemoveSpace(this.props.space);
this.hide();
}
async _handleRename(name: string) {
const { space } = this.props;
if (space) {
await models.space.update(space, { name });
}
await models.space.update(space, { name });
}
show() {
this.modal && this.modal.show();
this.modal?.show();
}
hide() {
this.modal && this.modal.hide();
this.modal?.hide();
}
render() {
const { space } = this.props;
if (!space) {
if (isBaseSpace(space) || isRemoteSpace(space)) {
return null;
}
@@ -98,12 +87,12 @@ class SpaceSettingsModal extends PureComponent<Props> {
}
}
const mapStateToProps = (state): ReduxStateProps => {
const mapStateToProps = (state: RootState) => {
const space = selectActiveSpace(state);
return { space };
};
const mapDispatchToProps = (dispatch): ReduxDispatchProps => {
const mapDispatchToProps = dispatch => {
const boundSpaceActions = bindActionCreators(spaceActions, dispatch);
return {
handleRemoveSpace: boundSpaceActions.removeSpace,
@@ -16,7 +16,7 @@ import SyncPullButton from '../sync-pull-button';
interface Props {
workspace: Workspace;
space?: Space;
space: Space;
syncItems: StatusCandidate[];
vcs: VCS;
}
@@ -9,6 +9,7 @@ import type { Environment } from '../../../models/environment';
import { GrpcRequest, isGrpcRequest } from '../../../models/grpc-request';
import { isRequest, Request } from '../../../models/request';
import type { RequestGroup } from '../../../models/request-group';
import { Space } from '../../../models/space';
import type { Workspace } from '../../../models/workspace';
import SidebarCreateDropdown from './sidebar-create-dropdown';
import SidebarRequestGroupRow from './sidebar-request-group-row';
@@ -43,6 +44,7 @@ interface Props {
hotKeyRegistry: HotKeyRegistry;
activeEnvironment?: Environment | null;
activeRequest?: Request | GrpcRequest | null;
activeSpace: Space;
}
@autoBindMethodsForReact(AUTOBIND_CFG)
@@ -91,6 +93,7 @@ class SidebarChildren extends PureComponent<Props> {
workspace,
hotKeyRegistry,
activeEnvironment,
activeSpace,
} = this.props;
const activeRequestId = activeRequest ? activeRequest._id : 'n/a';
return children.map(child => {
@@ -117,6 +120,7 @@ class SidebarChildren extends PureComponent<Props> {
request={child.doc}
hotKeyRegistry={hotKeyRegistry} // Necessary for plugin actions on requests
activeEnvironment={activeEnvironment}
activeSpace={activeSpace}
/>
);
}
@@ -158,6 +162,7 @@ class SidebarChildren extends PureComponent<Props> {
requestGroup={requestGroup}
hotKeyRegistry={hotKeyRegistry}
activeEnvironment={activeEnvironment}
activeSpace={activeSpace}
>
{children}
</SidebarRequestGroupRow>
@@ -10,6 +10,7 @@ import * as misc from '../../../common/misc';
import { HandleRender } from '../../../common/render';
import { Environment } from '../../../models/environment';
import { RequestGroup } from '../../../models/request-group';
import { Space } from '../../../models/space';
import { Workspace } from '../../../models/workspace';
import Highlight from '../base/highlight';
import RequestGroupActionsDropdown from '../dropdowns/request-group-actions-dropdown';
@@ -36,6 +37,7 @@ interface Props {
connectDragSource?: Function;
connectDropTarget?: Function;
activeEnvironment?: Environment | null;
activeSpace: Space;
}
interface State {
@@ -107,6 +109,7 @@ class SidebarRequestGroupRow extends PureComponent<Props, State> {
workspace,
hotKeyRegistry,
activeEnvironment,
activeSpace,
} = this.props;
const { dragDirection } = this.state;
let folderIconClass = 'fa-folder';
@@ -192,6 +195,7 @@ class SidebarRequestGroupRow extends PureComponent<Props, State> {
hotKeyRegistry={hotKeyRegistry}
isPinned={false} // Necessary so that plugin actions work
activeEnvironment={activeEnvironment}
activeSpace={activeSpace}
/>
)}
</ul>
@@ -13,6 +13,7 @@ import { GrpcRequest, isGrpcRequest } from '../../../models/grpc-request';
import * as requestOperations from '../../../models/helpers/request-operations';
import { Request } from '../../../models/request';
import { RequestGroup } from '../../../models/request-group';
import { Space } from '../../../models/space';
import Editable from '../base/editable';
import Highlight from '../base/highlight';
import RequestActionsDropdown from '../dropdowns/request-actions-dropdown';
@@ -23,6 +24,7 @@ import GrpcTag from '../tags/grpc-tag';
import MethodTag from '../tags/method-tag';
interface Props {
activeSpace: Space;
activeEnvironment?: Environment | null;
handleActivateRequest: Function;
handleSetRequestPinned: Function;
@@ -212,6 +214,7 @@ class SidebarRequestRow extends PureComponent<Props, State> {
isPinned,
request,
requestGroup,
activeSpace,
} = this.props;
const { dragDirection } = this.state;
let node;
@@ -283,6 +286,7 @@ class SidebarRequestRow extends PureComponent<Props, State> {
requestGroup={requestGroup}
hotKeyRegistry={hotKeyRegistry} // Necessary for plugin actions to have network capabilities
activeEnvironment={activeEnvironment}
activeSpace={activeSpace}
/>
</div>
{isPinned && (
@@ -9,7 +9,7 @@ import { showError } from './modals';
interface Props {
vcs: VCS;
branch: string;
space?: Space;
space: Space;
onPull: (...args: any[]) => any;
disabled?: boolean;
className?: string;
@@ -40,7 +40,7 @@ class SyncPullButton extends PureComponent<Props, State> {
try {
// Clone old VCS so we don't mess anything up while working on other projects
await newVCS.checkout([], branch);
await newVCS.pull([], space?.remoteId);
await newVCS.pull([], space.remoteId);
} catch (err) {
showError({
title: 'Pull Error',
@@ -23,6 +23,7 @@ const WorkspacePageHeader: FunctionComponent<Props> = ({
wrapperProps: {
activeApiSpec,
activeWorkspace,
activeSpace,
activeEnvironment,
settings,
activity,
@@ -47,6 +48,7 @@ const WorkspacePageHeader: FunctionComponent<Props> = ({
activeEnvironment={activeEnvironment}
activeWorkspace={activeWorkspace}
activeApiSpec={activeApiSpec}
activeSpace={activeSpace}
hotKeyRegistry={settings.hotKeyRegistry}
isLoading={isLoading}
/>
@@ -108,6 +108,7 @@ class WrapperDebug extends PureComponent<Props> {
handleSidebarSort,
} = this.props;
const {
activeSpace,
activeEnvironment,
activeRequest,
activeWorkspace,
@@ -185,6 +186,7 @@ class WrapperDebug extends PureComponent<Props> {
filter={sidebarFilter || ''}
hotKeyRegistry={settings.hotKeyRegistry}
activeEnvironment={activeEnvironment}
activeSpace={activeSpace}
/>
</Fragment>
);
@@ -160,6 +160,7 @@ class WrapperHome extends PureComponent<Props, State> {
renderCard(workspace: Workspace) {
const {
activeSpace,
apiSpecs,
workspaceMetas,
} = this.props.wrapperProps;
@@ -232,7 +233,7 @@ class WrapperHome extends PureComponent<Props, State> {
);
}
const docMenu = <WorkspaceCardDropdown apiSpec={apiSpec} workspace={workspace} />;
const docMenu = <WorkspaceCardDropdown apiSpec={apiSpec} workspace={workspace} space={activeSpace} />;
const version = spec?.info?.version || '';
let label: string = strings.collection.singular;
let format = '';
@@ -529,13 +529,12 @@ class App extends PureComponent<AppProps, State> {
async _fetchRenderContext() {
const { activeEnvironment, activeRequest, activeWorkspace } = this.props;
const environmentId = activeEnvironment ? activeEnvironment._id : null;
const ancestors = await db.withAncestors(activeRequest || activeWorkspace || null, [
models.request.type,
models.requestGroup.type,
models.workspace.type,
]);
return render.getRenderContext(activeRequest, environmentId, ancestors);
const ancestors = await render.getRenderContextAncestors(activeRequest || activeWorkspace);
return render.getRenderContext({
request: activeRequest || undefined,
environmentId: activeEnvironment?._id,
ancestors,
});
}
async _handleGetRenderContext(): Promise<RenderContextAndKeys> {
@@ -1208,7 +1207,7 @@ class App extends PureComponent<AppProps, State> {
);
/** All app data is stored within a namespaced GIT_INSOMNIA_DIR directory at the root of the repository and is read/written from the local NeDB database */
const neDbClient = NeDBClient.createClient(activeWorkspace._id, activeSpace?._id);
const neDbClient = NeDBClient.createClient(activeWorkspace._id, activeSpace._id);
/** All git metadata in the GIT_INTERNAL_DIR directory is stored in a git/ directory on the filesystem */
const gitDataClient = fsClient(baseDir);
@@ -4,7 +4,7 @@ import { mocked } from 'ts-jest/utils';
import { globalBeforeEach } from '../../../__jest__/before-each';
import { isLoggedIn as _isLoggedIn } from '../../../account/session';
import * as models from '../../../models';
import { Space } from '../../../models/space';
import { BASE_SPACE_ID, Space } from '../../../models/space';
import MemoryDriver from '../../../sync/store/drivers/memory-driver';
import { VCS } from '../../../sync/vcs/vcs';
import { useRemoteSpaces } from '../space';
@@ -36,7 +36,7 @@ describe('useRemoteSpaces', () => {
expect(vcs.teams).not.toHaveBeenCalled();
expect(result.current.loading).toBe(false);
await expect(models.space.all()).resolves.toHaveLength(0);
await expect(models.space.all()).resolves.toHaveLength(1);
});
it('should load teams each time VCS changes', async () => {
@@ -66,8 +66,11 @@ describe('useRemoteSpaces', () => {
expect(vcs2.teams).toHaveBeenCalledTimes(1);
const allSpaces = await models.space.all();
expect(allSpaces).toHaveLength(2);
expect(allSpaces).toHaveLength(3);
expect(allSpaces).toEqual(expect.arrayContaining([
expect.objectContaining<Partial<Space>>({
_id: BASE_SPACE_ID,
}),
expect.objectContaining<Partial<Space>>({
remoteId: team1.id,
name: team1.name,
@@ -92,7 +95,7 @@ describe('useRemoteSpaces', () => {
await waitFor(() => result.current.loading === false);
expect(vcs.teams).toHaveBeenCalledTimes(1);
await expect(models.space.all()).resolves.toHaveLength(0);
await expect(models.space.all()).resolves.toHaveLength(1);
const team1 = { id: 'id1', name: 'team1' };
const team2 = { id: 'id2', name: 'team2' };
@@ -105,8 +108,11 @@ describe('useRemoteSpaces', () => {
expect(vcs.teams).toHaveBeenCalledTimes(3);
const allSpaces = await models.space.all();
expect(allSpaces).toHaveLength(2);
expect(allSpaces).toHaveLength(3);
expect(allSpaces).toEqual(expect.arrayContaining([
expect.objectContaining<Partial<Space>>({
_id: BASE_SPACE_ID,
}),
expect.objectContaining<Partial<Space>>({
remoteId: team1.id,
name: team1.name,
@@ -1,6 +1,7 @@
import { globalBeforeEach } from '../../../__jest__/before-each';
import { reduxStateForTest } from '../../../__jest__/redux-state-for-test';
import * as models from '../../../models';
import { BASE_SPACE_ID, Space } from '../../../models/space';
import { selectActiveSpace } from '../selectors';
describe('selectors', () => {
@@ -19,7 +20,7 @@ describe('selectors', () => {
expect(space).toStrictEqual(spaceA);
});
it('should return undefined if active space not found', async () => {
it('should return base space if active space not found', async () => {
// create two spaces
await models.space.create();
await models.space.create();
@@ -28,19 +29,19 @@ describe('selectors', () => {
const state = await reduxStateForTest({ activeSpaceId: 'some-other-space' });
const space = selectActiveSpace(state);
expect(space).toBeUndefined();
expect(space).toStrictEqual(expect.objectContaining<Partial<Space>>({ _id: BASE_SPACE_ID }));
});
it('should return undefined if no active space', async () => {
it('should return base space if no active space', async () => {
// create two spaces
await models.space.create();
await models.space.create();
// set first as selected
const state = await reduxStateForTest({ activeSpaceId: null });
// set base as selected
const state = await reduxStateForTest({ activeSpaceId: undefined });
const space = selectActiveSpace(state);
expect(space).toBeUndefined();
expect(space).toStrictEqual(expect.objectContaining<Partial<Space>>({ _id: BASE_SPACE_ID }));
});
});
});
@@ -10,6 +10,7 @@ import * as models from '../../../../models';
import { ApiSpec } from '../../../../models/api-spec';
import { CookieJar } from '../../../../models/cookie-jar';
import { Environment } from '../../../../models/environment';
import { BASE_SPACE_ID } from '../../../../models/space';
import { Workspace, WorkspaceScope, WorkspaceScopeKeys } from '../../../../models/workspace';
import { WorkspaceMeta } from '../../../../models/workspace-meta';
import { getAndClearShowPromptMockArgs } from '../../../../test-utils';
@@ -31,7 +32,7 @@ const createStoreWithSpace = async () => {
return { store, space };
};
const expectedModelsCreated = async (name: string, scope: WorkspaceScope, parentId: string | null = null) => {
const expectedModelsCreated = async (name: string, scope: WorkspaceScope, parentId: string) => {
const workspaces = await models.workspace.all();
expect(workspaces).toHaveLength(1);
const workspace = workspaces[0];
@@ -40,7 +41,6 @@ const expectedModelsCreated = async (name: string, scope: WorkspaceScope, parent
expect(descendents).toHaveLength(5);
expect(descendents).toStrictEqual(expect.arrayContaining([
// @ts-expect-error parentId is nullable for workspaces
expect.objectContaining<Partial<Workspace>>({ name, scope, parentId, type: models.workspace.type }),
expect.objectContaining<Partial<ApiSpec>>({ parentId: workspace._id, type: models.apiSpec.type }),
expect.objectContaining<Partial<Environment>>({ parentId: workspace._id, type: models.environment.type }),
@@ -125,7 +125,7 @@ describe('workspace', () => {
const workspaceName = 'name';
await onComplete?.(workspaceName);
const workspaceId = await expectedModelsCreated(workspaceName, WorkspaceScopeKeys.collection);
const workspaceId = await expectedModelsCreated(workspaceName, WorkspaceScopeKeys.collection, BASE_SPACE_ID);
expect(trackSegmentEvent).toHaveBeenCalledWith('Collection Created');
expect(trackEvent).toHaveBeenCalledWith('Workspace', 'Create');
@@ -39,7 +39,7 @@ import install from '../../../plugins/install';
import { setTheme } from '../../../plugins/misc';
import AskModal from '../../../ui/components/modals/ask-modal';
import AlertModal from '../../components/modals/alert-modal';
import { showError, showModal } from '../../components/modals/index';
import { showAlert, showError, showModal } from '../../components/modals/index';
import LoginModal from '../../components/modals/login-modal';
import PaymentNotificationModal from '../../components/modals/payment-notification-modal';
import { SelectModal } from '../../components/modals/select-modal';
@@ -47,7 +47,7 @@ import SettingsModal, {
TAB_INDEX_PLUGINS,
TAB_INDEX_THEMES,
} from '../../components/modals/settings-modal';
import { selectSettings, selectWorkspacesForActiveSpace } from '../selectors';
import { selectActiveSpaceName, selectSettings, selectWorkspacesForActiveSpace } from '../selectors';
import { importUri } from './import';
export const LOCALSTORAGE_PREFIX = 'insomnia::meta';
@@ -79,7 +79,7 @@ function activeActivityReducer(state: string | null = null, action) {
}
}
function activeSpaceReducer(state: string | null = BASE_SPACE_ID, action) {
function activeSpaceReducer(state: string = BASE_SPACE_ID, action) {
switch (action.type) {
case SET_ACTIVE_SPACE:
return action.spaceId;
@@ -141,7 +141,7 @@ function loginStateChangeReducer(state = false, action) {
export interface GlobalState {
isLoading: boolean;
activeSpaceId: string | null;
activeSpaceId: string;
activeWorkspaceId: string | null;
activeActivity: GlobalActivity | null,
isLoggedIn: boolean;
@@ -461,10 +461,22 @@ const writeExportedFileToFileSystem = (filename: string, jsonData: string, onDon
export const exportAllToFile = () => async (dispatch: Dispatch, getState) => {
dispatch(loadStart());
const state = getState();
const activeSpaceName = selectActiveSpaceName(state);
const workspaces = selectWorkspacesForActiveSpace(state);
if (!workspaces.length) {
dispatch(loadStop());
showAlert({
title: 'Cannot export',
message: <>There are no workspaces to export in the <strong>{activeSpaceName}</strong> space.</>,
});
return;
}
showSelectExportTypeModal({
onCancel: () => { dispatch(loadStop()); },
onDone: async selectedFormat => {
const state = getState();
// Check if we want to export private environments.
const environments = await models.environment.all();
@@ -487,8 +499,6 @@ export const exportAllToFile = () => async (dispatch: Dispatch, getState) => {
return;
}
const workspaces = selectWorkspacesForActiveSpace(state);
let stringifiedExport;
try {
@@ -1,5 +1,4 @@
import { getAppName } from '../../../common/constants';
import { BASE_SPACE_ID, Space } from '../../../models/space';
import { Space } from '../../../models/space';
import { WorkspaceScope, WorkspaceScopeKeys } from '../../../models/workspace';
import { showModal } from '../../components/modals';
import AskModal from '../../components/modals/ask-modal';
@@ -64,18 +63,17 @@ export function askToSetWorkspaceScope(scope?: WorkspaceScope): SetWorkspaceScop
};
}
export type SetSpaceIdPrompt = () => Promise<string | null>;
export function askToImportIntoSpace({ spaces, activeSpace }: { spaces: Space[]; activeSpace?: Space; }): SetSpaceIdPrompt {
export type SetSpaceIdPrompt = () => Promise<string>;
export function askToImportIntoSpace({ spaces, activeSpace }: { spaces: Space[]; activeSpace: Space; }): SetSpaceIdPrompt {
return function() {
return new Promise(resolve => {
// If no spaces exist, return null (indicating no parent/space)
if (spaces.length === 0) {
return resolve(null);
// If only one space exists, return that
if (spaces.length === 1) {
return resolve(spaces[0]._id);
}
const options = [{ name: getAppName(), value: BASE_SPACE_ID }, ...spaces.map(space => ({ name: space.name, value: space._id }))];
const defaultValue = activeSpace?._id || options[0].value;
const options = spaces.map(space => ({ name: space.name, value: space._id }));
const defaultValue = activeSpace._id;
showSelectModal({
title: 'Import',
@@ -84,7 +82,8 @@ export function askToImportIntoSpace({ spaces, activeSpace }: { spaces: Space[];
value: defaultValue,
noEscape: true,
onDone: selectedSpaceId => {
resolve(selectedSpaceId === BASE_SPACE_ID ? null : selectedSpaceId);
// @ts-expect-error onDone can send null as an argument; why/how?
resolve(selectedSpaceId);
},
});
});
@@ -50,14 +50,13 @@ const convertToRawConfig = ({
}: ImportOptions,
state: RootState): ImportRawConfig => {
const activeSpace = selectActiveSpace(state);
// WARNING: this selector does not include the base space
const spaces = selectSpaces(state);
return ({
getWorkspaceScope: askToSetWorkspaceScope(forceToScope),
getWorkspaceId: askToImportIntoWorkspace({ workspaceId, forceToWorkspace }),
// Currently, just return the active space instead of prompting for which space to import into
getSpaceId: forceToSpace === 'prompt' ? askToImportIntoSpace({ spaces, activeSpace }) : () => Promise.resolve(activeSpace?._id || null),
getSpaceId: forceToSpace === 'prompt' ? askToImportIntoSpace({ spaces, activeSpace }) : () => Promise.resolve(activeSpace._id),
});
};
@@ -41,7 +41,6 @@ export const createWorkspace = ({ scope, onCreate }: {
}) => {
return (dispatch, getState) => {
const activeSpace = selectActiveSpace(getState());
const parentId = activeSpace?._id || null;
const design = isDesign({
scope,
@@ -61,8 +60,7 @@ export const createWorkspace = ({ scope, onCreate }: {
{
name,
scope,
// @ts-expect-error TSCONVERSION the common parentId isn't typed correctly
parentId,
parentId: activeSpace._id,
},
onCreate,
),
@@ -7,6 +7,7 @@ import { BaseModel } from '../../models';
import { getStatusCandidates } from '../../models/helpers/get-status-candidates';
import { isRequest, Request } from '../../models/request';
import { isRequestGroup, RequestGroup } from '../../models/request-group';
import { BASE_SPACE_ID } from '../../models/space';
import { UnitTestResult } from '../../models/unit-test-result';
import { RootState } from './modules';
@@ -60,7 +61,6 @@ export const selectSettings = createSelector(
selectEntitiesLists,
entities => entities.settings[0] || models.settings.init());
/** WARNING: this does not include the base space */
export const selectSpaces = createSelector(
selectEntitiesLists,
entities => entities.spaces,
@@ -70,7 +70,7 @@ export const selectActiveSpace = createSelector(
selectEntities,
(state: RootState) => state.global.activeSpaceId,
(entities, activeSpaceId) => {
return activeSpaceId ? entities.spaces[activeSpaceId] : undefined;
return entities.spaces[activeSpaceId] || entities.spaces[BASE_SPACE_ID];
},
);
@@ -82,10 +82,7 @@ export const selectAllWorkspaces = createSelector(
export const selectWorkspacesForActiveSpace = createSelector(
selectAllWorkspaces,
selectActiveSpace,
(workspaces, activeSpace) => {
const parentId = activeSpace?._id || null;
return workspaces.filter(w => w.parentId === parentId);
},
(workspaces, activeSpace) => workspaces.filter(w => w.parentId === activeSpace._id),
);
export const selectActiveWorkspace = createSelector(
@@ -366,7 +363,7 @@ export const selectActiveUnitTests = createSelector(
export const selectActiveSpaceName = createSelector(
selectActiveSpace,
activeSpace => activeSpace?.name,
activeSpace => activeSpace.name,
);
export const selectActiveUnitTestSuites = createSelector(