From 8e7c85c5ff2df1173b8e7656c2fa020bcf72cb6a Mon Sep 17 00:00:00 2001 From: Opender Singh Date: Fri, 12 Feb 2021 14:52:43 +1300 Subject: [PATCH] Add pull dropdown to homepage (#3074) --- .../insomnia-app/app/models/workspace-meta.js | 2 +- .../app/sync/vcs/__tests__/index.test.js | 79 ++++++++ packages/insomnia-app/app/sync/vcs/index.js | 8 +- .../dropdowns/remote-workspaces-dropdown.js | 189 ++++++++++++++++++ .../dropdowns/workspace-dropdown.js | 152 +------------- .../app/ui/components/wrapper-home.js | 62 +++--- .../components/dropdown/dropdown.js | 6 +- 7 files changed, 317 insertions(+), 181 deletions(-) create mode 100644 packages/insomnia-app/app/ui/components/dropdowns/remote-workspaces-dropdown.js diff --git a/packages/insomnia-app/app/models/workspace-meta.js b/packages/insomnia-app/app/models/workspace-meta.js index 640dc065f..6def1f754 100644 --- a/packages/insomnia-app/app/models/workspace-meta.js +++ b/packages/insomnia-app/app/models/workspace-meta.js @@ -86,7 +86,7 @@ export function update( export async function updateByParentId( workspaceId: string, - patch: Object = {}, + patch: $Shape = {}, ): Promise { const meta = await getByParentId(workspaceId); return db.docUpdate(meta, patch); diff --git a/packages/insomnia-app/app/sync/vcs/__tests__/index.test.js b/packages/insomnia-app/app/sync/vcs/__tests__/index.test.js index cd99cae42..337752a9e 100644 --- a/packages/insomnia-app/app/sync/vcs/__tests__/index.test.js +++ b/packages/insomnia-app/app/sync/vcs/__tests__/index.test.js @@ -671,6 +671,7 @@ describe('VCS', () => { ]); }); }); + describe('describeChanges()', () => { it('works with same object structure', async () => { const a = { foo: 'bar', nested: { baz: 10 } }; @@ -690,4 +691,82 @@ describe('VCS', () => { expect(describeChanges(a, b)).toEqual([]); }); }); + + describe('getHistory()', () => { + let v; + beforeEach(async () => { + v = await vcs('master'); + + const status1 = await v.status( + [{ key: 'foo', name: 'Foo', document: newDoc('foobar1') }], + {}, + ); + const stage1 = await v.stage(status1.stage, [status1.unstaged.foo]); + await v.takeSnapshot(stage1, 'Add foo'); + + const status2 = await v.status( + [{ key: 'bar', name: 'Bar', document: newDoc('foobar2') }], + {}, + ); + const stage2 = await v.stage(status2.stage, [status2.unstaged.bar]); + await v.takeSnapshot(stage2, 'Add bar'); + }); + + it('returns all history', async () => { + // get all history + expect(await v.getHistory()).toStrictEqual([ + { + author: '', + created: expect.any(Date), + description: '', + id: 'f271aed3e12317215491ca1545770bd8289948e1', + name: 'Add foo', + parent: '0000000000000000000000000000000000000000', + state: [ + { + blob: 'f3827e9fdf461634c4ce528b88ced46fecf6509c', + key: 'foo', + name: 'Foo', + }, + ], + }, + { + author: '', + created: expect.any(Date), + description: '', + id: '3acf54915cd8d34a9cd9ea4ac3a988105b483869', + name: 'Add bar', + parent: 'f271aed3e12317215491ca1545770bd8289948e1', + state: [ + { + blob: 'f3827e9fdf461634c4ce528b88ced46fecf6509c', + key: 'foo', + name: 'Foo', + }, + { + blob: '2d1f409ab33b0b997d6c239a7754d24875570c2d', + key: 'bar', + name: 'Bar', + }, + ], + }, + ]); + }); + + it('returns recent history', async () => { + const [s1, s2, ...others] = await v.getHistory(); + + // There should only be two items + expect(others).toHaveLength(0); + + // Get the latest item + expect(await v.getHistory(1)).toStrictEqual([s2]); + + // Get the last 2 items + expect(await v.getHistory(2)).toStrictEqual([s1, s2]); + + // Get the last 3 items (only 2 exist) + expect(await v.getHistory(3)).toStrictEqual([s1, s2]); + }); + }); }); diff --git a/packages/insomnia-app/app/sync/vcs/index.js b/packages/insomnia-app/app/sync/vcs/index.js index 0f252eda8..b5d98da8f 100644 --- a/packages/insomnia-app/app/sync/vcs/index.js +++ b/packages/insomnia-app/app/sync/vcs/index.js @@ -379,10 +379,14 @@ export default class VCS { return branch.snapshots.length; } - async getHistory(): Promise> { + async getHistory(count: number = 0): Promise> { const branch = await this._getCurrentBranch(); const snapshots = []; - for (const id of branch.snapshots) { + + const total = branch.snapshots.length; + const slice = count <= 0 || count > total ? 0 : total - count; + + for (const id of branch.snapshots.slice(slice)) { const snapshot = await this._getSnapshot(id); if (snapshot === null) { throw new Error(`Failed to get snapshot id=${id}`); diff --git a/packages/insomnia-app/app/ui/components/dropdowns/remote-workspaces-dropdown.js b/packages/insomnia-app/app/ui/components/dropdowns/remote-workspaces-dropdown.js new file mode 100644 index 000000000..c8403f809 --- /dev/null +++ b/packages/insomnia-app/app/ui/components/dropdowns/remote-workspaces-dropdown.js @@ -0,0 +1,189 @@ +// @flow + +import * as React from 'react'; +import { autoBindMethodsForReact } from 'class-autobind-decorator'; +import { AUTOBIND_CFG } from '../../../common/constants'; +import * as session from '../../../account/session'; +import VCS from '../../../sync/vcs'; +import type { Project } from '../../../sync/types'; +import { Dropdown, DropdownDivider, DropdownItem, Button } from 'insomnia-components'; +import type { Workspace } from '../../../models/workspace'; +import HelpTooltip from '../help-tooltip'; +import * as models from '../../../models'; +import * as db from '../../../common/database'; +import { showAlert } from '../modals'; + +type Props = { + className?: string, + vcs?: VCS, + workspaces: Array, +}; + +type State = { + loading: boolean, + localProjects: Array, + pullingProjects: { [string]: boolean }, + remoteProjects: Array, +}; + +@autoBindMethodsForReact(AUTOBIND_CFG) +class RemoteWorkspacesDropdown extends React.Component { + constructor(props) { + super(props); + + this.state = { + loading: false, + localProjects: [], + pullingProjects: {}, + remoteProjects: [], + }; + } + + 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(); + for (const doc of await newVCS.allDocuments()) { + await db.upsert(doc); + } + await db.flushChanges(flushId); + } + + await this._refreshRemoteWorkspaces(); + } catch (err) { + this._dropdown && 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(); + } + + render() { + const { className, workspaces } = this.props; + + const { loading, remoteProjects, localProjects, pullingProjects } = this.state; + + if (!session.isLoggedIn()) { + return null; + } + + 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 = ( + + ); + + return ( + + + Remote Workspaces{' '} + + These workspaces have been shared with you via Insomnia Sync and do not yet exist on + your machine. + {' '} + {loading && } + + {missingRemoteProjects.length === 0 && ( + Nothing to pull + )} + {missingRemoteProjects.map(p => ( + this._handlePullRemoteWorkspace(p)} + icon={ + pullingProjects[p.id] ? ( + + ) : ( + + ) + }> + + Pull {p.name} + + + ))} + + ); + } +} + +export default RemoteWorkspacesDropdown; diff --git a/packages/insomnia-app/app/ui/components/dropdowns/workspace-dropdown.js b/packages/insomnia-app/app/ui/components/dropdowns/workspace-dropdown.js index 15d12f484..792ab3d11 100644 --- a/packages/insomnia-app/app/ui/components/dropdowns/workspace-dropdown.js +++ b/packages/insomnia-app/app/ui/components/dropdowns/workspace-dropdown.js @@ -11,11 +11,10 @@ import DropdownHint from '../base/dropdown/dropdown-hint'; import SettingsModal, { TAB_INDEX_EXPORT } from '../modals/settings-modal'; import * as models from '../../../models'; -import { showAlert, showError, showModal, showPrompt } from '../modals'; +import { showError, showModal, showPrompt } from '../modals'; import Link from '../base/link'; import WorkspaceSettingsModal from '../modals/workspace-settings-modal'; import LoginModal from '../modals/login-modal'; -import Tooltip from '../tooltip'; import KeydownBinder from '../keydown-binder'; import type { HotKeyRegistry } from '../../../common/hotkeys'; import { hotKeyRefs } from '../../../common/hotkeys'; @@ -24,8 +23,6 @@ import type { Workspace } from '../../../models/workspace'; import SyncShareModal from '../modals/sync-share-modal'; import * as db from '../../../common/database'; import VCS from '../../../sync/vcs'; -import HelpTooltip from '../help-tooltip'; -import type { Project } from '../../../sync/types'; import PromptButton from '../base/prompt-button'; import * as session from '../../../account/session'; import type { WorkspaceAction } from '../../../plugins'; @@ -52,9 +49,6 @@ type Props = { type State = { actionPlugins: Array, loadingActions: { [string]: boolean }, - localProjects: Array, - pullingProjects: { [string]: boolean }, - remoteProjects: Array, }; @autoBindMethodsForReact(AUTOBIND_CFG) @@ -64,9 +58,6 @@ class WorkspaceDropdown extends React.PureComponent { state = { actionPlugins: [], loadingActions: {}, - localProjects: [], - pullingProjects: {}, - remoteProjects: [], }; async _handlePluginClick(p: WorkspaceAction) { @@ -111,86 +102,11 @@ class WorkspaceDropdown extends React.PureComponent { } async _handleDropdownOpen() { - this._refreshRemoteWorkspaces(); - // Load action plugins const plugins = await getWorkspaceActions(); this.setState({ actionPlugins: plugins }); } - async _refreshRemoteWorkspaces() { - const { vcs } = this.props; - if (!vcs) { - return; - } - - if (!session.isLoggedIn()) { - return; - } - - const remoteProjects = await vcs.remoteProjects(); - const localProjects = await vcs.localProjects(); - this.setState({ remoteProjects, localProjects }); - } - - 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(); - for (const doc of await newVCS.allDocuments()) { - await db.upsert(doc); - } - await db.flushChanges(flushId); - } - - await this._refreshRemoteWorkspaces(); - } catch (err) { - this._dropdown && this._dropdown.hide(); - showAlert({ - title: 'Pull Error', - message: `Failed to pull workspace. ${err.message}`, - }); - } - - this.setState(state => ({ - pullingProjects: { ...state.pullingProjects, [project.id]: false }, - })); - } - _setDropdownRef(n: ?Dropdown) { this._dropdown = n; } @@ -234,17 +150,6 @@ class WorkspaceDropdown extends React.PureComponent { }); } - componentDidUpdate(prevProps: Props) { - // Reload workspaces if we just got a new VCS instance - if (this.props.vcs && !prevProps.vcs) { - this._refreshRemoteWorkspaces(); - } - } - - componentDidMount() { - this._refreshRemoteWorkspaces(); - } - render() { const { displayName, @@ -258,21 +163,6 @@ class WorkspaceDropdown extends React.PureComponent { ...other } = this.props; - const { remoteProjects, localProjects, pullingProjects } = this.state; - - 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 nonActiveWorkspaces = workspaces - .filter(w => w._id !== activeWorkspace._id) - .sort((w1, w2) => w1.name.localeCompare(w2.name)); const classes = classnames(className, 'wide', 'workspace-dropdown'); const { actionPlugins, loadingActions } = this.state; @@ -303,50 +193,10 @@ class WorkspaceDropdown extends React.PureComponent { Share {activeWorkspace.name} - Switch Workspace - - {nonActiveWorkspaces.map(w => { - const isUnseen = !!unseenWorkspaces.find(v => v._id === w._id); - return ( - - To {w.name} - {isUnseen && ( - - - - )} - - ); - })} - Create Workspace - {missingRemoteProjects.length > 0 && ( - - Remote Workspaces{' '} - - These workspaces have been shared with you via Insomnia Sync and do not yet exist on - your machine. - - - )} - - {missingRemoteProjects.map(p => ( - this._handlePullRemoteWorkspace(p)}> - {pullingProjects[p.id] ? ( - - ) : ( - - )} - Pull {p.name} - - ))} - {getAppName()} v{getAppVersion()} diff --git a/packages/insomnia-app/app/ui/components/wrapper-home.js b/packages/insomnia-app/app/ui/components/wrapper-home.js index 2114da18a..16c615075 100644 --- a/packages/insomnia-app/app/ui/components/wrapper-home.js +++ b/packages/insomnia-app/app/ui/components/wrapper-home.js @@ -54,6 +54,7 @@ import { } from '../../sync/git/git-vcs'; import { parseApiSpec } from '../../common/api-specs'; import SettingsModal from './modals/settings-modal'; +import RemoteWorkspacesDropdown from './dropdowns/remote-workspaces-dropdown'; type Props = {| wrapperProps: WrapperProps, @@ -294,7 +295,6 @@ class WrapperHome extends React.PureComponent { } else { handleSetActiveActivity(activeActivity); } - handleSetActiveWorkspace(id); } @@ -335,7 +335,7 @@ class WrapperHome extends React.PureComponent { let log = ; let branch = lastActiveBranch; - if (apiSpec && lastCommitTime && apiSpec.modified > lastCommitTime) { + if (w.scope === 'designer' && lastCommitTime && apiSpec?.modified > lastCommitTime) { // Show locally unsaved changes for spec // NOTE: this doesn't work for non-spec workspaces branch = lastActiveBranch + '*'; @@ -349,7 +349,7 @@ class WrapperHome extends React.PureComponent { branch = lastActiveBranch; log = ( - by {lastCommitAuthor} + {lastCommitAuthor && `by ${lastCommitAuthor}`} ); } @@ -417,14 +417,16 @@ class WrapperHome extends React.PureComponent { ); } - renderMenu() { + renderCreateMenu() { + const button = ( + + ); + return ( - ( - - )}> + New } onClick={this._handleWorkspaceCreate}> Blank Document @@ -448,6 +450,30 @@ class WrapperHome extends React.PureComponent { ); } + renderDashboardMenu() { + const { vcs, workspaces } = this.props.wrapperProps; + return ( +
+
+ + + + +
+ {this.renderCreateMenu()} + +
+ ); + } + render() { const { workspaces } = this.props.wrapperProps; const { filter } = this.state; @@ -479,21 +505,7 @@ class WrapperHome extends React.PureComponent {

Dashboard

- -
- - - - -
- {this.renderMenu()} -
+ {this.renderDashboardMenu()}
{cards} {filter && cards.length === 0 && ( diff --git a/packages/insomnia-components/components/dropdown/dropdown.js b/packages/insomnia-components/components/dropdown/dropdown.js index 03cbae4e3..c97a2a08e 100644 --- a/packages/insomnia-components/components/dropdown/dropdown.js +++ b/packages/insomnia-components/components/dropdown/dropdown.js @@ -474,6 +474,8 @@ class Dropdown extends PureComponent { } const noResults = filter && filterItems && filterItems.length === 0; + + const button = typeof renderButton === 'function' ? renderButton({ open }) : renderButton; return ( - {renderButton({ open })} + {button} {ReactDOM.createPortal(