mirror of
https://github.com/Kong/insomnia
synced 2024-11-08 06:39:48 +00:00
Universal Insomnia/Git Sync for all files (documents/collections) (#5945)
* share design documents and tests with insomnia sync * share collection and documents with insomnia/git sync * checkpoint * redirect when cloning (pulling) a new remote project * backend workspace card * redirect logic * local changes styles * loading indicator * fix sync button styles * use interval to refetch git * open the repo modal first before switching to git sync * use gitRepositoryId from metadata * fix sync option update * cleanup fetcher check * fixes * cleanup pull-push * undo logo change * clean git-vcs * use cloud icon for insomnia sync * cleanup card * more cleanup * better return type for fetchfetcher * update git tests * fix tests * fix some prerelease tests * fix cloning without an api spec * fix some more tests that expect a file to exist * fix lint error
This commit is contained in:
parent
ba1f6e4190
commit
5b7f45e910
@ -10,7 +10,7 @@ test.describe('Cookie editor', async () => {
|
||||
await page.getByRole('menuitem', { name: 'Import' }).click();
|
||||
await page.getByText('Clipboard').click();
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('button', { name: 'Import' }).click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click();
|
||||
await page.getByText('Collectionsimplejust now').click();
|
||||
});
|
||||
|
||||
|
@ -7,7 +7,8 @@ test.describe('Dashboard', async () => {
|
||||
test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms');
|
||||
test.describe('Projects', async () => {
|
||||
test('Can create, rename and delete new project', async ({ page }) => {
|
||||
await expect(page.locator('.app')).toContainText('All Files (1)');
|
||||
await expect(page.locator('.app')).toContainText('All Files (0)');
|
||||
await expect(page.locator('.app')).not.toContainText('Git Sync');
|
||||
await expect(page.locator('.app')).not.toContainText('Setup Git Sync');
|
||||
|
||||
// Create new project
|
||||
@ -41,14 +42,15 @@ test.describe('Dashboard', async () => {
|
||||
await expect(page.locator('.app')).toContainText('Insomnia');
|
||||
await expect(page.locator('.app')).not.toContainText('My Project123');
|
||||
await expect(page.locator('.app')).toContainText('New Document');
|
||||
await expect(page.locator('.app')).toContainText('All Files (1)');
|
||||
await expect(page.locator('.app')).toContainText('All Files (0)');
|
||||
await expect(page.locator('.app')).not.toContainText('Setup Git Sync');
|
||||
});
|
||||
});
|
||||
test.describe('Interactions', async () => { // Not sure about the name here
|
||||
// TODO(INS-2504) - we don't support importing multiple collections at this time
|
||||
test.skip('Can filter through multiple collections', async ({ app, page }) => {
|
||||
await expect(page.locator('.app')).toContainText('All Files (1)');
|
||||
await expect(page.locator('.app')).toContainText('All Files (0)');
|
||||
await expect(page.locator('.app')).not.toContainText('Git Sync');
|
||||
await expect(page.locator('.app')).not.toContainText('Setup Git Sync');
|
||||
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
@ -57,7 +59,7 @@ test.describe('Dashboard', async () => {
|
||||
await page.getByRole('menuitem', { name: 'Import' }).click();
|
||||
await page.getByText('Clipboard').click();
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('button', { name: 'Import' }).click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click();
|
||||
await page.getByText('CollectionSmoke testsjust now').click();
|
||||
// Check that 10 new workspaces are imported besides the default one
|
||||
const workspaceCards = page.locator('.card-badge');
|
||||
@ -85,7 +87,8 @@ test.describe('Dashboard', async () => {
|
||||
});
|
||||
|
||||
test('Can create, rename and delete a document', async ({ page }) => {
|
||||
await expect(page.locator('.app')).toContainText('All Files (1)');
|
||||
await expect(page.locator('.app')).toContainText('All Files (0)');
|
||||
await expect(page.locator('.app')).not.toContainText('Git Sync');
|
||||
await expect(page.locator('.app')).not.toContainText('Setup Git Sync');
|
||||
|
||||
// Create new document
|
||||
@ -93,10 +96,10 @@ test.describe('Dashboard', async () => {
|
||||
await page.getByRole('menuitem', { name: 'Design Document' }).click();
|
||||
await page.locator('text=Create').nth(1).click();
|
||||
|
||||
// Return to dashboardawait expect(page.locator('.app')).toContainText('My Document');
|
||||
await page.getByTestId('proj_default-project').getByRole('link', { name: 'Insomnia' }).click();
|
||||
|
||||
// Rename document
|
||||
await page.click('text=DocumentMy Documentjust now >> button');
|
||||
await page.click('text=DocumentNew Documentjust now >> button');
|
||||
await page.getByRole('menuitem', { name: 'Rename' }).click();
|
||||
await page.locator('text=Rename DocumentName Rename >> input[type="text"]').fill('test123');
|
||||
await page.click('#root button:has-text("Rename")');
|
||||
@ -108,20 +111,21 @@ test.describe('Dashboard', async () => {
|
||||
await page.locator('input[name="name"]').fill('test123-duplicate');
|
||||
await page.click('[role="dialog"] button:has-text("Duplicate")');
|
||||
|
||||
// Return to dashboardawait expect(page.locator('.app')).toContainText('test123-duplicate');
|
||||
await page.getByTestId('proj_default-project').getByRole('link', { name: 'Insomnia' }).click();
|
||||
|
||||
const workspaceCards = page.locator('.card-badge');
|
||||
await expect(workspaceCards).toHaveCount(3);
|
||||
await expect(workspaceCards).toHaveCount(2);
|
||||
|
||||
// Delete document
|
||||
await page.click('text=Documenttest123just now >> button');
|
||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||
await page.locator('text=Yes').click();
|
||||
await expect(workspaceCards).toHaveCount(2);
|
||||
await expect(workspaceCards).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('Can create, rename and delete a collection', async ({ page }) => {
|
||||
await expect(page.locator('.app')).toContainText('All Files (1)');
|
||||
await expect(page.locator('.app')).toContainText('All Files (0)');
|
||||
await expect(page.locator('.app')).not.toContainText('Git Sync');
|
||||
await expect(page.locator('.app')).not.toContainText('Setup Git Sync');
|
||||
|
||||
// Create new collection
|
||||
@ -129,7 +133,7 @@ test.describe('Dashboard', async () => {
|
||||
await page.getByRole('menuitem', { name: 'Request Collection' }).click();
|
||||
await page.locator('text=Create').nth(1).click();
|
||||
|
||||
// Return to dashboardawait expect(page.locator('.app')).toContainText('My Collection');
|
||||
await page.getByTestId('proj_default-project').getByRole('link', { name: 'Insomnia' }).click();
|
||||
|
||||
// Rename collection
|
||||
await page.click('text=CollectionMy Collectionjust now >> button');
|
||||
@ -144,15 +148,15 @@ test.describe('Dashboard', async () => {
|
||||
await page.locator('input[name="name"]').fill('test123-duplicate');
|
||||
await page.click('[role="dialog"] button:has-text("Duplicate")');
|
||||
|
||||
// Return to dashboardawait expect(page.locator('.app')).toContainText('test123-duplicate');
|
||||
await page.getByTestId('proj_default-project').getByRole('link', { name: 'Insomnia' }).click();
|
||||
const workspaceCards = page.locator('.card-badge');
|
||||
await expect(workspaceCards).toHaveCount(3);
|
||||
await expect(workspaceCards).toHaveCount(2);
|
||||
|
||||
// Delete collection
|
||||
await page.click('text=Collectiontest123just now >> button');
|
||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||
await page.locator('text=Yes').click();
|
||||
await expect(workspaceCards).toHaveCount(2);
|
||||
await expect(workspaceCards).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -12,7 +12,7 @@ test.describe('Debug-Sidebar', async () => {
|
||||
await page.getByRole('menuitem', { name: 'Import' }).click();
|
||||
await page.getByText('Clipboard').click();
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('button', { name: 'Import' }).click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click();
|
||||
await page.getByText('Collectionsimplejust now').click();
|
||||
});
|
||||
|
||||
@ -61,9 +61,9 @@ test.describe('Debug-Sidebar', async () => {
|
||||
});
|
||||
|
||||
test('Open properties of the collection', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Untitled' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Document Settings' }).click();
|
||||
await page.getByText('Document Settings').click();
|
||||
await page.getByRole('button', { name: 'simple' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Collection Settings' }).click();
|
||||
await page.getByText('Collection Settings').click();
|
||||
});
|
||||
|
||||
test('Filter by request name', async ({ page }) => {
|
||||
|
@ -14,8 +14,8 @@ test.describe('Design interactions', async () => {
|
||||
await page.getByRole('menuitem', { name: 'Import' }).click();
|
||||
await page.getByText('Clipboard').click();
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('button', { name: 'Import' }).click();
|
||||
await page.getByText('CollectionSmoke testsjust now').click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click();
|
||||
await page.getByText('unit-test.yaml').click();
|
||||
// Switch to Test tab
|
||||
await page.click('a:has-text("Test")');
|
||||
|
||||
|
@ -10,8 +10,8 @@ test.describe('Environment Editor', async () => {
|
||||
await page.getByRole('menuitem', { name: 'Import' }).click();
|
||||
await page.getByText('Clipboard').click();
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('button', { name: 'Import' }).click();
|
||||
await page.getByText('CollectionSmoke testsjust now').click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click();
|
||||
await page.getByText('Collectionenvironments').click();
|
||||
});
|
||||
|
||||
test('create a new environment', async ({ page }) => {
|
||||
|
@ -13,7 +13,7 @@ test('Clone Repo with bad values', async ({ page }) => {
|
||||
await page.getByText('Author Email').fill('test');
|
||||
await page.getByText('Username').fill('test');
|
||||
await page.getByText('Authentication Token').fill('test');
|
||||
await page.getByRole('button', { name: 'Clone' }).click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Clone' }).click();
|
||||
|
||||
// Basic check repository data is loaded
|
||||
// Design doc
|
||||
@ -50,31 +50,6 @@ test('Clone Repo with bad values', async ({ page }) => {
|
||||
await expect(page.locator('.app')).toContainText('Error Pushing Repository');
|
||||
});
|
||||
|
||||
test('Clone Bitbucket Repo with bad values', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Git Clone' }).click();
|
||||
await page.getByRole('tab', { name: 'Git' }).nth(2).click();
|
||||
|
||||
// Fill in Git Sync details and clone repository
|
||||
await page.getByText('Git URI (https)').fill('https://bitbucket.org/atlassian/bitbucket-example-plugin.git');
|
||||
await page.getByText('Author Name').fill('test');
|
||||
await page.getByText('Author Email').fill('test');
|
||||
await page.getByText('Username').fill('test');
|
||||
await page.getByText('Authentication Token').fill('test');
|
||||
await page.getByRole('button', { name: 'Clone' }).click();
|
||||
|
||||
// Create a branch and try to push with bad Git token
|
||||
await page.getByRole('button', { name: 'master' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Branches' }).click();
|
||||
await page.getByPlaceholder('testing-branch').fill('test123');
|
||||
await page.getByRole('button', { name: '+ Create' }).click();
|
||||
await page.getByRole('cell', { name: 'test123(current)' }).click();
|
||||
await page.getByRole('button', { name: 'Done' }).click();
|
||||
await page.getByRole('button', { name: 'test123' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Push' }).click();
|
||||
await expect(page.locator('.app')).toContainText('Error Pushing Repository');
|
||||
});
|
||||
|
||||
test('Clone Gitlab Repo with bad values', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Git Clone' }).click();
|
||||
@ -86,7 +61,7 @@ test('Clone Gitlab Repo with bad values', async ({ page }) => {
|
||||
await page.getByText('Author Email').fill('test');
|
||||
await page.getByText('Username').fill('test');
|
||||
await page.getByText('Authentication Token').fill('test');
|
||||
await page.getByRole('button', { name: 'Clone' }).click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Clone' }).click();
|
||||
|
||||
// Create a branch and try to push with bad Git token
|
||||
await page.getByRole('button', { name: 'master' }).click();
|
||||
|
@ -19,7 +19,7 @@ test.describe('gRPC interactions', () => {
|
||||
await page.getByRole('menuitem', { name: 'Import' }).click();
|
||||
await page.getByText('Clipboard').click();
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('button', { name: 'Import' }).click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click();
|
||||
await page.getByText('CollectionPreRelease gRPCjust now').click();
|
||||
statusTag = page.locator('[data-testid="response-status-tag"]:visible');
|
||||
responseBody = page.locator('[data-testid="response-pane"] >> [data-testid="CodeEditor"]:visible', {
|
||||
|
@ -18,8 +18,8 @@ test.describe('Plugins', async () => {
|
||||
});
|
||||
|
||||
test('Check Declarative Config and Kong Kubernetes config', async ({ page }) => {
|
||||
// Switch to design tab
|
||||
await page.click('text=Design');
|
||||
await page.getByRole('button', { name: 'New Document' }).click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Set example OpenAPI spec
|
||||
await page.click('text=start from an example');
|
||||
|
@ -1,5 +1,13 @@
|
||||
import { test } from '../../playwright/test';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'New Collection' }).click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
await page.getByRole('button', { name: ' ' }).press('ArrowDown');
|
||||
await page.getByRole('menuitem', { name: 'HTTP Request' }).press('Enter');
|
||||
});
|
||||
|
||||
test('Select body dropdown', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Body' }).click();
|
||||
await page.getByRole('menuitem', { name: 'JSON' }).click();
|
||||
|
@ -3,6 +3,7 @@ import { test } from '../../playwright/test';
|
||||
test('Sign in with GitHub', async ({ app, page }) => {
|
||||
await page.getByRole('button', { name: 'New Document' }).click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Create' }).click();
|
||||
await page.getByRole('button', { name: 'Git Sync' }).click();
|
||||
await page.getByRole('button', { name: 'Setup Git Sync' }).click();
|
||||
|
||||
await page.getByRole('tab', { name: 'Github' }).click();
|
||||
|
@ -3,6 +3,7 @@ import { test } from '../../playwright/test';
|
||||
test('Sign in with Gitlab', async ({ app, page }) => {
|
||||
await page.getByRole('button', { name: 'New Document' }).click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Create' }).click();
|
||||
await page.getByRole('button', { name: 'Git Sync' }).click();
|
||||
await page.getByRole('button', { name: 'Setup Git Sync' }).click();
|
||||
await page.getByRole('tab', { name: 'GitLab' }).click();
|
||||
|
||||
|
@ -10,7 +10,7 @@ export const prefix = 'spc';
|
||||
|
||||
export const canDuplicate = true;
|
||||
|
||||
export const canSync = false;
|
||||
export const canSync = true;
|
||||
|
||||
export interface BaseApiSpec {
|
||||
fileName: string;
|
||||
|
@ -3,30 +3,12 @@ import { afterAll, beforeEach, describe, expect, it, jest } from '@jest/globals'
|
||||
import { globalBeforeEach } from '../../../__jest__/before-each';
|
||||
import * as models from '../../../models';
|
||||
import MemoryDriver from '../../store/drivers/memory-driver';
|
||||
import { initializeLocalBackendProjectAndMarkForSync, pushSnapshotOnInitialize } from '../initialize-backend-project';
|
||||
import { pushSnapshotOnInitialize } from '../initialize-backend-project';
|
||||
import { VCS } from '../vcs';
|
||||
|
||||
describe('initialize-backend-project', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
|
||||
describe('initializeLocalBackendProjectAndMarkForSync()', () => {
|
||||
it('should do nothing if not request collection', async () => {
|
||||
// Arrange
|
||||
const workspace = await models.workspace.create({ scope: 'design' });
|
||||
const vcs = new VCS(new MemoryDriver());
|
||||
const switchAndCreateBackendProjectIfNotExistSpy = jest.spyOn(vcs, 'switchAndCreateBackendProjectIfNotExist');
|
||||
|
||||
// Act
|
||||
await initializeLocalBackendProjectAndMarkForSync({ workspace, vcs });
|
||||
|
||||
// Assert
|
||||
expect(switchAndCreateBackendProjectIfNotExistSpy).not.toHaveBeenCalled();
|
||||
// const workspaceMeta = await models.workspaceMeta.getByParentId(workspace._id);
|
||||
// expect(workspaceMeta?.pushSnapshotOnInitialize).toBe(false);
|
||||
switchAndCreateBackendProjectIfNotExistSpy.mockClear();
|
||||
});
|
||||
});
|
||||
|
||||
describe('pushSnapshotOnInitialize()', () => {
|
||||
const vcs = new VCS(new MemoryDriver());
|
||||
|
||||
|
@ -2,18 +2,13 @@ import { database } from '../../common/database';
|
||||
import * as models from '../../models';
|
||||
import { getStatusCandidates } from '../../models/helpers/get-status-candidates';
|
||||
import { Project } from '../../models/project';
|
||||
import { isCollection, Workspace } from '../../models/workspace';
|
||||
import { Workspace } from '../../models/workspace';
|
||||
import { WorkspaceMeta } from '../../models/workspace-meta';
|
||||
import { VCS } from './vcs';
|
||||
|
||||
const blankStage = {};
|
||||
|
||||
export const initializeLocalBackendProjectAndMarkForSync = async ({ vcs, workspace }: { vcs: VCS; workspace: Workspace }) => {
|
||||
if (!isCollection(workspace)) {
|
||||
// Don't initialize and mark for sync unless we're in a collection
|
||||
return;
|
||||
}
|
||||
|
||||
// Create local project
|
||||
await vcs.switchAndCreateBackendProjectIfNotExist(workspace._id, workspace.name);
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { isLoggedIn } from '../../account/session';
|
||||
import { asyncFilter } from '../../common/async-array-helpers';
|
||||
import { database } from '../../common/database';
|
||||
import { isNotNullOrUndefined } from '../../common/misc';
|
||||
import * as models from '../../models';
|
||||
import { isRemoteProject, RemoteProject } from '../../models/project';
|
||||
import { isCollection, Workspace } from '../../models/workspace';
|
||||
@ -36,7 +36,7 @@ export const migrateCollectionsIntoRemoteProject = async (vcs: VCS) => {
|
||||
const isNotInRemoteProject = (collection: Workspace) => !Boolean(remoteProjects.find(project => project._id === collection.parentId));
|
||||
const hasLocalProject = (collection: Workspace) => vcs.hasBackendProjectForRootDocument(collection._id);
|
||||
|
||||
const needsMigration = await asyncFilter(collections, async coll => await hasLocalProject(coll) && isNotInRemoteProject(coll));
|
||||
const needsMigration = (await Promise.all(collections.map(async coll => await hasLocalProject(coll) && isNotInRemoteProject(coll) ? coll : null))).filter(isNotNullOrUndefined);
|
||||
|
||||
// If nothing to migrate, exit
|
||||
if (!needsMigration.length) {
|
||||
|
@ -2,7 +2,10 @@ import { DEFAULT_BRANCH_NAME } from '../../common/constants';
|
||||
import { database } from '../../common/database';
|
||||
import { RemoteProject } from '../../models/project';
|
||||
import { isWorkspace } from '../../models/workspace';
|
||||
import { initializeProjectFromTeam, initializeWorkspaceFromBackendProject } from './initialize-model-from';
|
||||
import {
|
||||
initializeProjectFromTeam,
|
||||
initializeWorkspaceFromBackendProject,
|
||||
} from './initialize-model-from';
|
||||
import { BackendProjectWithTeam } from './normalize-backend-project-team';
|
||||
import { interceptAccessError } from './util';
|
||||
import { VCS } from './vcs';
|
||||
@ -13,7 +16,11 @@ interface Options {
|
||||
remoteProjects: RemoteProject[];
|
||||
}
|
||||
|
||||
export const pullBackendProject = async ({ vcs, backendProject, remoteProjects }: Options) => {
|
||||
export const pullBackendProject = async ({
|
||||
vcs,
|
||||
backendProject,
|
||||
remoteProjects,
|
||||
}: Options) => {
|
||||
// Set backend project, checkout master, and pull
|
||||
await vcs.setBackendProject(backendProject);
|
||||
await vcs.checkout([], DEFAULT_BRANCH_NAME);
|
||||
@ -26,25 +33,35 @@ export const pullBackendProject = async ({ vcs, backendProject, remoteProjects }
|
||||
const defaultBranchMissing = !remoteBranches.includes(DEFAULT_BRANCH_NAME);
|
||||
|
||||
// Find or create the remote project locally
|
||||
let project = remoteProjects.find(({ remoteId }) => remoteId === backendProject.team.id);
|
||||
let project = remoteProjects.find(
|
||||
({ remoteId }) => remoteId === backendProject.team.id
|
||||
);
|
||||
if (!project) {
|
||||
project = await initializeProjectFromTeam(backendProject.team);
|
||||
await database.upsert(project);
|
||||
}
|
||||
|
||||
let workspaceId;
|
||||
|
||||
// The default branch does not exist, so we create it and the workspace locally
|
||||
if (defaultBranchMissing) {
|
||||
const workspace = await initializeWorkspaceFromBackendProject(backendProject, project);
|
||||
const workspace = await initializeWorkspaceFromBackendProject(
|
||||
backendProject,
|
||||
project
|
||||
);
|
||||
await database.upsert(workspace);
|
||||
|
||||
workspaceId = workspace._id;
|
||||
} else {
|
||||
await vcs.pull([], project.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() || [])) {
|
||||
for (const doc of (await vcs.allDocuments()) || []) {
|
||||
if (isWorkspace(doc)) {
|
||||
doc.parentId = project._id;
|
||||
workspaceId = doc._id;
|
||||
}
|
||||
await database.upsert(doc);
|
||||
}
|
||||
@ -52,5 +69,5 @@ export const pullBackendProject = async ({ vcs, backendProject, remoteProjects }
|
||||
await database.flushChanges(flushId);
|
||||
}
|
||||
|
||||
return project;
|
||||
return { project, workspaceId };
|
||||
};
|
||||
|
@ -1,13 +1,16 @@
|
||||
import classnames from 'classnames';
|
||||
import React, { FC, Fragment, useEffect, useRef, useState } from 'react';
|
||||
import { useFetcher, useParams } from 'react-router-dom';
|
||||
import { useFetcher, useParams, useRevalidator } from 'react-router-dom';
|
||||
import { useInterval } from 'react-use';
|
||||
|
||||
import { docsGitSync } from '../../../common/documentation';
|
||||
import { GitRepository } from '../../../models/git-repository';
|
||||
import { deleteGitRepository } from '../../../models/helpers/git-repository-operations';
|
||||
import { getOauth2FormatName } from '../../../sync/git/utils';
|
||||
import {
|
||||
GitFetchLoaderData,
|
||||
GitRepoLoaderData,
|
||||
GitStatusResult,
|
||||
PullFromGitRemoteResult,
|
||||
PushToGitRemoteResult,
|
||||
} from '../../routes/git-actions';
|
||||
@ -27,13 +30,15 @@ import { GitLogModal } from '../modals/git-log-modal';
|
||||
import { GitRepositorySettingsModal } from '../modals/git-repository-settings-modal';
|
||||
import { GitStagingModal } from '../modals/git-staging-modal';
|
||||
import { Button } from '../themed-button';
|
||||
import { Tooltip } from '../tooltip';
|
||||
|
||||
interface Props {
|
||||
gitRepository: GitRepository | null;
|
||||
className?: string;
|
||||
isInsomniaSyncEnabled: boolean;
|
||||
}
|
||||
|
||||
export const GitSyncDropdown: FC<Props> = ({ className, gitRepository }) => {
|
||||
export const GitSyncDropdown: FC<Props> = ({ className, gitRepository, isInsomniaSyncEnabled }) => {
|
||||
const { organizationId, projectId, workspaceId } = useParams() as {
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
@ -52,10 +57,12 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository }) => {
|
||||
const gitCheckoutFetcher = useFetcher();
|
||||
const gitRepoDataFetcher = useFetcher<GitRepoLoaderData>();
|
||||
const gitFetchFetcher = useFetcher<GitFetchLoaderData>();
|
||||
const gitStatusFetcher = useFetcher<GitStatusResult>();
|
||||
|
||||
const loadingPush = gitPushFetcher.state === 'loading';
|
||||
const loadingPull = gitPullFetcher.state === 'loading';
|
||||
const loadingFetch = gitFetchFetcher.state === 'loading';
|
||||
const loadingStatus = gitStatusFetcher.state === 'loading';
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@ -77,6 +84,17 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository }) => {
|
||||
workspaceId,
|
||||
]);
|
||||
|
||||
// Only fetch the repo status if we have a repo uri and we don't have the status already
|
||||
const shouldFetchGitRepoStatus = Boolean(gitRepository?.uri && gitRepository?._id && gitStatusFetcher.state === 'idle' && !gitStatusFetcher.data && gitRepoDataFetcher.data);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldFetchGitRepoStatus) {
|
||||
gitStatusFetcher.load(
|
||||
`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/status`
|
||||
);
|
||||
}
|
||||
}, [gitStatusFetcher, organizationId, projectId, shouldFetchGitRepoStatus, workspaceId]);
|
||||
|
||||
useEffect(() => {
|
||||
const errors = [...(gitPushFetcher.data?.errors ?? [])];
|
||||
if (errors.length > 0) {
|
||||
@ -133,7 +151,7 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository }) => {
|
||||
);
|
||||
}
|
||||
|
||||
let iconClassName = '';
|
||||
let iconClassName = 'fa-brands fa-git-alt';
|
||||
const providerName = getOauth2FormatName(gitRepository?.credentials);
|
||||
if (providerName === 'github') {
|
||||
iconClassName = 'fa fa-github';
|
||||
@ -148,10 +166,8 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository }) => {
|
||||
gitCheckoutFetcher.state === 'loading' ||
|
||||
gitPushFetcher.state === 'loading' ||
|
||||
gitPullFetcher.state === 'loading';
|
||||
const isButton =
|
||||
!gitRepository ||
|
||||
(isLoading && !gitRepoDataFetcher.data) ||
|
||||
(gitRepoDataFetcher.data && 'errors' in gitRepoDataFetcher.data);
|
||||
|
||||
const isSynced = Boolean(gitRepository?.uri && !isLoading && gitRepoDataFetcher.data && !('errors' in gitRepoDataFetcher.data));
|
||||
|
||||
const { branches, branch: currentBranch } =
|
||||
gitRepoDataFetcher.data && 'branches' in gitRepoDataFetcher.data
|
||||
@ -159,6 +175,7 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository }) => {
|
||||
: { branches: [], branch: '' };
|
||||
|
||||
let dropdown: React.ReactNode = null;
|
||||
const { revalidate } = useRevalidator();
|
||||
|
||||
const currentBranchActions = [
|
||||
{
|
||||
@ -212,51 +229,108 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository }) => {
|
||||
},
|
||||
];
|
||||
|
||||
if (isButton) {
|
||||
dropdown = (
|
||||
<Button
|
||||
disabled={isLoading}
|
||||
size="small"
|
||||
className="btn--clicky-small btn-sync"
|
||||
onClick={() => setIsGitRepoSettingsModalOpen(true)}
|
||||
>
|
||||
<i
|
||||
className={`fa fa-code-fork space-right ${
|
||||
isLoading ? 'fa-fade' : ''
|
||||
}`}
|
||||
/>
|
||||
{isLoading ? 'Loading...' : 'Setup Git Sync'}
|
||||
</Button>
|
||||
useInterval(() => {
|
||||
gitFetchFetcher.submit(
|
||||
{},
|
||||
{
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/fetch`,
|
||||
method: 'post',
|
||||
}
|
||||
);
|
||||
} else {
|
||||
}, 1000 * 60 * 5);
|
||||
|
||||
const status = gitStatusFetcher.data?.status;
|
||||
|
||||
const commitToolTipMsg = status?.localChanges ? 'Local changes made' : 'No local changes made';
|
||||
|
||||
if (isSynced) {
|
||||
dropdown = (
|
||||
<div className={className}>
|
||||
<Dropdown
|
||||
className="wide tall"
|
||||
ref={dropdownRef}
|
||||
onOpen={() => {
|
||||
gitFetchFetcher.submit(
|
||||
{},
|
||||
{
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/fetch`,
|
||||
method: 'post',
|
||||
}
|
||||
);
|
||||
}}
|
||||
triggerButton={
|
||||
<DropdownButton className="btn--clicky-small btn-sync">
|
||||
{iconClassName && (
|
||||
<i className={classnames('space-right', iconClassName)} />
|
||||
)}
|
||||
<div className="ellipsis">{currentBranch}</div>
|
||||
<i
|
||||
className={`fa fa-code-fork space-left ${
|
||||
isLoading ? 'fa-fade' : ''
|
||||
}`}
|
||||
/>
|
||||
<DropdownButton
|
||||
size="medium"
|
||||
variant='text'
|
||||
removePaddings={false}
|
||||
removeBorderRadius
|
||||
style={{
|
||||
width: '100%',
|
||||
borderRadius: '0',
|
||||
borderTop: '1px solid var(--hl-md)',
|
||||
justifyContent: 'flex-start !important',
|
||||
height: 'var(--line-height-sm)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--padding-xs)',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{iconClassName && (
|
||||
<i className={classnames('space-right', iconClassName)} />
|
||||
)}
|
||||
<div className="ellipsis">{currentBranch}</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
opacity: loadingStatus ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
<Tooltip message={commitToolTipMsg}>
|
||||
<span
|
||||
style={{
|
||||
opacity: status?.localChanges ? 1 : 0.5,
|
||||
color: status?.localChanges ? 'var(--color-notice)' : 'var(--color-hl)',
|
||||
}}
|
||||
><i className="fa fa-cube space-left" /></span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</DropdownButton>
|
||||
}
|
||||
>
|
||||
<DropdownSection
|
||||
items={isInsomniaSyncEnabled ? [{
|
||||
value: 'Use Insomnia Sync',
|
||||
id: 'use-insomnia-sync',
|
||||
}] : []}
|
||||
>
|
||||
{item => (
|
||||
<DropdownItem
|
||||
key={item.id}
|
||||
textValue='Use Insomnia Sync'
|
||||
arial-label='Use Insomnia Sync'
|
||||
>
|
||||
<Button
|
||||
variant='contained'
|
||||
bg='surprise'
|
||||
onClick={async () => {
|
||||
if (gitRepository) {
|
||||
await deleteGitRepository(gitRepository);
|
||||
revalidate();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--padding-sm)',
|
||||
margin: '0 var(--padding-sm)',
|
||||
}}
|
||||
>
|
||||
<i className="fa fa-cloud" /> Use Insomnia Sync
|
||||
</Button>
|
||||
</DropdownItem>
|
||||
)}
|
||||
</DropdownSection>
|
||||
<DropdownSection
|
||||
title={
|
||||
<span>
|
||||
@ -345,6 +419,108 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository }) => {
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
dropdown = (
|
||||
<div className={className}>
|
||||
<Dropdown
|
||||
className="wide tall"
|
||||
ref={dropdownRef}
|
||||
triggerButton={
|
||||
<DropdownButton
|
||||
size="medium"
|
||||
variant='text'
|
||||
removePaddings={false}
|
||||
removeBorderRadius
|
||||
style={{
|
||||
width: '100%',
|
||||
borderRadius: '0',
|
||||
borderTop: '1px solid var(--hl-md)',
|
||||
justifyContent: 'flex-start !important',
|
||||
height: 'var(--line-height-sm)',
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--padding-xs)',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{iconClassName && (
|
||||
<i className={classnames('space-right', iconClassName)} />
|
||||
)}
|
||||
<span className="ellipsis">Git Sync</span>
|
||||
</div>
|
||||
|
||||
</DropdownButton>
|
||||
}
|
||||
>
|
||||
<DropdownSection
|
||||
items={isInsomniaSyncEnabled ? [{
|
||||
value: 'Use Insomnia Sync',
|
||||
id: 'use-insomnia-sync',
|
||||
}] : []}
|
||||
>
|
||||
{item => (
|
||||
<DropdownItem
|
||||
key={item.id}
|
||||
arial-label='Use Insomnia Sync'
|
||||
>
|
||||
<Button
|
||||
variant='contained'
|
||||
bg='surprise'
|
||||
onClick={async () => {
|
||||
if (gitRepository) {
|
||||
await deleteGitRepository(gitRepository);
|
||||
revalidate();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--padding-sm)',
|
||||
margin: '0 var(--padding-sm)',
|
||||
}}
|
||||
>
|
||||
<i className="fa fa-cloud" /> Use Insomnia Sync
|
||||
</Button>
|
||||
</DropdownItem>
|
||||
)}
|
||||
</DropdownSection>
|
||||
<DropdownSection
|
||||
title={
|
||||
<span>
|
||||
Git Sync
|
||||
<HelpTooltip>
|
||||
Sync and collaborate with Git{' '}
|
||||
<Link href={docsGitSync}>
|
||||
<span className="no-wrap">
|
||||
<br />
|
||||
Documentation <i className="fa fa-external-link" />
|
||||
</span>
|
||||
</Link>
|
||||
</HelpTooltip>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<DropdownItem textValue="Settings">
|
||||
<ItemContent
|
||||
icon="wrench"
|
||||
label="Setup Git Sync"
|
||||
onClick={() => {
|
||||
setIsGitRepoSettingsModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -24,11 +24,13 @@ import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent }
|
||||
import { Link } from '../base/link';
|
||||
import { HelpTooltip } from '../help-tooltip';
|
||||
import { showError, showModal } from '../modals';
|
||||
import { GitRepositorySettingsModal } from '../modals/git-repository-settings-modal';
|
||||
import { LoginModal } from '../modals/login-modal';
|
||||
import { SyncBranchesModal } from '../modals/sync-branches-modal';
|
||||
import { SyncDeleteModal } from '../modals/sync-delete-modal';
|
||||
import { SyncHistoryModal } from '../modals/sync-history-modal';
|
||||
import { SyncStagingModal } from '../modals/sync-staging-modal';
|
||||
import { Button } from '../themed-button';
|
||||
import { Tooltip } from '../tooltip';
|
||||
|
||||
// TODO: handle refetching logic in one place not here in a component
|
||||
@ -81,7 +83,6 @@ export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
|
||||
const remoteProjects = useSelector(selectRemoteProjects);
|
||||
const syncItems = useSelector(selectSyncItems);
|
||||
const workspaceMeta = useSelector(selectActiveWorkspaceMeta);
|
||||
|
||||
const refetchRemoteBranch = useCallback(async () => {
|
||||
if (session.isLoggedIn()) {
|
||||
try {
|
||||
@ -125,6 +126,8 @@ export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
|
||||
refetchRemoteBranch();
|
||||
}, REFRESH_PERIOD);
|
||||
|
||||
const [isGitRepoSettingsModalOpen, setIsGitRepoSettingsModalOpen] = useState(false);
|
||||
|
||||
useMount(async () => {
|
||||
setState(state => ({
|
||||
...state,
|
||||
@ -162,10 +165,10 @@ export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
|
||||
loadingProjectPull: true,
|
||||
}));
|
||||
const pulledIntoProject = await pullBackendProject({ vcs, backendProject, remoteProjects });
|
||||
if (pulledIntoProject._id !== project._id) {
|
||||
if (pulledIntoProject.project._id !== project._id) {
|
||||
// If pulled into a different project, reactivate the workspace
|
||||
await dispatch(activateWorkspace({ workspaceId: workspace._id }));
|
||||
logCollectionMovedToProject(workspace, pulledIntoProject);
|
||||
dispatch(activateWorkspace({ workspaceId: workspace._id }));
|
||||
logCollectionMovedToProject(workspace, pulledIntoProject.project);
|
||||
}
|
||||
await refreshVCSAndRefetchRemote();
|
||||
setState(state => ({
|
||||
@ -338,23 +341,61 @@ export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
|
||||
return (
|
||||
<div>
|
||||
<Dropdown
|
||||
style={{
|
||||
marginLeft: 'var(--padding-md)',
|
||||
}}
|
||||
className="wide tall"
|
||||
onOpen={() => refreshVCSAndRefetchRemote()}
|
||||
aria-label="Select a project to sync with"
|
||||
triggerButton={
|
||||
<DropdownButton
|
||||
variant='outlined'
|
||||
size="medium"
|
||||
removeBorderRadius
|
||||
disableHoverBehavior={false}
|
||||
removePaddings={false}
|
||||
className="btn--clicky-small btn-sync wide text-left overflow-hidden row-spaced"
|
||||
variant='text'
|
||||
style={{
|
||||
width: '100%',
|
||||
borderRadius: '0',
|
||||
borderTop: '1px solid var(--hl-md)',
|
||||
height: 'var(--line-height-sm)',
|
||||
}}
|
||||
>
|
||||
<i className="fa fa-code-fork " /> Setup Sync
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--padding-xs)',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<i className="fa fa-cloud" /> Setup Sync
|
||||
</div>
|
||||
</DropdownButton>
|
||||
}
|
||||
>
|
||||
<DropdownSection>
|
||||
<DropdownItem
|
||||
key='gitSync'
|
||||
arial-label='Setup Git Sync'
|
||||
>
|
||||
<Button
|
||||
variant='contained'
|
||||
bg='surprise'
|
||||
onClick={async () => {
|
||||
setIsGitRepoSettingsModalOpen(true);
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--padding-sm)',
|
||||
margin: '0 var(--padding-sm)',
|
||||
justifyContent: 'flex-start!important',
|
||||
}}
|
||||
>
|
||||
<i className="fa-brands fa-git-alt" /> Use Git Sync
|
||||
</Button>
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
<DropdownSection
|
||||
aria-label='Sync Projects List'
|
||||
items={remoteBackendProjects.length === 0 ? emptyDropdownItemsArray : []}
|
||||
@ -363,8 +404,9 @@ export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
|
||||
{p =>
|
||||
<DropdownItem
|
||||
key={p.id}
|
||||
textValue={p.name}
|
||||
>
|
||||
<ItemContent {...p} />
|
||||
<ItemContent {...p} label={p.name} />
|
||||
</DropdownItem>
|
||||
}
|
||||
</DropdownSection>
|
||||
@ -387,6 +429,11 @@ export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
|
||||
}
|
||||
</DropdownSection>
|
||||
</Dropdown>
|
||||
{isGitRepoSettingsModalOpen && (
|
||||
<GitRepositorySettingsModal
|
||||
onHide={() => setIsGitRepoSettingsModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -404,9 +451,8 @@ export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
|
||||
return (
|
||||
<div>
|
||||
<Dropdown
|
||||
aria-label='Select a branch to sync with'
|
||||
style={{ marginLeft: 'var(--padding-md)' }}
|
||||
className="wide tall"
|
||||
aria-label='Select a branch to sync with'
|
||||
onOpen={() => refreshVCSAndRefetchRemote()}
|
||||
closeOnSelect={false}
|
||||
isDisabled={initializing}
|
||||
@ -414,56 +460,101 @@ export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
|
||||
currentBranch === null ?
|
||||
<Fragment>Sync</Fragment> :
|
||||
<DropdownButton
|
||||
variant='outlined'
|
||||
size="medium"
|
||||
removeBorderRadius
|
||||
disableHoverBehavior={false}
|
||||
removePaddings={false}
|
||||
className="btn--clicky-small btn-sync wide text-left overflow-hidden row-spaced"
|
||||
variant='text'
|
||||
style={{
|
||||
width: '100%',
|
||||
borderRadius: '0',
|
||||
borderTop: '1px solid var(--hl-md)',
|
||||
height: 'var(--line-height-sm)',
|
||||
}}
|
||||
>
|
||||
<div className="ellipsis">
|
||||
<i className="fa fa-code-fork space-right" />{' '}
|
||||
{initializing ? 'Initializing...' : currentBranch}
|
||||
</div>
|
||||
<div className="flex space-left">
|
||||
<Tooltip message={snapshotToolTipMsg} delay={800} position="bottom">
|
||||
<i
|
||||
className={classnames('fa fa-cube fa--fixed-width', {
|
||||
'super-duper-faint': !canCreateSnapshot,
|
||||
})}
|
||||
/>
|
||||
</Tooltip>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--padding-xs)',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div className="ellipsis">
|
||||
<i className="fa fa-cloud space-right" />{' '}
|
||||
{initializing ? 'Initializing...' : currentBranch}
|
||||
</div>
|
||||
<div className="flex space-left">
|
||||
<Tooltip message={snapshotToolTipMsg} delay={800} position="bottom">
|
||||
<i
|
||||
style={{
|
||||
color: canCreateSnapshot ? 'var(--color-notice)' : 'var(--color-hl)',
|
||||
}}
|
||||
className={classnames('fa fa-cube fa--fixed-width', {
|
||||
'super-duper-faint': !canCreateSnapshot,
|
||||
})}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{/* Only show cloud icons if logged in */}
|
||||
{session.isLoggedIn() && (
|
||||
<Fragment>
|
||||
{loadingPull ? (
|
||||
loadIcon
|
||||
) : (
|
||||
<Tooltip message={pullToolTipMsg} delay={800} position="bottom">
|
||||
<i
|
||||
className={classnames('fa fa-cloud-download fa--fixed-width', {
|
||||
'super-duper-faint': !canPull,
|
||||
})}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{/* Only show cloud icons if logged in */}
|
||||
{session.isLoggedIn() && (
|
||||
<Fragment>
|
||||
{loadingPull ? (
|
||||
loadIcon
|
||||
) : (
|
||||
<Tooltip message={pullToolTipMsg} delay={800} position="bottom">
|
||||
<i
|
||||
className={classnames('fa fa-cloud-download fa--fixed-width', {
|
||||
'super-duper-faint': !canPull,
|
||||
})}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{loadingPush ? (
|
||||
loadIcon
|
||||
) : (
|
||||
<Tooltip message={pushToolTipMsg} delay={800} position="bottom">
|
||||
<i
|
||||
className={classnames('fa fa-cloud-upload fa--fixed-width', {
|
||||
'super-duper-faint': !canPush,
|
||||
})}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
{loadingPush ? (
|
||||
loadIcon
|
||||
) : (
|
||||
<Tooltip message={pushToolTipMsg} delay={800} position="bottom">
|
||||
<i
|
||||
className={classnames('fa fa-cloud-upload fa--fixed-width', {
|
||||
'super-duper-faint': !canPush,
|
||||
})}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</DropdownButton>
|
||||
}
|
||||
>
|
||||
<DropdownSection>
|
||||
<DropdownItem
|
||||
key='gitSync'
|
||||
arial-label='Setup Git Sync'
|
||||
>
|
||||
<Button
|
||||
variant='contained'
|
||||
bg='surprise'
|
||||
onClick={async () => {
|
||||
setIsGitRepoSettingsModalOpen(true);
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--padding-sm)',
|
||||
margin: '0 var(--padding-sm)',
|
||||
justifyContent: 'flex-start!important',
|
||||
}}
|
||||
>
|
||||
<i className="fa-brands fa-git-alt" /> Use Git Sync
|
||||
</Button>
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
<DropdownSection
|
||||
aria-label='Sync Branches List'
|
||||
title={syncMenuHeader}
|
||||
@ -576,6 +667,11 @@ export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
</Dropdown>
|
||||
{isGitRepoSettingsModalOpen && (
|
||||
<GitRepositorySettingsModal
|
||||
onHide={() => setIsGitRepoSettingsModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,41 @@
|
||||
import { FC } from 'react';
|
||||
import React from 'react';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { isRemoteProject } from '../../../models/project';
|
||||
import { useVCS } from '../../hooks/use-vcs';
|
||||
import { WorkspaceLoaderData } from '../../routes/workspace';
|
||||
import { GitSyncDropdown } from './git-sync-dropdown';
|
||||
import { SyncDropdown } from './sync-dropdown';
|
||||
|
||||
export const WorkspaceSyncDropdown: FC = () => {
|
||||
const {
|
||||
activeProject,
|
||||
activeWorkspace,
|
||||
gitRepository,
|
||||
activeWorkspaceMeta,
|
||||
} = useRouteLoaderData(
|
||||
':workspaceId'
|
||||
) as WorkspaceLoaderData;
|
||||
|
||||
const vcs = useVCS({
|
||||
workspaceId: activeWorkspace?._id,
|
||||
});
|
||||
|
||||
if (isRemoteProject(activeProject) && vcs && !activeWorkspaceMeta?.gitRepositoryId) {
|
||||
return (
|
||||
<SyncDropdown
|
||||
key={activeWorkspace?._id}
|
||||
workspace={activeWorkspace}
|
||||
project={activeProject}
|
||||
vcs={vcs}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeWorkspaceMeta?.gitRepositoryId || !isRemoteProject(activeProject)) {
|
||||
return <GitSyncDropdown isInsomniaSyncEnabled={isRemoteProject(activeProject)} gitRepository={gitRepository} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
@ -1,5 +1,6 @@
|
||||
import React, { FC, forwardRef, ReactNode, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useRevalidator } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ACTIVITY_HOME } from '../../../common/constants';
|
||||
@ -12,7 +13,7 @@ import * as models from '../../../models/index';
|
||||
import { isRequest } from '../../../models/request';
|
||||
import { invariant } from '../../../utils/invariant';
|
||||
import { setActiveActivity } from '../../redux/modules/global';
|
||||
import { selectActiveApiSpec, selectActiveWorkspace, selectActiveWorkspaceClientCertificates, selectActiveWorkspaceName } from '../../redux/selectors';
|
||||
import { selectActiveApiSpec, selectActiveWorkspace, selectActiveWorkspaceClientCertificates, selectActiveWorkspaceMeta, selectActiveWorkspaceName } from '../../redux/selectors';
|
||||
import { FileInputButton } from '../base/file-input-button';
|
||||
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
|
||||
import { ModalBody } from '../base/modal-body';
|
||||
@ -57,6 +58,7 @@ const CertificateField: FC<{
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export interface WorkspaceSettingsModalOptions {
|
||||
showAddCertificateForm: boolean;
|
||||
host: string;
|
||||
@ -86,10 +88,13 @@ export const WorkspaceSettingsModal = forwardRef<WorkspaceSettingsModalHandle, M
|
||||
defaultPreviewMode: false,
|
||||
});
|
||||
|
||||
const { revalidate } = useRevalidator();
|
||||
const workspace = useSelector(selectActiveWorkspace);
|
||||
const apiSpec = useSelector(selectActiveApiSpec);
|
||||
const activeWorkspaceName = useSelector(selectActiveWorkspaceName);
|
||||
const clientCertificates = useSelector(selectActiveWorkspaceClientCertificates);
|
||||
const workspaceMeta = useSelector(selectActiveWorkspaceMeta);
|
||||
|
||||
const [caCert, setCaCert] = useState<CaCertificate | null>(null);
|
||||
useEffect(() => {
|
||||
if (!workspace) {
|
||||
@ -223,6 +228,7 @@ export const WorkspaceSettingsModal = forwardRef<WorkspaceSettingsModalHandle, M
|
||||
showDescription,
|
||||
defaultPreviewMode,
|
||||
} = state;
|
||||
|
||||
return (
|
||||
<Modal ref={modalRef}>
|
||||
{workspace ?
|
||||
@ -491,6 +497,47 @@ export const WorkspaceSettingsModal = forwardRef<WorkspaceSettingsModalHandle, M
|
||||
)}
|
||||
</PanelContainer>
|
||||
</TabItem>
|
||||
<TabItem key="git-sybc" title="Git Sync">
|
||||
<PanelContainer className="pad">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--padding-xs)',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(workspaceMeta?.gitRepositoryId)}
|
||||
onChange={async () => {
|
||||
if (workspaceMeta?.gitRepositoryId) {
|
||||
await models.workspaceMeta.update(workspaceMeta, {
|
||||
gitRepositoryId: null,
|
||||
});
|
||||
} else {
|
||||
invariant(workspaceMeta, 'Workspace meta not found');
|
||||
|
||||
const repo = await models.gitRepository.create({
|
||||
uri: '',
|
||||
});
|
||||
|
||||
await models.workspaceMeta.update(workspaceMeta, {
|
||||
gitRepositoryId: repo._id,
|
||||
});
|
||||
}
|
||||
|
||||
revalidate();
|
||||
}}
|
||||
/>
|
||||
Enable Git Sync
|
||||
</label>
|
||||
<p>
|
||||
By enabling Git Sync, you can sync your workspace with a Git repository. This will disable the ability to sync with Insomnia Sync.
|
||||
</p>
|
||||
</div>
|
||||
</PanelContainer>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</ModalBody> : null}
|
||||
</Modal>
|
||||
|
@ -187,6 +187,10 @@ const Pane = forwardRef<HTMLElement, { position: string; children: ReactNode }>(
|
||||
}
|
||||
);
|
||||
|
||||
export const SidebarFooter = styled.div({
|
||||
gridRowStart: 6,
|
||||
});
|
||||
|
||||
interface Props {
|
||||
renderPageSidebar?: ReactNode;
|
||||
renderPaneOne?: ReactNode;
|
||||
|
@ -27,20 +27,12 @@ const KongLink = styled.a({
|
||||
});
|
||||
|
||||
export const StatusBar: FC = () => {
|
||||
return <Bar>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--padding-xs)',
|
||||
color: 'var(--color-font)',
|
||||
fontSize: 'var(--font-size-xs)',
|
||||
}}
|
||||
>
|
||||
return (
|
||||
<Bar>
|
||||
<SettingsButton />
|
||||
</div>
|
||||
<KongLink className="made-with-love" href="https://konghq.com/">
|
||||
Made with <SvgIcon icon="heart" /> by Kong
|
||||
</KongLink>
|
||||
</Bar>;
|
||||
<KongLink className="made-with-love" href="https://konghq.com/">
|
||||
Made with <SvgIcon icon="heart" /> by Kong
|
||||
</KongLink>
|
||||
</Bar>
|
||||
);
|
||||
};
|
||||
|
@ -1,36 +1,27 @@
|
||||
import React, { FC, Fragment } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { GitRepository } from '../../models/git-repository';
|
||||
import { isRemoteProject, Project } from '../../models/project';
|
||||
import { isCollection, isDesign, Workspace } from '../../models/workspace';
|
||||
import { useVCS } from '../hooks/use-vcs';
|
||||
import { Project } from '../../models/project';
|
||||
import { isDesign, Workspace } from '../../models/workspace';
|
||||
import { ActivityToggle } from './activity-toggle';
|
||||
import { Breadcrumb } from './breadcrumb';
|
||||
import { GitSyncDropdown } from './dropdowns/git-sync-dropdown';
|
||||
import { SyncDropdown } from './dropdowns/sync-dropdown';
|
||||
import { WorkspaceDropdown } from './dropdowns/workspace-dropdown';
|
||||
|
||||
export const WorkspaceHeader: FC<{
|
||||
gitRepository: GitRepository | null;
|
||||
activeProject: Project;
|
||||
activeWorkspace: Workspace;
|
||||
}> = ({
|
||||
gitRepository,
|
||||
activeProject,
|
||||
activeWorkspace,
|
||||
}) => {
|
||||
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
|
||||
|
||||
const vcs = useVCS({
|
||||
workspaceId,
|
||||
});
|
||||
}> = ({ activeProject, activeWorkspace }) => {
|
||||
const { organizationId, projectId } = useParams<{
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
}>();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const crumbs = [
|
||||
{
|
||||
onClick: () => navigate(`/organization/${organizationId}/project/${projectId}`),
|
||||
onClick: () =>
|
||||
navigate(`/organization/${organizationId}/project/${projectId}`),
|
||||
id: activeProject._id,
|
||||
label: activeProject.name,
|
||||
node: <span data-testid="project">{activeProject.name}</span>,
|
||||
@ -46,8 +37,6 @@ export const WorkspaceHeader: FC<{
|
||||
<Fragment>
|
||||
<Breadcrumb crumbs={crumbs} />
|
||||
{isDesign(activeWorkspace) && <ActivityToggle />}
|
||||
{isDesign(activeWorkspace) && <GitSyncDropdown gitRepository={gitRepository} />}
|
||||
{isCollection(activeWorkspace) && isRemoteProject(activeProject) && vcs && <SyncDropdown key={workspaceId} workspace={activeWorkspace} project={activeProject} vcs={vcs} />}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
@ -286,6 +286,10 @@ const router = createMemoryRouter(
|
||||
{
|
||||
path: 'git',
|
||||
children: [
|
||||
{
|
||||
path: 'status',
|
||||
loader: async (...args) => (await import('./routes/git-actions')).gitStatusLoader(...args),
|
||||
},
|
||||
{
|
||||
path: 'changes',
|
||||
loader: async (...args) => (await import('./routes/git-actions')).gitChangesLoader(...args),
|
||||
|
@ -112,13 +112,13 @@ export const createNewWorkspaceAction: ActionFunction = async ({
|
||||
// Create default env, cookie jar, and meta
|
||||
await models.environment.getOrCreateForParentId(workspace._id);
|
||||
await models.cookieJar.getOrCreateForParentId(workspace._id);
|
||||
await models.workspaceMeta.getOrCreateByParentId(workspace._id);
|
||||
const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspace._id);
|
||||
|
||||
await database.flushChanges(flushId);
|
||||
if (session.isLoggedIn() && isRemoteProject(project) && isCollection(workspace)) {
|
||||
if (session.isLoggedIn() && isRemoteProject(project) && !workspaceMeta.gitRepositoryId) {
|
||||
const vcs = getVCS();
|
||||
if (vcs) {
|
||||
initializeLocalBackendProjectAndMarkForSync({
|
||||
await initializeLocalBackendProjectAndMarkForSync({
|
||||
vcs,
|
||||
workspace,
|
||||
});
|
||||
|
@ -15,6 +15,7 @@ import { isWebSocketRequest } from '../../models/websocket-request';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
import { SegmentEvent, trackSegmentEvent } from '../analytics';
|
||||
import { EnvironmentsDropdown } from '../components/dropdowns/environments-dropdown';
|
||||
import { WorkspaceSyncDropdown } from '../components/dropdowns/workspace-sync-dropdown';
|
||||
import { ErrorBoundary } from '../components/error-boundary';
|
||||
import { useDocBodyKeyboardShortcuts } from '../components/keydown-binder';
|
||||
import { showModal } from '../components/modals';
|
||||
@ -61,6 +62,7 @@ export interface GrpcRequestState {
|
||||
methods: GrpcMethodInfo[];
|
||||
reloadMethods: boolean;
|
||||
}
|
||||
|
||||
const INITIAL_GRPC_REQUEST_STATE = {
|
||||
running: false,
|
||||
requestMessages: [],
|
||||
@ -70,6 +72,7 @@ const INITIAL_GRPC_REQUEST_STATE = {
|
||||
methods: [],
|
||||
reloadMethods: true,
|
||||
};
|
||||
|
||||
export const Debug: FC = () => {
|
||||
const activeEnvironment = useSelector(selectActiveEnvironment);
|
||||
const activeRequest = useSelector(selectActiveRequest);
|
||||
@ -264,6 +267,7 @@ export const Debug: FC = () => {
|
||||
<SidebarChildren
|
||||
filter={sidebarFilter || ''}
|
||||
/>
|
||||
<WorkspaceSyncDropdown />
|
||||
</Fragment>
|
||||
: null}
|
||||
renderPaneOne={activeWorkspace ?
|
||||
|
@ -24,6 +24,7 @@ import {
|
||||
CodeEditorHandle,
|
||||
} from '../components/codemirror/code-editor';
|
||||
import { DesignEmptyState } from '../components/design-empty-state';
|
||||
import { WorkspaceSyncDropdown } from '../components/dropdowns/workspace-sync-dropdown';
|
||||
import { ErrorBoundary } from '../components/error-boundary';
|
||||
import { Notice, NoticeTable } from '../components/notice-table';
|
||||
import { SidebarLayout } from '../components/sidebar-layout';
|
||||
@ -256,9 +257,25 @@ const Design: FC = () => {
|
||||
apiSpec={apiSpec}
|
||||
handleSetSelection={handleScrollToSelection}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
gridRowStart: 6,
|
||||
}}
|
||||
>
|
||||
<WorkspaceSyncDropdown />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
) : (
|
||||
<EmptySpaceHelper>A spec navigator will render here</EmptySpaceHelper>
|
||||
<Fragment>
|
||||
<EmptySpaceHelper>A spec navigator will render here</EmptySpaceHelper>
|
||||
<div
|
||||
style={{
|
||||
gridRowStart: 6,
|
||||
}}
|
||||
>
|
||||
<WorkspaceSyncDropdown />
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
renderPaneTwo={showRightPane && <SwaggerUIDiv text={apiSpec.contents} />}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { invariant } from '@remix-run/router';
|
||||
import { fromUrl } from 'hosted-git-info';
|
||||
import { Errors } from 'isomorphic-git';
|
||||
import path from 'path';
|
||||
@ -34,18 +33,27 @@ import {
|
||||
addDotGit,
|
||||
getOauth2FormatName,
|
||||
} from '../../sync/git/utils';
|
||||
import { SegmentEvent, trackSegmentEvent, vcsSegmentEventProperties } from '../analytics';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
import {
|
||||
SegmentEvent,
|
||||
trackSegmentEvent,
|
||||
vcsSegmentEventProperties,
|
||||
} from '../analytics';
|
||||
|
||||
// Loaders
|
||||
export type GitRepoLoaderData = {
|
||||
branch: string;
|
||||
branches: string[];
|
||||
gitRepository: GitRepository | null;
|
||||
} | {
|
||||
errors: string[];
|
||||
};
|
||||
export type GitRepoLoaderData =
|
||||
| {
|
||||
branch: string;
|
||||
branches: string[];
|
||||
gitRepository: GitRepository | null;
|
||||
}
|
||||
| {
|
||||
errors: string[];
|
||||
};
|
||||
|
||||
export const gitRepoLoader: LoaderFunction = async ({ params }): Promise<GitRepoLoaderData> => {
|
||||
export const gitRepoLoader: LoaderFunction = async ({
|
||||
params,
|
||||
}): Promise<GitRepoLoaderData> => {
|
||||
try {
|
||||
const { workspaceId, projectId } = params;
|
||||
invariant(typeof workspaceId === 'string', 'Workspace Id is required');
|
||||
@ -54,89 +62,103 @@ export const gitRepoLoader: LoaderFunction = async ({ params }): Promise<GitRepo
|
||||
const workspace = await models.workspace.getById(workspaceId);
|
||||
invariant(workspace, 'Workspace not found');
|
||||
const workspaceMeta = await models.workspaceMeta.getByParentId(workspaceId);
|
||||
if (!workspaceMeta?.gitRepositoryId) {
|
||||
invariant(workspaceMeta, 'Workspace meta not found');
|
||||
if (!workspaceMeta.gitRepositoryId) {
|
||||
return {
|
||||
errors: ['Workspace is not linked to a git repository'],
|
||||
};
|
||||
}
|
||||
|
||||
const gitRepository = await models.gitRepository.getById(workspaceMeta?.gitRepositoryId);
|
||||
const gitRepository = await models.gitRepository.getById(
|
||||
workspaceMeta.gitRepositoryId
|
||||
);
|
||||
invariant(gitRepository, 'Git Repository not found');
|
||||
|
||||
if (!GitVCS.isInitializedForRepo(gitRepository._id)) {
|
||||
const baseDir = path.join(
|
||||
process.env['INSOMNIA_DATA_PATH'] || window.app.getPath('userData'),
|
||||
`version-control/git/${gitRepository._id}`,
|
||||
);
|
||||
if (GitVCS.isInitializedForRepo(gitRepository._id)) {
|
||||
return {
|
||||
branch: await GitVCS.getBranch(),
|
||||
branches: await GitVCS.listBranches(),
|
||||
gitRepository: gitRepository,
|
||||
};
|
||||
}
|
||||
|
||||
// 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(workspaceId, projectId);
|
||||
const baseDir = path.join(
|
||||
process.env['INSOMNIA_DATA_PATH'] || window.app.getPath('userData'),
|
||||
`version-control/git/${gitRepository._id}`
|
||||
);
|
||||
|
||||
// All git metadata in the GIT_INTERNAL_DIR directory is stored in a git/ directory on the filesystem
|
||||
const gitDataClient = fsClient(baseDir);
|
||||
// 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(workspaceId, projectId);
|
||||
|
||||
// All data outside the directories listed below will be stored in an 'other' directory. This is so we can support files that exist outside the ones the app is specifically in charge of.
|
||||
const otherDatClient = fsClient(path.join(baseDir, 'other'));
|
||||
// All git metadata in the GIT_INTERNAL_DIR directory is stored in a git/ directory on the filesystem
|
||||
const gitDataClient = fsClient(baseDir);
|
||||
|
||||
// The routable FS client directs isomorphic-git to read/write from the database or from the correct directory on the file system while performing git operations.
|
||||
const routableFS = routableFSClient(otherDatClient, {
|
||||
[GIT_INSOMNIA_DIR]: neDbClient,
|
||||
[GIT_INTERNAL_DIR]: gitDataClient,
|
||||
// All data outside the directories listed below will be stored in an 'other' directory. This is so we can support files that exist outside the ones the app is specifically in charge of.
|
||||
const otherDatClient = fsClient(path.join(baseDir, 'other'));
|
||||
|
||||
// The routable FS client directs isomorphic-git to read/write from the database or from the correct directory on the file system while performing git operations.
|
||||
const routableFS = routableFSClient(otherDatClient, {
|
||||
[GIT_INSOMNIA_DIR]: neDbClient,
|
||||
[GIT_INTERNAL_DIR]: gitDataClient,
|
||||
});
|
||||
|
||||
// Init VCS
|
||||
const { credentials, uri } = gitRepository;
|
||||
if (gitRepository.needsFullClone) {
|
||||
await GitVCS.initFromClone({
|
||||
repoId: gitRepository._id,
|
||||
url: uri,
|
||||
gitCredentials: credentials,
|
||||
directory: GIT_CLONE_DIR,
|
||||
fs: routableFS,
|
||||
gitDirectory: GIT_INTERNAL_DIR,
|
||||
});
|
||||
|
||||
// Init VCS
|
||||
const { credentials, uri } = gitRepository;
|
||||
if (gitRepository.needsFullClone) {
|
||||
await GitVCS.initFromClone({
|
||||
repoId: gitRepository._id,
|
||||
url: uri,
|
||||
gitCredentials: credentials,
|
||||
directory: GIT_CLONE_DIR,
|
||||
fs: routableFS,
|
||||
gitDirectory: GIT_INTERNAL_DIR,
|
||||
});
|
||||
|
||||
await models.gitRepository.update(gitRepository, {
|
||||
needsFullClone: false,
|
||||
});
|
||||
} else {
|
||||
await GitVCS.init({
|
||||
repoId: gitRepository._id,
|
||||
uri,
|
||||
directory: GIT_CLONE_DIR,
|
||||
fs: routableFS,
|
||||
gitDirectory: GIT_INTERNAL_DIR,
|
||||
gitCredentials: credentials,
|
||||
});
|
||||
}
|
||||
|
||||
// Configure basic info
|
||||
const { author, uri: gitUri } = gitRepository;
|
||||
await GitVCS.setAuthor(author.name, author.email);
|
||||
await GitVCS.addRemote(gitUri);
|
||||
await models.gitRepository.update(gitRepository, {
|
||||
needsFullClone: false,
|
||||
});
|
||||
} else {
|
||||
await GitVCS.init({
|
||||
repoId: gitRepository._id,
|
||||
uri,
|
||||
directory: GIT_CLONE_DIR,
|
||||
fs: routableFS,
|
||||
gitDirectory: GIT_INTERNAL_DIR,
|
||||
gitCredentials: credentials,
|
||||
});
|
||||
}
|
||||
|
||||
// Configure basic info
|
||||
const { author, uri: gitUri } = gitRepository;
|
||||
await GitVCS.setAuthor(author.name, author.email);
|
||||
await GitVCS.addRemote(gitUri);
|
||||
|
||||
return {
|
||||
branch: await GitVCS.getBranch(),
|
||||
branches: await GitVCS.listBranches(),
|
||||
gitRepository: gitRepository,
|
||||
};
|
||||
} catch (e) {
|
||||
const errorMessage = e instanceof Error ? e.message : 'Error while fetching git repository.';
|
||||
const errorMessage =
|
||||
e instanceof Error ? e.message : 'Error while fetching git repository.';
|
||||
return {
|
||||
errors: [errorMessage],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export type GitBranchesLoaderData = {
|
||||
branches: string[];
|
||||
remoteBranches: string[];
|
||||
} | {
|
||||
errors: string[];
|
||||
};
|
||||
export type GitBranchesLoaderData =
|
||||
| {
|
||||
branches: string[];
|
||||
remoteBranches: string[];
|
||||
}
|
||||
| {
|
||||
errors: string[];
|
||||
};
|
||||
|
||||
export const gitBranchesLoader: LoaderFunction = async ({ params }): Promise<GitBranchesLoaderData> => {
|
||||
export const gitBranchesLoader: LoaderFunction = async ({
|
||||
params,
|
||||
}): Promise<GitBranchesLoaderData> => {
|
||||
const { workspaceId, projectId } = params;
|
||||
invariant(typeof workspaceId === 'string', 'Workspace Id is required');
|
||||
invariant(typeof projectId === 'string', 'Project Id is required');
|
||||
@ -144,13 +166,16 @@ export const gitBranchesLoader: LoaderFunction = async ({ params }): Promise<Git
|
||||
const workspace = await models.workspace.getById(workspaceId);
|
||||
invariant(workspace, 'Workspace not found');
|
||||
const workspaceMeta = await models.workspaceMeta.getByParentId(workspaceId);
|
||||
if (!workspaceMeta?.gitRepositoryId) {
|
||||
invariant(workspaceMeta, 'Workspace meta not found');
|
||||
if (!workspaceMeta.gitRepositoryId) {
|
||||
return {
|
||||
errors: ['Workspace is not linked to a git repository'],
|
||||
};
|
||||
}
|
||||
|
||||
const gitRepository = await models.gitRepository.getById(workspaceMeta?.gitRepositoryId);
|
||||
const gitRepository = await models.gitRepository.getById(
|
||||
workspaceMeta.gitRepositoryId
|
||||
);
|
||||
invariant(gitRepository, 'Git Repository not found');
|
||||
|
||||
const branches = await GitVCS.listBranches();
|
||||
@ -163,11 +188,13 @@ export const gitBranchesLoader: LoaderFunction = async ({ params }): Promise<Git
|
||||
};
|
||||
};
|
||||
|
||||
export type GitFetchLoaderData = {
|
||||
errors: string[];
|
||||
} | {};
|
||||
export interface GitFetchLoaderData {
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export const gitFetchAction: ActionFunction = async ({ params }): Promise<GitFetchLoaderData> => {
|
||||
export const gitFetchAction: ActionFunction = async ({
|
||||
params,
|
||||
}): Promise<GitFetchLoaderData> => {
|
||||
const { workspaceId, projectId } = params;
|
||||
invariant(typeof workspaceId === 'string', 'Workspace Id is required');
|
||||
invariant(typeof projectId === 'string', 'Project Id is required');
|
||||
@ -175,17 +202,24 @@ export const gitFetchAction: ActionFunction = async ({ params }): Promise<GitFet
|
||||
const workspace = await models.workspace.getById(workspaceId);
|
||||
invariant(workspace, 'Workspace not found');
|
||||
const workspaceMeta = await models.workspaceMeta.getByParentId(workspaceId);
|
||||
if (!workspaceMeta?.gitRepositoryId) {
|
||||
invariant(workspaceMeta, 'Workspace meta not found');
|
||||
if (!workspaceMeta.gitRepositoryId) {
|
||||
return {
|
||||
errors: ['Workspace is not linked to a git repository'],
|
||||
};
|
||||
}
|
||||
|
||||
const gitRepository = await models.gitRepository.getById(workspaceMeta?.gitRepositoryId);
|
||||
const gitRepository = await models.gitRepository.getById(
|
||||
workspaceMeta.gitRepositoryId
|
||||
);
|
||||
invariant(gitRepository, 'Git Repository not found');
|
||||
|
||||
try {
|
||||
await GitVCS.fetch({ singleBranch: true, depth: 1, credentials: gitRepository?.credentials });
|
||||
await GitVCS.fetch({
|
||||
singleBranch: true,
|
||||
depth: 1,
|
||||
credentials: gitRepository.credentials,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return {
|
||||
@ -193,16 +227,22 @@ export const gitFetchAction: ActionFunction = async ({ params }): Promise<GitFet
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
return {
|
||||
errors: [],
|
||||
};
|
||||
};
|
||||
|
||||
export type GitLogLoaderData = {
|
||||
log: GitLogEntry[];
|
||||
} | {
|
||||
errors: string[];
|
||||
};
|
||||
export type GitLogLoaderData =
|
||||
| {
|
||||
log: GitLogEntry[];
|
||||
}
|
||||
| {
|
||||
errors: string[];
|
||||
};
|
||||
|
||||
export const gitLogLoader: LoaderFunction = async ({ params }): Promise<GitLogLoaderData> => {
|
||||
export const gitLogLoader: LoaderFunction = async ({
|
||||
params,
|
||||
}): Promise<GitLogLoaderData> => {
|
||||
const { workspaceId, projectId } = params;
|
||||
invariant(typeof workspaceId === 'string', 'Workspace Id is required');
|
||||
invariant(typeof projectId === 'string', 'Project Id is required');
|
||||
@ -210,13 +250,16 @@ export const gitLogLoader: LoaderFunction = async ({ params }): Promise<GitLogLo
|
||||
const workspace = await models.workspace.getById(workspaceId);
|
||||
invariant(workspace, 'Workspace not found');
|
||||
const workspaceMeta = await models.workspaceMeta.getByParentId(workspaceId);
|
||||
if (!workspaceMeta?.gitRepositoryId) {
|
||||
invariant(workspaceMeta, 'Workspace meta not found');
|
||||
if (!workspaceMeta.gitRepositoryId) {
|
||||
return {
|
||||
errors: ['Workspace is not linked to a git repository'],
|
||||
};
|
||||
}
|
||||
|
||||
const gitRepository = await models.gitRepository.getById(workspaceMeta?.gitRepositoryId);
|
||||
const gitRepository = await models.gitRepository.getById(
|
||||
workspaceMeta.gitRepositoryId
|
||||
);
|
||||
invariant(gitRepository, 'Git Repository not found');
|
||||
|
||||
const log = await GitVCS.log({ depth: 35 });
|
||||
@ -232,7 +275,9 @@ export interface GitChangesLoaderData {
|
||||
statusNames: Record<string, string>;
|
||||
}
|
||||
|
||||
export const gitChangesLoader: LoaderFunction = async ({ params }): Promise<GitChangesLoaderData> => {
|
||||
export const gitChangesLoader: LoaderFunction = async ({
|
||||
params,
|
||||
}): Promise<GitChangesLoaderData> => {
|
||||
const { workspaceId } = params;
|
||||
invariant(typeof workspaceId === 'string', 'Workspace Id is required');
|
||||
|
||||
@ -261,9 +306,11 @@ export const gitChangesLoader: LoaderFunction = async ({ params }): Promise<GitC
|
||||
};
|
||||
|
||||
// Actions
|
||||
type CloneGitActionResult = Response | {
|
||||
errors?: string[];
|
||||
};
|
||||
type CloneGitActionResult =
|
||||
| Response
|
||||
| {
|
||||
errors?: string[];
|
||||
};
|
||||
|
||||
export function parseGitToHttpsURL(s: string) {
|
||||
// try to convert any git URL to https URL
|
||||
@ -289,7 +336,7 @@ export function parseGitToHttpsURL(s: string) {
|
||||
export const cloneGitRepoAction: ActionFunction = async ({
|
||||
request,
|
||||
params,
|
||||
}): Promise<CloneGitActionResult> => {
|
||||
}): Promise<CloneGitActionResult> => {
|
||||
const { organizationId, projectId } = params;
|
||||
|
||||
invariant(typeof projectId === 'string', 'ProjectId is required.');
|
||||
@ -317,7 +364,10 @@ export const cloneGitRepoAction: ActionFunction = async ({
|
||||
// Git Credentials
|
||||
const oauth2format = formData.get('oauth2format');
|
||||
if (oauth2format) {
|
||||
invariant(oauth2format === 'gitlab' || oauth2format === 'github', 'OAuth2 format is required');
|
||||
invariant(
|
||||
oauth2format === 'gitlab' || oauth2format === 'github',
|
||||
'OAuth2 format is required'
|
||||
);
|
||||
const token = formData.get('token');
|
||||
invariant(typeof token === 'string', 'Token is required');
|
||||
const username = formData.get('username');
|
||||
@ -382,7 +432,9 @@ export const cloneGitRepoAction: ActionFunction = async ({
|
||||
providerName,
|
||||
});
|
||||
return {
|
||||
errors: ['Error Cloning Repository: failed to clone with and without `.git` suffix'],
|
||||
errors: [
|
||||
'Error Cloning Repository: failed to clone with and without `.git` suffix',
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -419,6 +471,7 @@ export const cloneGitRepoAction: ActionFunction = async ({
|
||||
parentId: project._id,
|
||||
description: `Insomnia Workspace for ${repoSettingsPatch.uri}}`,
|
||||
});
|
||||
await models.apiSpec.getOrCreateForParentId(workspace._id);
|
||||
trackSegmentEvent(SegmentEvent.vcsSyncComplete, {
|
||||
...vcsSegmentEventProperties('git', 'clone', 'no directory found'),
|
||||
providerName,
|
||||
@ -446,7 +499,11 @@ export const cloneGitRepoAction: ActionFunction = async ({
|
||||
|
||||
if (workspaces.length > 1) {
|
||||
trackSegmentEvent(SegmentEvent.vcsSyncComplete, {
|
||||
...vcsSegmentEventProperties('git', 'clone', 'multiple workspaces found'),
|
||||
...vcsSegmentEventProperties(
|
||||
'git',
|
||||
'clone',
|
||||
'multiple workspaces found'
|
||||
),
|
||||
providerName,
|
||||
});
|
||||
|
||||
@ -464,11 +521,17 @@ export const cloneGitRepoAction: ActionFunction = async ({
|
||||
|
||||
if (existingWorkspace) {
|
||||
trackSegmentEvent(SegmentEvent.vcsSyncComplete, {
|
||||
...vcsSegmentEventProperties('git', 'clone', 'workspace already exists'),
|
||||
...vcsSegmentEventProperties(
|
||||
'git',
|
||||
'clone',
|
||||
'workspace already exists'
|
||||
),
|
||||
providerName,
|
||||
});
|
||||
return {
|
||||
errors: [`Workspace ${existingWorkspace.name} already exists. Please delete it before cloning.`],
|
||||
errors: [
|
||||
`Workspace ${existingWorkspace.name} already exists. Please delete it before cloning.`,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@ -505,7 +568,9 @@ export const cloneGitRepoAction: ActionFunction = async ({
|
||||
|
||||
invariant(workspaceId, 'Workspace ID is required');
|
||||
|
||||
return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/${ACTIVITY_SPEC}`);
|
||||
return redirect(
|
||||
`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/${ACTIVITY_SPEC}`
|
||||
);
|
||||
};
|
||||
|
||||
export const updateGitRepoAction: ActionFunction = async ({
|
||||
@ -541,7 +606,10 @@ export const updateGitRepoAction: ActionFunction = async ({
|
||||
// Git Credentials
|
||||
const oauth2format = formData.get('oauth2format');
|
||||
if (oauth2format) {
|
||||
invariant(oauth2format === 'gitlab' || oauth2format === 'github', 'OAuth2 format is required');
|
||||
invariant(
|
||||
oauth2format === 'gitlab' || oauth2format === 'github',
|
||||
'OAuth2 format is required'
|
||||
);
|
||||
const token = formData.get('token');
|
||||
invariant(typeof token === 'string', 'Token is required');
|
||||
const username = formData.get('username');
|
||||
@ -581,9 +649,7 @@ export const updateGitRepoAction: ActionFunction = async ({
|
||||
return null;
|
||||
};
|
||||
|
||||
export const resetGitRepoAction: ActionFunction = async ({
|
||||
params,
|
||||
}) => {
|
||||
export const resetGitRepoAction: ActionFunction = async ({ params }) => {
|
||||
const { workspaceId } = params;
|
||||
invariant(workspaceId, 'Workspace ID is required');
|
||||
|
||||
@ -661,15 +727,21 @@ export const commitToGitRepoAction: ActionFunction = async ({
|
||||
});
|
||||
|
||||
for (const item of changesToCommit) {
|
||||
item.status.includes('deleted') ? await GitVCS.remove(item.path) : await GitVCS.add(item.path);
|
||||
item.status.includes('deleted')
|
||||
? await GitVCS.remove(item.path)
|
||||
: await GitVCS.add(item.path);
|
||||
}
|
||||
|
||||
await GitVCS.commit(message);
|
||||
|
||||
const providerName = getOauth2FormatName(repo?.credentials);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'commit'), providerName });
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, {
|
||||
...vcsSegmentEventProperties('git', 'commit'),
|
||||
providerName,
|
||||
});
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : 'Error while committing changes';
|
||||
const message =
|
||||
e instanceof Error ? e.message : 'Error while committing changes';
|
||||
return { errors: [message] };
|
||||
}
|
||||
|
||||
@ -677,10 +749,13 @@ export const commitToGitRepoAction: ActionFunction = async ({
|
||||
};
|
||||
|
||||
export interface CreateNewGitBranchResult {
|
||||
errors?: string[];
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export const createNewGitBranchAction: ActionFunction = async ({ request, params }): Promise<CreateNewGitBranchResult> => {
|
||||
export const createNewGitBranchAction: ActionFunction = async ({
|
||||
request,
|
||||
params,
|
||||
}): Promise<CreateNewGitBranchResult> => {
|
||||
const { workspaceId } = params;
|
||||
invariant(workspaceId, 'Workspace ID is required');
|
||||
|
||||
@ -703,9 +778,15 @@ export const createNewGitBranchAction: ActionFunction = async ({ request, params
|
||||
try {
|
||||
const providerName = getOauth2FormatName(repo?.credentials);
|
||||
await GitVCS.checkout(branch);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'create_branch'), providerName });
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, {
|
||||
...vcsSegmentEventProperties('git', 'create_branch'),
|
||||
providerName,
|
||||
});
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Something went wrong while creating a new branch';
|
||||
const errorMessage =
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: 'Something went wrong while creating a new branch';
|
||||
return {
|
||||
errors: [errorMessage],
|
||||
};
|
||||
@ -716,7 +797,7 @@ export const createNewGitBranchAction: ActionFunction = async ({ request, params
|
||||
|
||||
export interface CheckoutGitBranchResult {
|
||||
errors?: string[];
|
||||
}
|
||||
}
|
||||
export const checkoutGitBranchAction: ActionFunction = async ({
|
||||
request,
|
||||
params,
|
||||
@ -774,7 +855,10 @@ export interface MergeGitBranchResult {
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export const mergeGitBranchAction: ActionFunction = async ({ request, params }): Promise<MergeGitBranchResult> => {
|
||||
export const mergeGitBranchAction: ActionFunction = async ({
|
||||
request,
|
||||
params,
|
||||
}): Promise<MergeGitBranchResult> => {
|
||||
const { workspaceId } = params;
|
||||
invariant(workspaceId, 'Workspace ID is required');
|
||||
|
||||
@ -802,12 +886,18 @@ export const mergeGitBranchAction: ActionFunction = async ({ request, params }):
|
||||
const bufferId = await database.bufferChanges();
|
||||
|
||||
await GitVCS.checkout(branch);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'checkout_branch'), providerName });
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, {
|
||||
...vcsSegmentEventProperties('git', 'checkout_branch'),
|
||||
providerName,
|
||||
});
|
||||
await database.flushChanges(bufferId, true);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'merge_branch'), providerName });
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, {
|
||||
...vcsSegmentEventProperties('git', 'merge_branch'),
|
||||
providerName,
|
||||
});
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
||||
return { errors: [errorMessage] };
|
||||
return { errors: [errorMessage] };
|
||||
}
|
||||
|
||||
return {};
|
||||
@ -817,7 +907,10 @@ export interface DeleteGitBranchResult {
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export const deleteGitBranchAction: ActionFunction = async ({ request, params }): Promise<DeleteGitBranchResult> => {
|
||||
export const deleteGitBranchAction: ActionFunction = async ({
|
||||
request,
|
||||
params,
|
||||
}): Promise<DeleteGitBranchResult> => {
|
||||
const { workspaceId } = params;
|
||||
invariant(workspaceId, 'Workspace ID is required');
|
||||
|
||||
@ -842,10 +935,13 @@ export const deleteGitBranchAction: ActionFunction = async ({ request, params })
|
||||
try {
|
||||
const providerName = getOauth2FormatName(repo?.credentials);
|
||||
await GitVCS.deleteBranch(branch);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'delete_branch'), providerName });
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, {
|
||||
...vcsSegmentEventProperties('git', 'delete_branch'),
|
||||
providerName,
|
||||
});
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
||||
return { errors: [errorMessage] };
|
||||
return { errors: [errorMessage] };
|
||||
}
|
||||
|
||||
return {};
|
||||
@ -858,7 +954,7 @@ export interface PushToGitRemoteResult {
|
||||
export const pushToGitRemoteAction: ActionFunction = async ({
|
||||
request,
|
||||
params,
|
||||
}): Promise<PushToGitRemoteResult> => {
|
||||
}): Promise<PushToGitRemoteResult> => {
|
||||
const { workspaceId } = params;
|
||||
invariant(workspaceId, 'Workspace ID is required');
|
||||
const workspace = await models.workspace.getById(workspaceId);
|
||||
@ -896,11 +992,17 @@ export const pushToGitRemoteAction: ActionFunction = async ({
|
||||
const providerName = getOauth2FormatName(gitRepository.credentials);
|
||||
try {
|
||||
await GitVCS.push(gitRepository.credentials);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', force ? 'force_push' : 'push'), providerName });
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, {
|
||||
...vcsSegmentEventProperties('git', force ? 'force_push' : 'push'),
|
||||
providerName,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown Error';
|
||||
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'push', errorMessage), providerName });
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, {
|
||||
...vcsSegmentEventProperties('git', 'push', errorMessage),
|
||||
providerName,
|
||||
});
|
||||
|
||||
if (err instanceof Errors.PushRejectedError) {
|
||||
return {
|
||||
@ -919,7 +1021,7 @@ export const pushToGitRemoteAction: ActionFunction = async ({
|
||||
};
|
||||
|
||||
export interface PullFromGitRemoteResult {
|
||||
errors?: string[];
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export const pullFromGitRemoteAction: ActionFunction = async ({
|
||||
@ -945,17 +1047,27 @@ export const pullFromGitRemoteAction: ActionFunction = async ({
|
||||
const providerName = getOauth2FormatName(gitRepository.credentials);
|
||||
|
||||
try {
|
||||
await GitVCS.fetch({ singleBranch: true, depth: 1, credentials: gitRepository?.credentials });
|
||||
await GitVCS.fetch({
|
||||
singleBranch: true,
|
||||
depth: 1,
|
||||
credentials: gitRepository?.credentials,
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Error fetching from remote');
|
||||
}
|
||||
|
||||
try {
|
||||
await GitVCS.pull(gitRepository.credentials);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'pull'), providerName });
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, {
|
||||
...vcsSegmentEventProperties('git', 'pull'),
|
||||
providerName,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown Error';
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, vcsSegmentEventProperties('git', 'pull', errorMessage));
|
||||
trackSegmentEvent(
|
||||
SegmentEvent.vcsAction,
|
||||
vcsSegmentEventProperties('git', 'pull', errorMessage)
|
||||
);
|
||||
|
||||
return {
|
||||
errors: [`${errorMessage}`],
|
||||
@ -1006,8 +1118,10 @@ async function getGitChanges(vcs: typeof GitVCS, workspace: Workspace) {
|
||||
const statusNames: Record<string, string> = {};
|
||||
for (const doc of docs) {
|
||||
const name = (isApiSpec(doc) && doc.fileName) || doc.name || '';
|
||||
statusNames[path.join(GIT_INSOMNIA_DIR_NAME, doc.type, `${doc._id}.json`)] = name;
|
||||
statusNames[path.join(GIT_INSOMNIA_DIR_NAME, doc.type, `${doc._id}.yml`)] = name;
|
||||
statusNames[path.join(GIT_INSOMNIA_DIR_NAME, doc.type, `${doc._id}.json`)] =
|
||||
name;
|
||||
statusNames[path.join(GIT_INSOMNIA_DIR_NAME, doc.type, `${doc._id}.yml`)] =
|
||||
name;
|
||||
}
|
||||
// Create status items
|
||||
const items: Record<string, GitChange> = {};
|
||||
@ -1063,7 +1177,10 @@ export interface GitRollbackChangesResult {
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export const gitRollbackChangesAction: ActionFunction = async ({ params, request }): Promise<GitRollbackChangesResult> => {
|
||||
export const gitRollbackChangesAction: ActionFunction = async ({
|
||||
params,
|
||||
request,
|
||||
}): Promise<GitRollbackChangesResult> => {
|
||||
const { workspaceId } = params;
|
||||
invariant(typeof workspaceId === 'string', 'Workspace Id is required');
|
||||
|
||||
@ -1088,7 +1205,11 @@ export const gitRollbackChangesAction: ActionFunction = async ({ params, request
|
||||
const { changes } = await getGitChanges(GitVCS, workspace);
|
||||
|
||||
const files = changes
|
||||
.filter(i => changeType === 'modified' ? !i.status.includes('added') : i.status.includes('added'))
|
||||
.filter(i =>
|
||||
changeType === 'modified'
|
||||
? !i.status.includes('added')
|
||||
: i.status.includes('added')
|
||||
)
|
||||
// only rollback if editable
|
||||
.filter(i => i.editable)
|
||||
// only rollback if in selected path or for all paths
|
||||
@ -1100,7 +1221,8 @@ export const gitRollbackChangesAction: ActionFunction = async ({ params, request
|
||||
|
||||
await gitRollback(GitVCS, files);
|
||||
} catch (e) {
|
||||
const errorMessage = e instanceof Error ? e.message : 'Error while rolling back changes';
|
||||
const errorMessage =
|
||||
e instanceof Error ? e.message : 'Error while rolling back changes';
|
||||
return {
|
||||
errors: [errorMessage],
|
||||
};
|
||||
@ -1108,3 +1230,28 @@ export const gitRollbackChangesAction: ActionFunction = async ({ params, request
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
export interface GitStatusResult {
|
||||
status: {
|
||||
localChanges: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const gitStatusLoader: LoaderFunction = async ({
|
||||
params,
|
||||
}): Promise<GitStatusResult> => {
|
||||
const { workspaceId } = params;
|
||||
invariant(typeof workspaceId === 'string', 'Workspace Id is required');
|
||||
|
||||
const workspace = await models.workspace.getById(workspaceId);
|
||||
invariant(workspace, 'Workspace not found');
|
||||
|
||||
const { changes } = await getGitChanges(GitVCS, workspace);
|
||||
const localChanges = changes.filter(i => i.editable).length;
|
||||
|
||||
return {
|
||||
status: {
|
||||
localChanges,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ActionFunction, LoaderFunction } from 'react-router-dom';
|
||||
import { ActionFunction, LoaderFunction, redirect } from 'react-router-dom';
|
||||
|
||||
import { database } from '../../common/database';
|
||||
import { isNotNullOrUndefined } from '../../common/misc';
|
||||
@ -9,7 +9,8 @@ import { pullBackendProject } from '../../sync/vcs/pull-backend-project';
|
||||
import { getVCS } from '../../sync/vcs/vcs';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
|
||||
export const pullRemoteCollectionAction: ActionFunction = async ({ request }) => {
|
||||
export const pullRemoteCollectionAction: ActionFunction = async ({ request, params }) => {
|
||||
const { organizationId, projectId } = params;
|
||||
const formData = await request.formData();
|
||||
|
||||
const backendProjectId = formData.get('backendProjectId');
|
||||
@ -37,7 +38,11 @@ export const pullRemoteCollectionAction: ActionFunction = async ({ request }) =>
|
||||
// Remove all backend projects for workspace first
|
||||
await newVCS.removeBackendProjectsForRoot(backendProject.rootDocumentId);
|
||||
|
||||
await pullBackendProject({ vcs: newVCS, backendProject, remoteProjects });
|
||||
const { workspaceId } = await pullBackendProject({ vcs: newVCS, backendProject, remoteProjects });
|
||||
|
||||
if (workspaceId) {
|
||||
return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug`);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
@ -23,7 +23,7 @@ export const indexLoader: LoaderFunction = async ({ params }) => {
|
||||
return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${testSuiteId}/test-result/${testResult._id}`);
|
||||
}
|
||||
|
||||
return;
|
||||
return null;
|
||||
};
|
||||
|
||||
export const loader: LoaderFunction = async ({
|
||||
|
@ -221,7 +221,7 @@ export const indexLoader: LoaderFunction = async ({ params }) => {
|
||||
`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${unitTestSuites[0]._id}`
|
||||
);
|
||||
}
|
||||
return;
|
||||
return null;
|
||||
};
|
||||
|
||||
interface LoaderData {
|
||||
|
@ -15,9 +15,10 @@ import * as models from '../../models';
|
||||
import type { UnitTestSuite } from '../../models/unit-test-suite';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
import { Dropdown, DropdownButton, DropdownItem, ItemContent } from '../components/base/dropdown';
|
||||
import { WorkspaceSyncDropdown } from '../components/dropdowns/workspace-sync-dropdown';
|
||||
import { ErrorBoundary } from '../components/error-boundary';
|
||||
import { showPrompt } from '../components/modals';
|
||||
import { SidebarLayout } from '../components/sidebar-layout';
|
||||
import { SidebarFooter, SidebarLayout } from '../components/sidebar-layout';
|
||||
import { Button } from '../components/themed-button';
|
||||
import { TestRunStatus } from './test-results';
|
||||
import TestSuiteRoute from './test-suite';
|
||||
@ -161,6 +162,9 @@ const TestRoute: FC = () => {
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<SidebarFooter>
|
||||
<WorkspaceSyncDropdown />
|
||||
</SidebarFooter>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
renderPaneOne={
|
||||
|
Loading…
Reference in New Issue
Block a user