mirror of
https://github.com/Kong/insomnia.git
synced 2026-10-25 16:19:20 -04:00
Filter remote collections by the active space (#3456)
Co-authored-by: David Marby <david@dmarby.se>
This commit is contained in:
1 parent
d0fac873d0
commit
58160e82bc
24 files changed
+803
-494
No files matched your search
@@ -94,9 +94,12 @@ export const UPDATE_CHANNEL_BETA = 'beta';
|
||||
export const UPDATE_URL_MAC = 'https://updates.insomnia.rest/builds/check/mac';
|
||||
export const UPDATE_URL_WINDOWS = 'https://updates.insomnia.rest/updates/win';
|
||||
|
||||
// AP
|
||||
// API
|
||||
export const API_BASE_URL = 'https://api.insomnia.rest';
|
||||
|
||||
// SYNC
|
||||
export const DEFAULT_BRANCH_NAME = 'master';
|
||||
|
||||
// PLUGIN
|
||||
export const PLUGIN_HUB_BASE = 'https://insomnia.rest/plugins';
|
||||
export const NPM_PACKAGE_BASE = 'https://www.npmjs.com/package';
|
||||
|
||||
@@ -17,6 +17,7 @@ import { Environment } from '../models/environment';
|
||||
export interface Query {
|
||||
_id?: string | SpecificQuery;
|
||||
parentId?: string | null;
|
||||
remoteId?: string | null;
|
||||
plugin?: string;
|
||||
key?: string;
|
||||
environmentId?: string | null;
|
||||
|
||||
@@ -444,6 +444,6 @@ export function snapNumberToLimits(value: number, min?: number, max?: number) {
|
||||
return value;
|
||||
}
|
||||
|
||||
export function isNotNullOrUndefined(obj: unknown) {
|
||||
export function isNotNullOrUndefined(obj: any | null | undefined) {
|
||||
return obj !== null && obj !== undefined;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Schema } from '@develohpanda/fluent-builder';
|
||||
import { BaseModel, workspace } from '..';
|
||||
import { Workspace } from '../workspace';
|
||||
|
||||
export const baseModelSchema: Schema<BaseModel> = {
|
||||
_id: () => 'id',
|
||||
created: () => 1234,
|
||||
isPrivate: () => false,
|
||||
modified: () => 5678,
|
||||
name: () => 'name',
|
||||
parentId: () => '',
|
||||
type: () => 'base',
|
||||
};
|
||||
|
||||
export const workspaceModelSchema: Schema<Workspace> = {
|
||||
...baseModelSchema,
|
||||
type: () => workspace.type,
|
||||
description: () => '',
|
||||
certificates: () => undefined,
|
||||
scope: () => 'collection',
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { createBuilder, Schema } from '@develohpanda/fluent-builder';
|
||||
import { baseModelSchema } from '../../models/__schemas__/model-schemas';
|
||||
import { Branch, MergeConflict, Project, StatusCandidate } from '../types';
|
||||
|
||||
export const projectSchema: Schema<Project> = {
|
||||
id: () => 'id',
|
||||
rootDocumentId: () => 'rootDocumentId',
|
||||
name: () => 'name',
|
||||
};
|
||||
|
||||
export const branchSchema: Schema<Branch> = {
|
||||
created: () => new Date(0),
|
||||
modified: () => new Date(0),
|
||||
name: () => '',
|
||||
snapshots: () => [],
|
||||
};
|
||||
|
||||
export const mergeConflictSchema: Schema<MergeConflict> = {
|
||||
key: () => 'key',
|
||||
choose: () => null,
|
||||
mineBlob: () => null,
|
||||
theirsBlob: () => null,
|
||||
message: () => 'message',
|
||||
name: () => 'name',
|
||||
};
|
||||
|
||||
export const statusCandidateSchema: Schema<StatusCandidate> = {
|
||||
key: () => 'key',
|
||||
name: () => 'name',
|
||||
document: () => createBuilder(baseModelSchema).build(),
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BaseModel } from '../models';
|
||||
|
||||
export interface Team {
|
||||
id: string;
|
||||
@@ -92,7 +93,7 @@ export type Stage = Record<DocumentKey, StageEntry>;
|
||||
export interface StatusCandidate {
|
||||
key: DocumentKey;
|
||||
name: string;
|
||||
document: Record<string, any>;
|
||||
document: BaseModel;
|
||||
}
|
||||
|
||||
export type StatusCandidateMap = Record<DocumentKey, StatusCandidate>;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { createBuilder } from '@develohpanda/fluent-builder';
|
||||
import { baseModelSchema, workspaceModelSchema } from '../../../models/__schemas__/model-schemas';
|
||||
import { deleteKeys, resetKeys, shouldIgnoreKey } from '../ignore-keys';
|
||||
|
||||
const baseModelBuilder = createBuilder(baseModelSchema);
|
||||
const workspaceModelBuilder = createBuilder(workspaceModelSchema);
|
||||
|
||||
describe('shouldIgnoreKey()', () => {
|
||||
it('should always ignore keys', () => {
|
||||
const base = baseModelBuilder.build();
|
||||
const workspace = workspaceModelBuilder.build();
|
||||
|
||||
// don't ignore _id
|
||||
expect(shouldIgnoreKey('_id', base)).toBe(false);
|
||||
expect(shouldIgnoreKey('_id', workspace)).toBe(false);
|
||||
|
||||
// do ignore modified
|
||||
expect(shouldIgnoreKey('modified', base)).toBe(true);
|
||||
expect(shouldIgnoreKey('modified', workspace)).toBe(true);
|
||||
});
|
||||
|
||||
it('should always ignore parent id from workspace', () => {
|
||||
const base = baseModelBuilder.build();
|
||||
const workspace = workspaceModelBuilder.build();
|
||||
|
||||
expect(shouldIgnoreKey('parentId', base)).toBe(false);
|
||||
expect(shouldIgnoreKey('parentId', workspace)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteKeys', () => {
|
||||
it('should always delete the modified key', () => {
|
||||
const base = baseModelBuilder.build();
|
||||
const workspace = workspaceModelBuilder.build();
|
||||
|
||||
deleteKeys(base);
|
||||
deleteKeys(workspace);
|
||||
|
||||
// Don't delete _id
|
||||
expect('_id' in base).toBe(true);
|
||||
expect('_id' in workspace).toBe(true);
|
||||
|
||||
// Do delete modified
|
||||
expect('modified' in base).not.toBe(true);
|
||||
expect('modified' in workspace).not.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetKeys', () => {
|
||||
it('should clear parentId from the workspace', () => {
|
||||
const base = baseModelBuilder._id('123').parentId('abc').build();
|
||||
const workspace = workspaceModelBuilder._id('123').parentId('abc').build();
|
||||
|
||||
resetKeys(base);
|
||||
resetKeys(workspace);
|
||||
|
||||
// Don't reset _id
|
||||
expect(base._id).toBe('123');
|
||||
expect(workspace._id).toBe('123');
|
||||
|
||||
// Do reset parentId for a workspace
|
||||
expect(base.parentId).toBe('abc');
|
||||
expect(workspace.parentId).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
import { createBuilder } from '@develohpanda/fluent-builder';
|
||||
import { DEFAULT_BRANCH_NAME } from '../../../common/constants';
|
||||
import * as models from '../../../models';
|
||||
import { Workspace } from '../../../models/workspace';
|
||||
import { globalBeforeEach } from '../../../__jest__/before-each';
|
||||
import { projectSchema } from '../../__schemas__/type-schemas';
|
||||
import { pullProject } from '../pull-project';
|
||||
import { mocked } from 'ts-jest/utils';
|
||||
import MemoryDriver from '../../store/drivers/memory-driver';
|
||||
import { VCS } from '../vcs';
|
||||
|
||||
jest.mock('../vcs');
|
||||
|
||||
const project = createBuilder(projectSchema).build();
|
||||
|
||||
const newMockedVcs = () => mocked(new VCS(new MemoryDriver()), true);
|
||||
|
||||
describe('pullProject()', () => {
|
||||
let vcs = newMockedVcs();
|
||||
|
||||
beforeEach(async () => {
|
||||
(VCS as jest.MockedClass<typeof VCS>).mockClear();
|
||||
await globalBeforeEach();
|
||||
vcs = newMockedVcs();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
expect(vcs.setProject).toHaveBeenCalledWith(project);
|
||||
expect(vcs.checkout).toHaveBeenCalledWith([], DEFAULT_BRANCH_NAME);
|
||||
});
|
||||
|
||||
describe('creating a new project', () => {
|
||||
beforeEach(() => {
|
||||
vcs.getRemoteBranches.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
expect(vcs.pull).not.toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('should insert a workspace with no parent', async () => {
|
||||
// Arrange
|
||||
const spaceId = undefined;
|
||||
|
||||
// Act
|
||||
await pullProject({ vcs, project, spaceId });
|
||||
|
||||
// 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 spaceId = 'spaceId';
|
||||
|
||||
// Act
|
||||
await pullProject({ vcs, project, spaceId });
|
||||
|
||||
// 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,
|
||||
parentId: spaceId,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should update a workspace if the name or parentId is different', async () => {
|
||||
// Arrange
|
||||
await models.workspace.create({ _id: project.rootDocumentId, name: 'someName' });
|
||||
const spaceId = 'spaceId';
|
||||
|
||||
// Act
|
||||
await pullProject({ vcs, project, spaceId });
|
||||
|
||||
// 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,
|
||||
parentId: spaceId,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('pulling an existing project', () => {
|
||||
beforeEach(() => {
|
||||
vcs.getRemoteBranches.mockResolvedValue([DEFAULT_BRANCH_NAME]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
expect(vcs.pull).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('should overwrite the parentId only for a workspace with null', async () => {
|
||||
// Arrange
|
||||
const spaceId = 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, spaceId });
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
it('should overwrite the parentId only for a workspace with the space id', async () => {
|
||||
// Arrange
|
||||
const spaceId = 'spaceId';
|
||||
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, spaceId });
|
||||
|
||||
// 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,
|
||||
parentId: spaceId,
|
||||
}));
|
||||
|
||||
const requests = await models.request.all();
|
||||
expect(requests).toHaveLength(1);
|
||||
const request = requests[0];
|
||||
expect(request).toStrictEqual(existingReq);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,7 @@
|
||||
import { createBuilder } from '@develohpanda/fluent-builder';
|
||||
import { baseModelSchema, workspaceModelSchema } from '../../../models/__schemas__/model-schemas';
|
||||
import { StageEntry } from '../../types';
|
||||
import { branchSchema, statusCandidateSchema, mergeConflictSchema } from '../../__schemas__/type-schemas';
|
||||
import {
|
||||
combinedMapKeys,
|
||||
compareBranches,
|
||||
@@ -5,6 +9,7 @@ import {
|
||||
generateSnapshotStateMap,
|
||||
getRootSnapshot,
|
||||
getStagable,
|
||||
hash,
|
||||
hashDocument,
|
||||
preMergeCheck,
|
||||
stateDelta,
|
||||
@@ -12,7 +17,74 @@ import {
|
||||
updateStateWithConflictResolutions,
|
||||
} from '../util';
|
||||
|
||||
const statusCandidateBuilder = createBuilder(statusCandidateSchema);
|
||||
const baseModelBuilder = createBuilder(baseModelSchema);
|
||||
const workspaceModelBuilder = createBuilder(workspaceModelSchema);
|
||||
const mergeConflictBuilder = createBuilder(mergeConflictSchema);
|
||||
const branchBuilder = createBuilder(branchSchema);
|
||||
|
||||
describe('util', () => {
|
||||
beforeEach(() => {
|
||||
statusCandidateBuilder.reset();
|
||||
baseModelBuilder.reset();
|
||||
mergeConflictBuilder.reset();
|
||||
branchBuilder.reset();
|
||||
workspaceModelBuilder.reset();
|
||||
});
|
||||
|
||||
const DA1 = statusCandidateBuilder.reset()
|
||||
.key('a')
|
||||
.document(baseModelBuilder.reset()._id('a').name('1').build())
|
||||
.build();
|
||||
const DA2 = statusCandidateBuilder.reset()
|
||||
.key('a')
|
||||
.document(baseModelBuilder.reset()._id('a').name('2').build())
|
||||
.build();
|
||||
const DB1 = statusCandidateBuilder.reset()
|
||||
.key('b')
|
||||
.document(baseModelBuilder.reset()._id('b').name('1').build())
|
||||
.build();
|
||||
const DB2 = statusCandidateBuilder.reset()
|
||||
.key('b')
|
||||
.document(baseModelBuilder.reset()._id('b').name('2').build())
|
||||
.build();
|
||||
const DC1 = statusCandidateBuilder.reset()
|
||||
.key('c')
|
||||
.document(baseModelBuilder.reset()._id('c').name('1').build())
|
||||
.build();
|
||||
|
||||
const HA1 = hashDocument(DA1.document);
|
||||
const HA2 = hashDocument(DA2.document);
|
||||
const HB1 = hashDocument(DB1.document);
|
||||
const HB2 = hashDocument(DB2.document);
|
||||
const HC1 = hashDocument(DC1.document);
|
||||
|
||||
const A1 = {
|
||||
key: DA1.key,
|
||||
blob: HA1.hash,
|
||||
name: '',
|
||||
};
|
||||
const A2 = {
|
||||
key: DA2.key,
|
||||
blob: HA2.hash,
|
||||
name: '',
|
||||
};
|
||||
const B1 = {
|
||||
key: DB1.key,
|
||||
blob: HB1.hash,
|
||||
name: '',
|
||||
};
|
||||
const B2 = {
|
||||
key: DB2.key,
|
||||
blob: HB2.hash,
|
||||
name: '',
|
||||
};
|
||||
const C1 = {
|
||||
key: DC1.key,
|
||||
blob: HC1.hash,
|
||||
name: '',
|
||||
};
|
||||
|
||||
describe('generateSnapshotStateMap()', () => {
|
||||
it('generates from simple states', async () => {
|
||||
const snapshot = newSnapshot(1, [
|
||||
@@ -351,81 +423,15 @@ describe('util', () => {
|
||||
});
|
||||
|
||||
describe('getStagable()', () => {
|
||||
const A1 = {
|
||||
key: 'a',
|
||||
blob: '1932cc749f57a77ba4a231d62559c47ee931ee2c',
|
||||
name: '',
|
||||
};
|
||||
const A2 = {
|
||||
key: 'a',
|
||||
blob: 'cf2ec18c2e686b4c966725f30050120d42d4b2e5',
|
||||
name: '',
|
||||
};
|
||||
const B1 = {
|
||||
key: 'b',
|
||||
blob: '05ab2ec03c7e32eff27305b4a11c573a3843fa01',
|
||||
name: '',
|
||||
};
|
||||
const B2 = {
|
||||
key: 'b',
|
||||
blob: 'eb157a78ebd367a004c5a32a18d6d8c18e60ce00',
|
||||
name: '',
|
||||
};
|
||||
const C1 = {
|
||||
key: 'c',
|
||||
blob: 'db7d35b96291b823e674bf23ac59e6e116b1890e',
|
||||
name: '',
|
||||
};
|
||||
const DA1 = {
|
||||
key: 'a',
|
||||
document: {
|
||||
id: 'a',
|
||||
v: 1,
|
||||
},
|
||||
name: '',
|
||||
};
|
||||
const DA2 = {
|
||||
key: 'a',
|
||||
document: {
|
||||
id: 'a',
|
||||
v: 2,
|
||||
},
|
||||
name: '',
|
||||
};
|
||||
const DB1 = {
|
||||
key: 'b',
|
||||
document: {
|
||||
id: 'b',
|
||||
v: 1,
|
||||
},
|
||||
name: '',
|
||||
};
|
||||
const DB2 = {
|
||||
key: 'b',
|
||||
document: {
|
||||
id: 'b',
|
||||
v: 2,
|
||||
},
|
||||
name: '',
|
||||
};
|
||||
const DC1 = {
|
||||
key: 'c',
|
||||
document: {
|
||||
id: 'c',
|
||||
v: 1,
|
||||
},
|
||||
name: '',
|
||||
};
|
||||
|
||||
it('works with empty state', () => {
|
||||
const state = [];
|
||||
const dsrd = [DA1];
|
||||
expect(getStagable(state, dsrd)).toEqual([
|
||||
expect(getStagable(state, dsrd)).toEqual<StageEntry[]>([
|
||||
{
|
||||
blobContent: '{"id":"a","v":1}',
|
||||
blobId: A1.blob,
|
||||
key: 'a',
|
||||
name: '',
|
||||
blobContent: HA1.content,
|
||||
blobId: HA1.hash,
|
||||
key: A1.key,
|
||||
name: DA1.name,
|
||||
added: true,
|
||||
},
|
||||
]);
|
||||
@@ -442,10 +448,10 @@ describe('util', () => {
|
||||
const dsrd = [DA1, DB1];
|
||||
expect(getStagable(state, dsrd)).toEqual([
|
||||
{
|
||||
key: 'b',
|
||||
blobId: B1.blob,
|
||||
blobContent: '{"id":"b","v":1}',
|
||||
name: '',
|
||||
key: B1.key,
|
||||
blobId: HB1.hash,
|
||||
blobContent: HB1.content,
|
||||
name: DB1.name,
|
||||
added: true,
|
||||
},
|
||||
]);
|
||||
@@ -456,10 +462,10 @@ describe('util', () => {
|
||||
const dsrd = [DB2];
|
||||
expect(getStagable(state, dsrd)).toEqual([
|
||||
{
|
||||
key: 'b',
|
||||
blobId: B2.blob,
|
||||
blobContent: '{"id":"b","v":2}',
|
||||
name: '',
|
||||
key: B2.key,
|
||||
blobId: HB2.hash,
|
||||
blobContent: HB2.content,
|
||||
name: DB2.name,
|
||||
modified: true,
|
||||
},
|
||||
]);
|
||||
@@ -470,8 +476,8 @@ describe('util', () => {
|
||||
const dsrd = [];
|
||||
expect(getStagable(state, dsrd)).toEqual([
|
||||
{
|
||||
key: 'b',
|
||||
blobId: '05ab2ec03c7e32eff27305b4a11c573a3843fa01',
|
||||
key: B1.key,
|
||||
blobId: HB1.hash,
|
||||
name: '',
|
||||
deleted: true,
|
||||
},
|
||||
@@ -483,23 +489,23 @@ describe('util', () => {
|
||||
const dsrd = [DA2, DB1];
|
||||
expect(getStagable(state, dsrd)).toEqual([
|
||||
{
|
||||
blobContent: '{"id":"a","v":2}',
|
||||
blobId: A2.blob,
|
||||
key: 'a',
|
||||
blobContent: HA2.content,
|
||||
blobId: HA2.hash,
|
||||
key: A2.key,
|
||||
modified: true,
|
||||
name: '',
|
||||
name: DA2.name,
|
||||
},
|
||||
{
|
||||
key: 'c',
|
||||
blobId: 'db7d35b96291b823e674bf23ac59e6e116b1890e',
|
||||
key: C1.key,
|
||||
blobId: HC1.hash,
|
||||
name: '',
|
||||
deleted: true,
|
||||
},
|
||||
{
|
||||
key: 'b',
|
||||
blobId: B1.blob,
|
||||
blobContent: '{"id":"b","v":1}',
|
||||
name: '',
|
||||
key: B1.key,
|
||||
blobId: HB1.hash,
|
||||
blobContent: HB1.content,
|
||||
name: DB1.name,
|
||||
added: true,
|
||||
},
|
||||
]);
|
||||
@@ -548,26 +554,6 @@ describe('util', () => {
|
||||
});
|
||||
|
||||
describe('updateStateWithConflictResolutions()', () => {
|
||||
const A1 = {
|
||||
key: 'a',
|
||||
blob: '1932cc749f57a77ba4a231d62559c47ee931ee2c',
|
||||
name: '',
|
||||
};
|
||||
const A2 = {
|
||||
key: 'a',
|
||||
blob: 'cf2ec18c2e686b4c966725f30050120d42d4b2e5',
|
||||
name: '',
|
||||
};
|
||||
const B1 = {
|
||||
key: 'b',
|
||||
blob: '05ab2ec03c7e32eff27305b4a11c573a3843fa01',
|
||||
name: '',
|
||||
};
|
||||
const C1 = {
|
||||
key: 'c',
|
||||
blob: 'db7d35b96291b823e674bf23ac59e6e116b1890e',
|
||||
name: '',
|
||||
};
|
||||
const D1 = {
|
||||
key: 'd',
|
||||
blob: 'f23ac59e6e11db7d35b96291b823e674b6b1890e',
|
||||
@@ -587,93 +573,40 @@ describe('util', () => {
|
||||
it('does it', () => {
|
||||
const state = [A1, B1, E1, F1];
|
||||
const conflicts = [
|
||||
{
|
||||
key: A1.key,
|
||||
name: A1.name,
|
||||
mineBlob: A1.blob,
|
||||
theirsBlob: A2.blob,
|
||||
choose: A1.blob,
|
||||
},
|
||||
{
|
||||
key: B1.key,
|
||||
name: B1.name,
|
||||
mineBlob: B1.blob,
|
||||
theirsBlob: null,
|
||||
choose: null,
|
||||
},
|
||||
{
|
||||
key: C1.key,
|
||||
name: C1.name,
|
||||
mineBlob: null,
|
||||
theirsBlob: C1.blob,
|
||||
choose: C1.blob,
|
||||
},
|
||||
{
|
||||
key: D1.key,
|
||||
name: D1.name,
|
||||
mineBlob: D1.blob,
|
||||
theirsBlob: null,
|
||||
choose: null,
|
||||
},
|
||||
mergeConflictBuilder.reset()
|
||||
.key(A1.key)
|
||||
.name(A1.name)
|
||||
.mineBlob(A1.blob)
|
||||
.theirsBlob(A2.blob)
|
||||
.choose(A1.blob)
|
||||
.build(),
|
||||
mergeConflictBuilder.reset()
|
||||
.key(B1.key)
|
||||
.name(B1.name)
|
||||
.mineBlob(B1.blob)
|
||||
.theirsBlob(null)
|
||||
.choose(null)
|
||||
.build(),
|
||||
mergeConflictBuilder.reset()
|
||||
.key(C1.key)
|
||||
.name(C1.name)
|
||||
.mineBlob(null)
|
||||
.theirsBlob(C1.blob)
|
||||
.choose(C1.blob)
|
||||
.build(),
|
||||
mergeConflictBuilder.reset()
|
||||
.key(D1.key)
|
||||
.name(D1.name)
|
||||
.mineBlob(D1.blob)
|
||||
.theirsBlob(null)
|
||||
.choose(null)
|
||||
.build(),
|
||||
];
|
||||
expect(updateStateWithConflictResolutions(state, conflicts)).toEqual([A1, E1, F1, C1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('preMergeCheck()', () => {
|
||||
const A1 = {
|
||||
key: 'a',
|
||||
blob: '1932cc749f57a77ba4a231d62559c47ee931ee2c',
|
||||
name: '',
|
||||
};
|
||||
const A2 = {
|
||||
key: 'a',
|
||||
blob: 'cf2ec18c2e686b4c966725f30050120d42d4b2e5',
|
||||
name: '',
|
||||
};
|
||||
const B1 = {
|
||||
key: 'b',
|
||||
blob: '05ab2ec03c7e32eff27305b4a11c573a3843fa01',
|
||||
name: '',
|
||||
};
|
||||
const B2 = {
|
||||
key: 'b',
|
||||
blob: 'eb157a78ebd367a004c5a32a18d6d8c18e60ce00',
|
||||
name: '',
|
||||
};
|
||||
const DA1 = {
|
||||
key: 'a',
|
||||
document: {
|
||||
id: 'a',
|
||||
v: 1,
|
||||
},
|
||||
name: '',
|
||||
};
|
||||
const DA2 = {
|
||||
key: 'a',
|
||||
document: {
|
||||
id: 'a',
|
||||
v: 2,
|
||||
},
|
||||
name: '',
|
||||
};
|
||||
const DB1 = {
|
||||
key: 'b',
|
||||
document: {
|
||||
id: 'b',
|
||||
v: 1,
|
||||
},
|
||||
name: '',
|
||||
};
|
||||
const DC1 = {
|
||||
key: 'c',
|
||||
document: {
|
||||
id: 'c',
|
||||
v: 1,
|
||||
},
|
||||
name: '',
|
||||
};
|
||||
|
||||
it('no changes', () => {
|
||||
const trunk = [A1];
|
||||
const other = [A1];
|
||||
@@ -885,14 +818,14 @@ describe('util', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('hashDocument()', () => {
|
||||
describe('hash()', () => {
|
||||
it('ignore object key order', () => {
|
||||
const result1 = hashDocument({
|
||||
const result1 = hash({
|
||||
c: 'c',
|
||||
a: 'a',
|
||||
b: 'b',
|
||||
});
|
||||
const result2 = hashDocument({
|
||||
const result2 = hash({
|
||||
a: 'a',
|
||||
b: 'b',
|
||||
c: 'c',
|
||||
@@ -900,12 +833,12 @@ describe('util', () => {
|
||||
expect(result1.hash).toBe(result2.hash);
|
||||
expect(result1.hash).toBe('fed03259e8027e1fab2e2e57b402671bef7f3eb9');
|
||||
expect(result2.hash).toBe('fed03259e8027e1fab2e2e57b402671bef7f3eb9');
|
||||
expect(result1.content.toString('utf8')).toBe('{"a":"a","b":"b","c":"c"}');
|
||||
expect(result2.content.toString('utf8')).toBe('{"a":"a","b":"b","c":"c"}');
|
||||
expect(result1.content).toBe('{"a":"a","b":"b","c":"c"}');
|
||||
expect(result2.content).toBe('{"a":"a","b":"b","c":"c"}');
|
||||
});
|
||||
|
||||
it('works on recursive things', () => {
|
||||
const result1 = hashDocument({
|
||||
const result1 = hash({
|
||||
arr: [
|
||||
{
|
||||
b: 'b',
|
||||
@@ -919,7 +852,7 @@ describe('util', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const result2 = hashDocument({
|
||||
const result2 = hash({
|
||||
arr: [
|
||||
{
|
||||
b: 'b',
|
||||
@@ -936,79 +869,101 @@ describe('util', () => {
|
||||
expect(result1.hash).toBe(result2.hash);
|
||||
expect(result1.hash).toBe('7eaaa8a03bada54b403aeada681aad6892a28ab3');
|
||||
expect(result2.hash).toBe('7eaaa8a03bada54b403aeada681aad6892a28ab3');
|
||||
expect(result1.content.toString('utf8')).toBe(
|
||||
expect(result1.content).toBe(
|
||||
'{"arr":[{"a":"a","b":"b"}],"obj":{"obj2":{"a":"a","b":"b"}}}',
|
||||
);
|
||||
expect(result2.content.toString('utf8')).toBe(
|
||||
expect(result2.content).toBe(
|
||||
'{"arr":[{"a":"a","b":"b"}],"obj":{"obj2":{"a":"a","b":"b"}}}',
|
||||
);
|
||||
});
|
||||
|
||||
it('array order matters', () => {
|
||||
const result1 = hashDocument(['c', 'a', 'b']);
|
||||
const result2 = hashDocument(['a', 'b', 'c']);
|
||||
const result1 = hash(['c', 'a', 'b']);
|
||||
const result2 = hash(['a', 'b', 'c']);
|
||||
expect(result1.hash).not.toBe(result2.hash);
|
||||
expect(result1.hash).toBe('edf94aad27c26bdcfe7477e0ed68991cbaedf8d8');
|
||||
expect(result2.hash).toBe('e13460afb1e68af030bb9bee8344c274494661fa');
|
||||
expect(result1.content.toString('utf8')).toBe('["c","a","b"]');
|
||||
expect(result2.content.toString('utf8')).toBe('["a","b","c"]');
|
||||
expect(result1.content).toBe('["c","a","b"]');
|
||||
expect(result2.content).toBe('["a","b","c"]');
|
||||
});
|
||||
|
||||
it('works with strange types', () => {
|
||||
const sDate1 = hashDocument(new Date(1541178019555));
|
||||
const sDate2 = hashDocument('2018-11-02T17:00:19.555Z');
|
||||
const sDate1 = hash(new Date(1541178019555));
|
||||
const sDate2 = hash('2018-11-02T17:00:19.555Z');
|
||||
expect(sDate1.hash).toBe(sDate2.hash);
|
||||
expect(sDate1.hash).toBe('ec0a18c47fb39de00204c57b5b492fd408394a01');
|
||||
expect(sDate2.hash).toBe('ec0a18c47fb39de00204c57b5b492fd408394a01');
|
||||
expect(sDate1.content.toString('utf8')).toBe('"2018-11-02T17:00:19.555Z"');
|
||||
expect(sDate2.content.toString('utf8')).toBe('"2018-11-02T17:00:19.555Z"');
|
||||
const sNull2 = hashDocument('null');
|
||||
expect(sDate1.content).toBe('"2018-11-02T17:00:19.555Z"');
|
||||
expect(sDate2.content).toBe('"2018-11-02T17:00:19.555Z"');
|
||||
const sNull2 = hash('null');
|
||||
expect(sNull2.hash).toBe('8c1030365643f1f4b7f00e3d88c0a3c555522b60');
|
||||
expect(sNull2.content.toString('utf8')).toBe('"null"');
|
||||
expect(sNull2.content).toBe('"null"');
|
||||
});
|
||||
|
||||
it('skips non-json types', () => {
|
||||
const sFunc1 = hashDocument({
|
||||
const sFunc1 = hash({
|
||||
a: [0, () => null],
|
||||
fn: () => null,
|
||||
});
|
||||
const sFunc2 = hashDocument({
|
||||
const sFunc2 = hash({
|
||||
a: [0],
|
||||
});
|
||||
expect(sFunc1.hash).toBe(sFunc2.hash);
|
||||
expect(sFunc1.hash).toBe('38b742facb1034438d82cf1e294d9e71051bf120');
|
||||
expect(sFunc2.hash).toBe('38b742facb1034438d82cf1e294d9e71051bf120');
|
||||
expect(sFunc1.content.toString('utf8')).toBe('{"a":[0]}');
|
||||
expect(sFunc2.content.toString('utf8')).toBe('{"a":[0]}');
|
||||
expect(sFunc1.content.toString()).toBe('{"a":[0]}');
|
||||
expect(sFunc2.content.toString()).toBe('{"a":[0]}');
|
||||
});
|
||||
|
||||
it('fails on undefined', () => {
|
||||
try {
|
||||
hashDocument(undefined);
|
||||
hashDocument();
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Expected hashDocument(undefined) to fail');
|
||||
expect(() => hash(undefined)).toThrowError('Cannot hash undefined value');
|
||||
expect(() => hash()).toThrowError('Cannot hash undefined value');
|
||||
});
|
||||
});
|
||||
describe('hashDocument()', () => {
|
||||
it('fails on undefined', () => {
|
||||
expect(() => hashDocument(undefined)).toThrowError('Cannot hash undefined value');
|
||||
expect(() => hashDocument()).toThrowError('Cannot hash undefined value');
|
||||
});
|
||||
|
||||
it('ignores object keys that do not matter', () => {
|
||||
const result1 = hashDocument({
|
||||
a: 'a',
|
||||
modified: 456,
|
||||
});
|
||||
const result2 = hashDocument({
|
||||
a: 'a',
|
||||
modified: 123,
|
||||
});
|
||||
const result3 = hashDocument({
|
||||
a: 'b',
|
||||
modified: 123,
|
||||
});
|
||||
it('ignores global object keys that do not matter', () => {
|
||||
const result1 = hashDocument(
|
||||
baseModelBuilder.modified(123).parentId('abc').build(),
|
||||
);
|
||||
const result2 = hashDocument(
|
||||
baseModelBuilder.modified(456).build(),
|
||||
);
|
||||
const result3 = hashDocument(
|
||||
baseModelBuilder.name('abc').modified(456).build(),
|
||||
);
|
||||
expect(result1.hash).toBe(result2.hash);
|
||||
expect(result1.hash).not.toBe(result3.hash);
|
||||
});
|
||||
|
||||
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
|
||||
const originalSyncedWorkspace = workspaceModelBuilder.parentId(null).build();
|
||||
|
||||
// Existing synced workspaces do not have a modified field
|
||||
// @ts-expect-error delete non-optional
|
||||
delete originalSyncedWorkspace.modified;
|
||||
|
||||
// intentionally hash and not hashDocument, to bypass ignoring logic
|
||||
const original = hash(originalSyncedWorkspace);
|
||||
|
||||
// Act
|
||||
const withParent = hashDocument(
|
||||
workspaceModelBuilder.parentId('abc').build(),
|
||||
);
|
||||
const unique = hashDocument(
|
||||
workspaceModelBuilder.name('unique').parentId('abc').build(),
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(original.hash).toBe(withParent.hash);
|
||||
expect(original.hash).not.toBe(unique.hash);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1032,19 +987,11 @@ function newStateEntry(key, blob) {
|
||||
};
|
||||
}
|
||||
|
||||
function newCandidate(key, n) {
|
||||
return {
|
||||
key,
|
||||
name: `Candidate ${n}`,
|
||||
content: `Content for candidate ${key}.${n}`,
|
||||
};
|
||||
}
|
||||
const newCandidate = (key: string, n: number) => statusCandidateBuilder
|
||||
.reset()
|
||||
.key(key)
|
||||
.name(`Candidate ${n}`)
|
||||
.document(baseModelBuilder.reset().name(`Content for candidate ${key}.${n}`).build())
|
||||
.build();
|
||||
|
||||
function newBranch(snapshots) {
|
||||
return {
|
||||
name: '',
|
||||
created: new Date(0),
|
||||
modified: new Date(0),
|
||||
snapshots,
|
||||
};
|
||||
}
|
||||
const newBranch = (snapshots: string[]) => branchBuilder.snapshots(snapshots).build();
|
||||
@@ -0,0 +1,44 @@
|
||||
import { BaseModel } from '../../models';
|
||||
import { isWorkspace } from '../../models/helpers/is-model';
|
||||
import { Workspace } from '../../models/workspace';
|
||||
|
||||
// Key for VCS to delete before computing changes
|
||||
const DELETE_KEY: keyof BaseModel = 'modified';
|
||||
|
||||
type ResetModelKeys<T extends BaseModel> = {
|
||||
[K in keyof T]?: T[K] | null;
|
||||
}
|
||||
|
||||
// Keys for VCS to reset before computing changes
|
||||
// We can't always delete keys
|
||||
// If we delete a key that previously existed (even as null), we then have a different hash because of the deletion
|
||||
// Therefore, we have to set it back to a default value
|
||||
const RESET_WORKSPACE_KEYS: ResetModelKeys<Workspace> = {
|
||||
parentId: null,
|
||||
};
|
||||
|
||||
export const shouldIgnoreKey = <T extends BaseModel>(key: keyof T, doc: T) => {
|
||||
if (key === DELETE_KEY) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isWorkspace(doc)) {
|
||||
return key in RESET_WORKSPACE_KEYS;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const deleteKeys = <T extends BaseModel>(doc: T) => {
|
||||
// @ts-expect-error force delete the key even if it is required
|
||||
delete doc[DELETE_KEY];
|
||||
};
|
||||
|
||||
export const resetKeys = <T extends BaseModel>(doc: T) => {
|
||||
if (isWorkspace(doc)) {
|
||||
Object.keys(RESET_WORKSPACE_KEYS)
|
||||
.forEach(key => {
|
||||
doc[key] = RESET_WORKSPACE_KEYS[key];
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
import { DEFAULT_BRANCH_NAME } from '../../common/constants';
|
||||
import { isWorkspace } from '../../models/helpers/is-model';
|
||||
import { Workspace } from '../../models/workspace';
|
||||
import { Project } from '../types';
|
||||
import * as models from '../../models';
|
||||
import { database } from '../../common/database';
|
||||
import { VCS } from './vcs';
|
||||
|
||||
interface Options {
|
||||
vcs: VCS;
|
||||
project: Project;
|
||||
spaceId?: string;
|
||||
}
|
||||
|
||||
export const pullProject = async ({ vcs, project, spaceId }: Options) => {
|
||||
// Set project, checkout master, and pull
|
||||
await vcs.setProject(project);
|
||||
await vcs.checkout([], DEFAULT_BRANCH_NAME);
|
||||
const remoteBranches = await vcs.getRemoteBranches();
|
||||
const defaultBranchMissing = !remoteBranches.includes(DEFAULT_BRANCH_NAME);
|
||||
|
||||
const workspaceParentId = spaceId || null;
|
||||
|
||||
// The default branch does not exist, so we create it and the workspace locally
|
||||
if (defaultBranchMissing) {
|
||||
const workspace: Workspace = await models.initModel(models.workspace.type, {
|
||||
_id: project.rootDocumentId,
|
||||
name: project.name,
|
||||
parentId: workspaceParentId,
|
||||
});
|
||||
await database.upsert(workspace);
|
||||
} else {
|
||||
await vcs.pull([]); // 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);
|
||||
}
|
||||
|
||||
await database.flushChanges(flushId);
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import clone from 'clone';
|
||||
import crypto from 'crypto';
|
||||
import { BaseModel } from '../../models';
|
||||
import { deterministicStringify } from '../lib/deterministicStringify';
|
||||
import type {
|
||||
Branch,
|
||||
@@ -13,8 +14,7 @@ import type {
|
||||
StatusCandidate,
|
||||
StatusCandidateMap,
|
||||
} from '../types';
|
||||
// Keys for VCS to ignore when computing changes
|
||||
const IGNORED_KEYS = ['modified'];
|
||||
import { deleteKeys, resetKeys, shouldIgnoreKey } from './ignore-keys';
|
||||
|
||||
export function generateSnapshotStateMap(snapshot: Snapshot | null): SnapshotStateMap {
|
||||
if (!snapshot) {
|
||||
@@ -451,24 +451,16 @@ export function preMergeCheck(
|
||||
};
|
||||
}
|
||||
|
||||
export function hashDocument(
|
||||
doc: Record<string, any>,
|
||||
): {
|
||||
// Intentionally any to handle strange types, see unit tests
|
||||
export function hash(obj?: any): {
|
||||
content: string;
|
||||
hash: string;
|
||||
} {
|
||||
if (!doc) {
|
||||
if (!obj) {
|
||||
throw new Error('Cannot hash undefined value');
|
||||
}
|
||||
|
||||
// Remove fields we don't care about for sync purposes
|
||||
const newDoc = clone(doc);
|
||||
|
||||
for (const key of IGNORED_KEYS) {
|
||||
delete newDoc[key];
|
||||
}
|
||||
|
||||
const content = deterministicStringify(newDoc);
|
||||
const content = deterministicStringify(obj);
|
||||
const hash = crypto.createHash('sha1').update(content).digest('hex');
|
||||
return {
|
||||
hash,
|
||||
@@ -476,6 +468,18 @@ export function hashDocument(
|
||||
};
|
||||
}
|
||||
|
||||
export function hashDocument(doc?: BaseModel) {
|
||||
// Remove fields we don't care about for sync purposes
|
||||
const newDoc = clone(doc);
|
||||
|
||||
if (newDoc) {
|
||||
deleteKeys(newDoc);
|
||||
resetKeys(newDoc);
|
||||
}
|
||||
|
||||
return hash(newDoc);
|
||||
}
|
||||
|
||||
export function updateStateWithConflictResolutions(state: SnapshotState, conflicts: MergeConflict[]) {
|
||||
const newStateMap = generateStateMap(state);
|
||||
|
||||
@@ -509,7 +513,7 @@ export function updateStateWithConflictResolutions(state: SnapshotState, conflic
|
||||
return Object.keys(newStateMap).map(k => newStateMap[k]);
|
||||
}
|
||||
|
||||
export function describeChanges(a: any, b: any): string[] {
|
||||
export function describeChanges<T extends BaseModel>(a: T, b: T): string[] {
|
||||
const aT = Object.prototype.toString.call(a);
|
||||
const bT = Object.prototype.toString.call(b);
|
||||
|
||||
@@ -521,7 +525,7 @@ export function describeChanges(a: any, b: any): string[] {
|
||||
const allKeys = [...Object.keys({ ...a, ...b })];
|
||||
|
||||
for (const key of allKeys) {
|
||||
if (IGNORED_KEYS.includes(key)) {
|
||||
if (shouldIgnoreKey(key as keyof T, a)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
generateCandidateMap,
|
||||
getRootSnapshot,
|
||||
getStagable,
|
||||
hash,
|
||||
hashDocument,
|
||||
preMergeCheck,
|
||||
stateDelta,
|
||||
@@ -34,6 +35,7 @@ import * as crypt from '../../account/crypt';
|
||||
import * as session from '../../account/session';
|
||||
import * as fetch from '../../account/fetch';
|
||||
import { strings } from '../../common/strings';
|
||||
import { BaseModel } from '../../models';
|
||||
|
||||
const EMPTY_HASH = crypto.createHash('sha1').digest('hex').replace(/./g, '0');
|
||||
|
||||
@@ -154,10 +156,7 @@ export class VCS {
|
||||
return {
|
||||
stage,
|
||||
unstaged,
|
||||
key: hashDocument({
|
||||
stage,
|
||||
unstaged,
|
||||
}).hash,
|
||||
key: hash({ stage, unstaged }).hash,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1541,9 +1540,9 @@ export class VCS {
|
||||
await this._store.setItem(paths.head(this._projectId()), head);
|
||||
}
|
||||
|
||||
async _getBlob(id: string) {
|
||||
_getBlob(id: string) {
|
||||
const p = paths.blob(this._projectId(), id);
|
||||
return this._store.getItem(p) as Record<string, any> | null;
|
||||
return this._store.getItem(p) as Promise<BaseModel | null>;
|
||||
}
|
||||
|
||||
async _getBlobs(ids: string[]) {
|
||||
|
||||
@@ -1,194 +1,81 @@
|
||||
import React, { Component } from 'react';
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import { AUTOBIND_CFG } from '../../../common/constants';
|
||||
import * as session from '../../../account/session';
|
||||
import React, { FC } from 'react';
|
||||
import { VCS } from '../../../sync/vcs/vcs';
|
||||
import type { Project } from '../../../sync/types';
|
||||
import { Dropdown, DropdownDivider, DropdownItem, Button, Tooltip } from 'insomnia-components';
|
||||
import type { Workspace } from '../../../models/workspace';
|
||||
import HelpTooltip from '../help-tooltip';
|
||||
import * as models from '../../../models';
|
||||
import { database as db } from '../../../common/database';
|
||||
import { showAlert } from '../modals';
|
||||
import { strings } from '../../../common/strings';
|
||||
import { isLoggedIn } from '../../../account/session';
|
||||
import { useRemoteWorkspaces } from '../../hooks/workspace';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { selectActiveSpace } from '../../redux/selectors';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
vcs?: VCS | null;
|
||||
workspaces: Workspace[];
|
||||
}
|
||||
|
||||
interface State {
|
||||
loading: boolean;
|
||||
localProjects: Project[];
|
||||
pullingProjects: Record<string, boolean>;
|
||||
remoteProjects: Project[];
|
||||
}
|
||||
const PullButton: FC<{disabled?: boolean, className?: string}> = ({ disabled, className }) => (
|
||||
<Button className={className} disabled={disabled}>
|
||||
Pull
|
||||
<i className="fa fa-caret-down pad-left-sm" />
|
||||
</Button>
|
||||
);
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
class RemoteWorkspacesDropdown extends Component<Props, State> {
|
||||
state: State = {
|
||||
loading: false,
|
||||
localProjects: [],
|
||||
pullingProjects: {},
|
||||
remoteProjects: [],
|
||||
export const RemoteWorkspacesDropdown: FC<Props> = ({ className, vcs }) => {
|
||||
const {
|
||||
loading,
|
||||
refresh,
|
||||
missingProjects,
|
||||
pullingProjects,
|
||||
pull,
|
||||
} = useRemoteWorkspaces(vcs || undefined);
|
||||
|
||||
const isRemoteSpace = Boolean(useSelector(selectActiveSpace)?.remoteId);
|
||||
|
||||
// Don't show the pull dropdown if we are not in a remote space
|
||||
if (!isRemoteSpace) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async _refreshRemoteWorkspaces() {
|
||||
const { vcs } = this.props;
|
||||
|
||||
if (!vcs) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session.isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
const remoteProjects = await vcs.remoteProjects();
|
||||
const localProjects = await vcs.localProjects();
|
||||
this.setState({
|
||||
remoteProjects,
|
||||
localProjects,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
async _handlePullRemoteWorkspace(project: Project) {
|
||||
const { vcs } = this.props;
|
||||
|
||||
if (!vcs) {
|
||||
throw new Error('VCS is not defined');
|
||||
}
|
||||
|
||||
this.setState(state => ({
|
||||
pullingProjects: { ...state.pullingProjects, [project.id]: true },
|
||||
}));
|
||||
|
||||
try {
|
||||
// Clone old VCS so we don't mess anything up while working on other projects
|
||||
const newVCS = vcs.newInstance();
|
||||
// Remove all projects for workspace first
|
||||
await newVCS.removeProjectsForRoot(project.rootDocumentId);
|
||||
// Set project, checkout master, and pull
|
||||
const defaultBranch = 'master';
|
||||
await newVCS.setProject(project);
|
||||
await newVCS.checkout([], defaultBranch);
|
||||
const remoteBranches = await newVCS.getRemoteBranches();
|
||||
const defaultBranchMissing = !remoteBranches.includes(defaultBranch);
|
||||
|
||||
// The default branch does not exist, so we create it and the workspace locally
|
||||
if (defaultBranchMissing) {
|
||||
const workspace: Workspace = await models.initModel(models.workspace.type, {
|
||||
_id: project.rootDocumentId,
|
||||
name: project.name,
|
||||
});
|
||||
await db.upsert(workspace);
|
||||
} else {
|
||||
await newVCS.pull([]); // There won't be any existing docs since it's a new pull
|
||||
|
||||
const flushId = await db.bufferChanges();
|
||||
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
for (const doc of (await newVCS.allDocuments() || [])) {
|
||||
await db.upsert(doc);
|
||||
}
|
||||
|
||||
await db.flushChanges(flushId);
|
||||
}
|
||||
|
||||
await this._refreshRemoteWorkspaces();
|
||||
} catch (err) {
|
||||
// @ts-expect-error -- TSCONVERSION this appears to be a genuine error because there's no _dropdown assigned elsewhere in this class and it doesn't extend another class that has _dropdown and even if it did it's supposed to be private presumably so....
|
||||
this._dropdown.hide();
|
||||
showAlert({
|
||||
title: 'Pull Error',
|
||||
message: `Failed to pull workspace. ${err.message}`,
|
||||
});
|
||||
}
|
||||
|
||||
this.setState(state => ({
|
||||
pullingProjects: { ...state.pullingProjects, [project.id]: false },
|
||||
}));
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
// Reload workspaces if we just got a new VCS instance
|
||||
if (this.props.vcs && !prevProps.vcs) {
|
||||
this._refreshRemoteWorkspaces();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._refreshRemoteWorkspaces();
|
||||
}
|
||||
|
||||
renderButton(disabled = false) {
|
||||
// Show a disabled button if remote space but not logged in
|
||||
if (!isLoggedIn()) {
|
||||
return (
|
||||
<Button className={this.props.className} disabled={disabled}>
|
||||
Pull
|
||||
<i className="fa fa-caret-down pad-left-sm" />
|
||||
</Button>
|
||||
<Tooltip message="Please log in to access your remote collections" position="bottom">
|
||||
<PullButton className={className} disabled />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { workspaces } = this.props;
|
||||
const { loading, remoteProjects, localProjects, pullingProjects } = this.state;
|
||||
|
||||
if (!session.isLoggedIn()) {
|
||||
return (
|
||||
<Tooltip message="Please log in to access your remote collections" position="bottom">
|
||||
{this.renderButton(true)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
const missingRemoteProjects = remoteProjects.filter(({ id, rootDocumentId }) => {
|
||||
const localProjectExists = localProjects.find(p => p.id === id);
|
||||
const workspaceExists = workspaces.find(w => w._id === rootDocumentId);
|
||||
// Mark as missing if:
|
||||
// - the project doesn't yet exists locally
|
||||
// - the project exists locally but somehow the workspace doesn't anymore
|
||||
return !(workspaceExists && localProjectExists);
|
||||
});
|
||||
const button = this.renderButton();
|
||||
return (
|
||||
<Dropdown onOpen={this._refreshRemoteWorkspaces} renderButton={button}>
|
||||
<DropdownDivider>
|
||||
Remote {strings.collection.plural}
|
||||
<HelpTooltip>
|
||||
These {strings.collection.plural.toLowerCase()} have been shared with you via Insomnia
|
||||
Sync and do not yet exist on your machine.
|
||||
</HelpTooltip>{' '}
|
||||
{loading && <i className="fa fa-spin fa-refresh" />}
|
||||
</DropdownDivider>
|
||||
{missingRemoteProjects.length === 0 && (
|
||||
<DropdownItem disabled>Nothing to pull</DropdownItem>
|
||||
)}
|
||||
{missingRemoteProjects.map(p => (
|
||||
<DropdownItem
|
||||
key={p.id}
|
||||
stayOpenAfterClick
|
||||
onClick={() => this._handlePullRemoteWorkspace(p)}
|
||||
icon={
|
||||
pullingProjects[p.id] ? (
|
||||
<i className="fa fa-refresh fa-spin" />
|
||||
) : (
|
||||
<i className="fa fa-cloud-download" />
|
||||
)
|
||||
}>
|
||||
<span>
|
||||
Pull <strong>{p.name}</strong>
|
||||
</span>
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RemoteWorkspacesDropdown;
|
||||
return (
|
||||
<Dropdown onOpen={refresh} renderButton={<PullButton className={className} />}>
|
||||
<DropdownDivider>
|
||||
Remote {strings.collection.plural}
|
||||
<HelpTooltip>
|
||||
These {strings.collection.plural.toLowerCase()} have been shared with you via Insomnia
|
||||
Sync and do not yet exist on your machine.
|
||||
</HelpTooltip>{' '}
|
||||
{loading && <i className="fa fa-spin fa-refresh" />}
|
||||
</DropdownDivider>
|
||||
{missingProjects.length === 0 && (
|
||||
<DropdownItem disabled>Nothing to pull</DropdownItem>
|
||||
)}
|
||||
{missingProjects.map(p => (
|
||||
<DropdownItem
|
||||
key={p.id}
|
||||
stayOpenAfterClick
|
||||
value={p}
|
||||
onClick={pull}
|
||||
icon={
|
||||
pullingProjects[p.id] ? (
|
||||
<i className="fa fa-refresh fa-spin" />
|
||||
) : (
|
||||
<i className="fa fa-cloud-download" />
|
||||
)
|
||||
}>
|
||||
<span>
|
||||
Pull <strong>{p.name}</strong>
|
||||
</span>
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { Fragment, PureComponent } from 'react';
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import { AUTOBIND_CFG } from '../../../common/constants';
|
||||
import { AUTOBIND_CFG, DEFAULT_BRANCH_NAME } from '../../../common/constants';
|
||||
import classnames from 'classnames';
|
||||
import { Dropdown, DropdownButton, DropdownDivider, DropdownItem } from '../base/dropdown';
|
||||
import type { Workspace } from '../../../models/workspace';
|
||||
@@ -23,14 +23,16 @@ import { database as db } from '../../../common/database';
|
||||
import * as models from '../../../models';
|
||||
import { docsVersionControl } from '../../../common/documentation';
|
||||
import { strings } from '../../../common/strings';
|
||||
import { pullProject } from '../../../sync/vcs/pull-project';
|
||||
import { Space } from '../../../models/space';
|
||||
// Stop refreshing if user hasn't been active in this long
|
||||
const REFRESH_USER_ACTIVITY = 1000 * 60 * 10;
|
||||
// Refresh dropdown periodically
|
||||
const REFRESH_PERIOD = 1000 * 60 * 1;
|
||||
const DEFAULT_BRANCH_NAME = 'master';
|
||||
|
||||
interface Props {
|
||||
workspace: Workspace;
|
||||
space: Space | null;
|
||||
vcs: VCS;
|
||||
syncItems: StatusCandidate[];
|
||||
className?: string;
|
||||
@@ -286,35 +288,12 @@ class SyncDropdown extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
async _handleSetProject(p: Project) {
|
||||
const { vcs } = this.props;
|
||||
const { vcs, space } = this.props;
|
||||
this.setState({
|
||||
loadingProjectPull: true,
|
||||
});
|
||||
await vcs.setProject(p);
|
||||
await vcs.checkout([], DEFAULT_BRANCH_NAME);
|
||||
const remoteBranches = await vcs.getRemoteBranches();
|
||||
const defaultBranchMissing = !remoteBranches.includes(DEFAULT_BRANCH_NAME);
|
||||
|
||||
// The default branch does not exist, so we create it and the workspace locally
|
||||
if (defaultBranchMissing) {
|
||||
const workspace: Workspace = await models.initModel(models.workspace.type, {
|
||||
_id: p.rootDocumentId,
|
||||
name: p.name,
|
||||
});
|
||||
await db.upsert(workspace);
|
||||
} else {
|
||||
// Pull changes
|
||||
await vcs.pull([]); // There won't be any existing docs since it's a new pull
|
||||
|
||||
const flushId = await db.bufferChanges();
|
||||
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
for (const doc of await vcs.allDocuments()) {
|
||||
await db.upsert(doc);
|
||||
}
|
||||
|
||||
await db.flushChanges(flushId);
|
||||
}
|
||||
await pullProject({ vcs, project: p, spaceId: space?._id });
|
||||
|
||||
await this.refreshMainAttributes({
|
||||
loadingProjectPull: false,
|
||||
|
||||
@@ -13,6 +13,7 @@ import Tooltip from '../tooltip';
|
||||
import IndeterminateCheckbox from '../base/indeterminate-checkbox';
|
||||
import { describeChanges } from '../../../sync/vcs/util';
|
||||
import { strings } from '../../../common/strings';
|
||||
import { BaseModel } from '../../../models';
|
||||
|
||||
interface Props {
|
||||
workspace: Workspace;
|
||||
@@ -155,7 +156,7 @@ class SyncStagingModal extends PureComponent<Props, State> {
|
||||
|
||||
for (const key of allKeys) {
|
||||
const item = syncItems.find(si => si.key === key);
|
||||
const oldDoc: Record<string, any> | null = await vcs.blobFromLastSnapshot(key);
|
||||
const oldDoc: BaseModel | null = await vcs.blobFromLastSnapshot(key);
|
||||
const doc = (item && item.document) || oldDoc;
|
||||
const entry = status.stage[key] || status.unstaged[key];
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ interface Props {
|
||||
class WrapperDebug extends PureComponent<Props> {
|
||||
_renderPageHeader() {
|
||||
const { wrapperProps, gitSyncDropdown, handleActivityChange } = this.props;
|
||||
const { vcs, activeWorkspace, syncItems } = this.props.wrapperProps;
|
||||
const { vcs, activeWorkspace, activeSpace, syncItems } = this.props.wrapperProps;
|
||||
const collection = isCollection(activeWorkspace);
|
||||
const design = isDesign(activeWorkspace);
|
||||
const share = session.isLoggedIn() && collection && (
|
||||
@@ -67,7 +67,7 @@ class WrapperDebug extends PureComponent<Props> {
|
||||
</Button>
|
||||
);
|
||||
const betaSync = collection && vcs && isLoggedIn() && (
|
||||
<SyncDropdown workspace={activeWorkspace} vcs={vcs} syncItems={syncItems} />
|
||||
<SyncDropdown workspace={activeWorkspace} space={activeSpace} vcs={vcs} syncItems={syncItems} />
|
||||
);
|
||||
const gitSync = design && gitSyncDropdown;
|
||||
const sync = betaSync || gitSync;
|
||||
|
||||
@@ -41,7 +41,7 @@ import PageLayout from './page-layout';
|
||||
import { ForceToWorkspaceKeys } from '../redux/modules/helpers';
|
||||
import coreLogo from '../images/insomnia-core-logo.png';
|
||||
import { parseApiSpec, ParsedApiSpec } from '../../common/api-specs';
|
||||
import RemoteWorkspacesDropdown from './dropdowns/remote-workspaces-dropdown';
|
||||
import { RemoteWorkspacesDropdown } from './dropdowns/remote-workspaces-dropdown';
|
||||
import SettingsButton from './buttons/settings-button';
|
||||
import AccountDropdown from './dropdowns/account-dropdown';
|
||||
import { strings } from '../../common/strings';
|
||||
@@ -335,7 +335,7 @@ class WrapperHome extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
renderDashboardMenu() {
|
||||
const { vcs, workspaces } = this.props.wrapperProps;
|
||||
const { vcs } = this.props.wrapperProps;
|
||||
return (
|
||||
<div className="row row--right pad-left wide">
|
||||
<div
|
||||
@@ -354,7 +354,7 @@ class WrapperHome extends PureComponent<Props, State> {
|
||||
<span className="fa fa-search filter-icon" />
|
||||
</KeydownBinder>
|
||||
</div>
|
||||
<RemoteWorkspacesDropdown vcs={vcs} workspaces={workspaces} className="margin-left" />
|
||||
<RemoteWorkspacesDropdown vcs={vcs} className="margin-left" />
|
||||
{this.renderCreateMenu()}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -99,6 +99,7 @@ import WrapperAnalytics from './wrapper-analytics';
|
||||
import { HandleGetRenderContext, HandleRender } from '../../common/render';
|
||||
import { RequestGroup } from '../../models/request-group';
|
||||
import SpaceSettingsModal from './modals/space-settings-modal';
|
||||
import { Space } from '../../models/space';
|
||||
|
||||
const spectral = new Spectral();
|
||||
|
||||
@@ -173,6 +174,7 @@ export interface WrapperProps {
|
||||
activeWorkspaceMeta?: WorkspaceMeta;
|
||||
environments: Environment[];
|
||||
activeApiSpec: ApiSpec;
|
||||
activeSpace: Space | null;
|
||||
activeUnitTestSuite: UnitTestSuite | null;
|
||||
activeRequestResponses: Response[];
|
||||
activeWorkspace: Workspace;
|
||||
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
selectActiveRequestMeta,
|
||||
selectActiveRequestResponses,
|
||||
selectActiveResponse,
|
||||
selectActiveSpace,
|
||||
selectActiveUnitTestResult,
|
||||
selectActiveUnitTests,
|
||||
selectActiveUnitTestSuite,
|
||||
@@ -1686,6 +1687,8 @@ function mapStateToProps(state, props) {
|
||||
const settings = entitiesLists.settings[0];
|
||||
// Workspace stuff
|
||||
|
||||
// @ts-expect-error -- TSCONVERSION https://github.com/reduxjs/reselect#accessing-react-props-in-selectors
|
||||
const activeSpace = selectActiveSpace(state, props);
|
||||
// @ts-expect-error -- TSCONVERSION https://github.com/reduxjs/reselect#accessing-react-props-in-selectors
|
||||
const workspaces = selectWorkspacesForActiveSpace(state, props);
|
||||
// @ts-expect-error -- TSCONVERSION https://github.com/reduxjs/reselect#accessing-react-props-in-selectors
|
||||
@@ -1749,6 +1752,7 @@ function mapStateToProps(state, props) {
|
||||
const activeUnitTestResult = selectActiveUnitTestResult(state, props);
|
||||
return Object.assign({}, state, {
|
||||
activity: activeActivity,
|
||||
activeSpace,
|
||||
activeApiSpec,
|
||||
activeCookieJar,
|
||||
activeEnvironment,
|
||||
|
||||
@@ -28,6 +28,7 @@ export const useRemoteSpaces = (vcs?: VCS) => {
|
||||
}
|
||||
}, [vcs]);
|
||||
|
||||
// If the refresh callback changes, refresh
|
||||
useEffect(() => {
|
||||
(async () => { await refresh(); })();
|
||||
}, [refresh]);
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { isLoggedIn } from '../../account/session';
|
||||
import { Project } from '../../sync/types';
|
||||
import { VCS } from '../../sync/vcs/vcs';
|
||||
import { pullProject } from '../../sync/vcs/pull-project';
|
||||
import { showAlert } from '../components/modals';
|
||||
import { selectActiveSpace, selectAllWorkspaces } from '../redux/selectors';
|
||||
|
||||
export const useRemoteWorkspaces = (vcs?: VCS) => {
|
||||
// Fetch from redux
|
||||
const workspaces = useSelector(selectAllWorkspaces);
|
||||
const activeSpace = useSelector(selectActiveSpace);
|
||||
const spaceRemoteId = activeSpace?.remoteId || undefined;
|
||||
const spaceId = activeSpace?._id;
|
||||
|
||||
// Local state
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [localProjects, setLocalProjects] = useState<Project[]>([]);
|
||||
const [remoteProjects, setRemoteProjects] = useState<Project[]>([]);
|
||||
const [pullingProjects, setPullingProjects] = useState<Record<string, boolean>>({});
|
||||
|
||||
// Refresh remote spaces
|
||||
const refresh = useCallback(async () => {
|
||||
if (!vcs || !isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
const remote = await vcs.remoteProjects(spaceRemoteId);
|
||||
const local = await vcs.localProjects();
|
||||
setRemoteProjects(remote);
|
||||
setLocalProjects(local);
|
||||
setLoading(false);
|
||||
},
|
||||
[spaceRemoteId, vcs]);
|
||||
|
||||
// Find remote spaces that haven't been pulled
|
||||
const missingProjects = useMemo(() => remoteProjects.filter(({ id, rootDocumentId }) => {
|
||||
const localProjectExists = localProjects.find(p => p.id === id);
|
||||
const workspaceExists = workspaces.find(w => w._id === rootDocumentId);
|
||||
// Mark as missing if:
|
||||
// - the project doesn't yet exists locally
|
||||
// - the project exists locally but somehow the workspace doesn't anymore
|
||||
return !(workspaceExists && localProjectExists);
|
||||
}), [localProjects, remoteProjects, workspaces]);
|
||||
|
||||
// Pull a remote space
|
||||
const pull = useCallback(async (project: Project) => {
|
||||
if (!vcs) {
|
||||
throw new Error('VCS is not defined');
|
||||
}
|
||||
|
||||
setPullingProjects(state => ({ ...state, [project.id]: true }));
|
||||
|
||||
try {
|
||||
// Clone old VCS so we don't mess anything up while working on other projects
|
||||
const newVCS = vcs.newInstance();
|
||||
// Remove all projects for workspace first
|
||||
await newVCS.removeProjectsForRoot(project.rootDocumentId);
|
||||
|
||||
await pullProject({ vcs: newVCS, project, spaceId });
|
||||
|
||||
await refresh();
|
||||
} catch (err) {
|
||||
showAlert({
|
||||
title: 'Pull Error',
|
||||
message: `Failed to pull workspace. ${err.message}`,
|
||||
});
|
||||
} finally {
|
||||
setPullingProjects(state => ({ ...state, [project.id]: false }));
|
||||
}
|
||||
}, [vcs, refresh, spaceId]);
|
||||
|
||||
// If the refresh callback changes, refresh
|
||||
useEffect(() => {
|
||||
(async () => { await refresh(); })();
|
||||
}, [refresh]);
|
||||
|
||||
return {
|
||||
loading,
|
||||
missingProjects,
|
||||
pullingProjects,
|
||||
refresh,
|
||||
pull,
|
||||
};
|
||||
};
|
||||
@@ -1,8 +1,10 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import * as models from '../../models';
|
||||
import { BaseModel } from '../../models';
|
||||
import { Space } from '../../models/space';
|
||||
import { UnitTestResult } from '../../models/unit-test-result';
|
||||
import { Workspace } from '../../models/workspace';
|
||||
import { StatusCandidate } from '../../sync/types';
|
||||
|
||||
// ~~~~~~~~~ //
|
||||
// Selectors //
|
||||
@@ -150,7 +152,7 @@ export const selectActiveWorkspaceEntities = createSelector(
|
||||
selectActiveWorkspace,
|
||||
selectEntitiesChildrenMap,
|
||||
(activeWorkspace, childrenMap) => {
|
||||
const descendants = [activeWorkspace];
|
||||
const descendants: BaseModel[] = [activeWorkspace];
|
||||
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
const addChildrenOf = parent => {
|
||||
@@ -226,6 +228,15 @@ export const selectActiveOAuth2Token = createSelector(
|
||||
return entities.oAuth2Tokens.find(t => t.parentId === id);
|
||||
},
|
||||
);
|
||||
|
||||
export const selectAllWorkspaces = createSelector<any, {}, Workspace[]>(
|
||||
selectEntitiesLists,
|
||||
entities => {
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
const { workspaces } = entities;
|
||||
return workspaces;
|
||||
});
|
||||
|
||||
export const selectUnseenWorkspaces = createSelector(selectEntitiesLists, entities => {
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
const { workspaces, workspaceMetas } = entities;
|
||||
@@ -345,10 +356,15 @@ export const selectActiveUnitTestSuites = createSelector(
|
||||
return entities.unitTestSuites.filter(s => s.parentId === activeWorkspace._id);
|
||||
},
|
||||
);
|
||||
export const selectSyncItems = createSelector(selectActiveWorkspaceEntities, workspaceEntities =>
|
||||
workspaceEntities.filter(models.canSync).map(doc => ({
|
||||
key: doc._id,
|
||||
name: doc.name || '',
|
||||
document: doc,
|
||||
})),
|
||||
|
||||
export const selectSyncItems = createSelector(
|
||||
selectActiveWorkspaceEntities,
|
||||
workspaceEntities =>
|
||||
workspaceEntities
|
||||
.filter(models.canSync)
|
||||
.map<StatusCandidate>(doc => ({
|
||||
key: doc._id,
|
||||
name: doc.name || '',
|
||||
document: doc,
|
||||
})),
|
||||
);
|
||||
@@ -16,7 +16,12 @@ const { readFile, writeFile } = promises;
|
||||
// Start build if ran from CLI
|
||||
if (require.main === module) {
|
||||
process.nextTick(async () => {
|
||||
await module.exports.start(false);
|
||||
try {
|
||||
await module.exports.start(false);
|
||||
} catch (err) {
|
||||
console.log('[build] ERROR:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user