From 6a90fdf819508e78b1cd00d40aef43dab620486e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Collonval?= Date: Fri, 24 Jul 2020 10:43:50 +0200 Subject: [PATCH] Merge pull request #699 from mlucool/git-menu Clean up git menu --- src/components/Toolbar.tsx | 64 +++-------------- src/gitMenuCommands.ts | 96 ++++++++++++++++++++++++-- src/index.ts | 24 ++++--- src/tokens.ts | 3 + tests/test-components/Toolbar.spec.tsx | 59 ++++++++++------ 5 files changed, 155 insertions(+), 91 deletions(-) diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index 4e857d8db..98fa141be 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { classes } from 'typestyle'; -import { Dialog, showDialog } from '@jupyterlab/apputils'; import { PathExt } from '@jupyterlab/coreutils'; import { @@ -23,52 +22,9 @@ import { toolbarMenuWrapperClass, toolbarNavClass } from '../style/Toolbar'; -import { GitCredentialsForm } from '../widgets/CredentialsBox'; -import { GitPullPushDialog, Operation } from '../widgets/gitPushPull'; import { IGitExtension } from '../tokens'; import { BranchMenu } from './BranchMenu'; - -/** - * Displays an error dialog when a Git operation fails. - * - * @private - * @param model - Git extension model - * @param operation - Git operation name - * @returns Promise for displaying a dialog - */ -async function showGitOperationDialog( - model: IGitExtension, - operation: Operation -): Promise { - const title = `Git ${operation}`; - let result = await showDialog({ - title: title, - body: new GitPullPushDialog(model, operation), - buttons: [Dialog.okButton({ label: 'DISMISS' })] - }); - let retry = false; - while (!result.button.accept) { - const credentials = await showDialog({ - title: 'Git credentials required', - body: new GitCredentialsForm( - 'Enter credentials for remote repository', - retry ? 'Incorrect username or password.' : '' - ), - buttons: [Dialog.cancelButton(), Dialog.okButton({ label: 'OK' })] - }); - - if (!credentials.button.accept) { - break; - } - - result = await showDialog({ - title: title, - body: new GitPullPushDialog(model, operation, credentials.value), - buttons: [Dialog.okButton({ label: 'DISMISS' })] - }); - retry = true; - } -} +import { CommandIDs } from '../gitMenuCommands'; /** * Interface describing component properties. @@ -326,11 +282,10 @@ export class Toolbar extends React.Component { * @param event - event object */ private _onPullClick = (): void => { - showGitOperationDialog(this.props.model, Operation.Pull).catch(reason => { - console.error( - `Encountered an error when pulling changes. Error: ${reason}` - ); - }); + const commands = this.props.model.commands; + if (commands) { + commands.execute(CommandIDs.gitPull); + } }; /** @@ -339,11 +294,10 @@ export class Toolbar extends React.Component { * @param event - event object */ private _onPushClick = (): void => { - showGitOperationDialog(this.props.model, Operation.Push).catch(reason => { - console.error( - `Encountered an error when pushing changes. Error: ${reason}` - ); - }); + const commands = this.props.model.commands; + if (commands) { + commands.execute(CommandIDs.gitPush); + } }; /** diff --git a/src/gitMenuCommands.ts b/src/gitMenuCommands.ts index a449478d6..3410a7369 100644 --- a/src/gitMenuCommands.ts +++ b/src/gitMenuCommands.ts @@ -11,6 +11,8 @@ import { FileBrowser } from '@jupyterlab/filebrowser'; import { ITerminal } from '@jupyterlab/terminal'; import { IGitExtension } from './tokens'; import { doGitClone } from './widgets/gitClone'; +import { GitPullPushDialog, Operation } from './widgets/gitPushPull'; +import { GitCredentialsForm } from './widgets/CredentialsBox'; /** * The command IDs used by the git plugin. @@ -24,6 +26,8 @@ export namespace CommandIDs { export const gitToggleDoubleClickDiff = 'git:toggle-double-click-diff'; export const gitAddRemote = 'git:add-remote'; export const gitClone = 'git:clone'; + export const gitPush = 'git:push'; + export const gitPull = 'git:pull'; } /** @@ -63,7 +67,8 @@ export function addCommands( console.error(e); main.dispose(); } - } + }, + isEnabled: () => model.pathRepository !== null }); /** Add open/go to git interface command */ @@ -81,8 +86,8 @@ export function addCommands( /** Add git init command */ commands.addCommand(CommandIDs.gitInit, { - label: 'Init', - caption: ' Create an empty Git repository or reinitialize an existing one', + label: 'Initialize a Repository', + caption: 'Create an empty Git repository or reinitialize an existing one', execute: async () => { const currentPath = fileBrowser.model.path; const result = await showDialog({ @@ -95,7 +100,8 @@ export function addCommands( await model.init(currentPath); model.pathRepository = currentPath; } - } + }, + isEnabled: () => model.pathRepository === null }); /** Open URL externally */ @@ -127,7 +133,7 @@ export function addCommands( /** Command to add a remote Git repository */ commands.addCommand(CommandIDs.gitAddRemote, { - label: 'Add remote repository', + label: 'Add Remote Repository', caption: 'Add a Git remote repository', isEnabled: () => model.pathRepository !== null, execute: async args => { @@ -162,7 +168,7 @@ export function addCommands( /** Add git clone command */ commands.addCommand(CommandIDs.gitClone, { - label: 'Clone', + label: 'Clone a Repository', caption: 'Clone a repository from a URL', isEnabled: () => model.pathRepository === null, execute: async () => { @@ -170,4 +176,82 @@ export function addCommands( fileBrowser.model.refresh(); } }); + + /** Add git push command */ + commands.addCommand(CommandIDs.gitPush, { + label: 'Push to Remote', + caption: 'Push code to remote repository', + isEnabled: () => model.pathRepository !== null, + execute: async () => { + await Private.showGitOperationDialog(model, Operation.Push).catch( + reason => { + console.error( + `Encountered an error when pushing changes. Error: ${reason}` + ); + } + ); + } + }); + + /** Add git pull command */ + commands.addCommand(CommandIDs.gitPull, { + label: 'Pull from Remote', + caption: 'Pull latest code from remote repository', + isEnabled: () => model.pathRepository !== null, + execute: async () => { + await Private.showGitOperationDialog(model, Operation.Pull).catch( + reason => { + console.error( + `Encountered an error when pulling changes. Error: ${reason}` + ); + } + ); + } + }); +} + +/* eslint-disable no-inner-declarations */ +namespace Private { + /** + * Displays an error dialog when a Git operation fails. + * + * @private + * @param model - Git extension model + * @param operation - Git operation name + * @returns Promise for displaying a dialog + */ + export async function showGitOperationDialog( + model: IGitExtension, + operation: Operation + ): Promise { + const title = `Git ${operation}`; + let result = await showDialog({ + title: title, + body: new GitPullPushDialog(model, operation), + buttons: [Dialog.okButton({ label: 'DISMISS' })] + }); + let retry = false; + while (!result.button.accept) { + const credentials = await showDialog({ + title: 'Git credentials required', + body: new GitCredentialsForm( + 'Enter credentials for remote repository', + retry ? 'Incorrect username or password.' : '' + ), + buttons: [Dialog.cancelButton(), Dialog.okButton({ label: 'OK' })] + }); + + if (!credentials.button.accept) { + break; + } + + result = await showDialog({ + title: title, + body: new GitPullPushDialog(model, operation, credentials.value), + buttons: [Dialog.okButton({ label: 'DISMISS' })] + }); + retry = true; + } + } } +/* eslint-enable no-inner-declarations */ diff --git a/src/index.ts b/src/index.ts index 634098942..56ffb84d2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -146,30 +146,34 @@ function createGitMenu( const menu = new Menu({ commands }); menu.title.label = 'Git'; [ - CommandIDs.gitUI, - CommandIDs.gitTerminalCommand, CommandIDs.gitInit, CommandIDs.gitClone, - CommandIDs.gitAddRemote + CommandIDs.gitPush, + CommandIDs.gitPull, + CommandIDs.gitAddRemote, + CommandIDs.gitTerminalCommand ].forEach(command => { menu.addItem({ command }); }); + menu.addItem({ type: 'separator' }); + + menu.addItem({ command: CommandIDs.gitToggleSimpleStaging }); + + menu.addItem({ command: CommandIDs.gitToggleDoubleClickDiff }); + + menu.addItem({ type: 'separator' }); + const tutorial = new Menu({ commands }); - tutorial.title.label = ' Tutorial '; + tutorial.title.label = ' Help '; RESOURCES.map(args => { tutorial.addItem({ args, command: CommandIDs.gitOpenUrl }); }); - menu.addItem({ type: 'submenu', submenu: tutorial }); - - menu.addItem({ type: 'separator' }); - menu.addItem({ command: CommandIDs.gitToggleSimpleStaging }); - - menu.addItem({ command: CommandIDs.gitToggleDoubleClickDiff }); + menu.addItem({ type: 'submenu', submenu: tutorial }); return menu; } diff --git a/src/tokens.ts b/src/tokens.ts index bc9e28ffb..87f863d09 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -2,6 +2,7 @@ import { IChangedArgs } from '@jupyterlab/coreutils'; import { Token, JSONObject } from '@phosphor/coreutils'; import { IDisposable } from '@phosphor/disposable'; import { ISignal } from '@phosphor/signaling'; +import { CommandRegistry } from '@phosphor/commands'; export const EXTENSION_ID = 'jupyter.extensions.git_plugin'; @@ -56,6 +57,8 @@ export interface IGitExtension extends IDisposable { */ readonly statusChanged: ISignal; + readonly commands: CommandRegistry | null; + /** * Make request to add one or all files into * the staging area in repository diff --git a/tests/test-components/Toolbar.spec.tsx b/tests/test-components/Toolbar.spec.tsx index f8d4037a8..80231c6ca 100644 --- a/tests/test-components/Toolbar.spec.tsx +++ b/tests/test-components/Toolbar.spec.tsx @@ -1,9 +1,11 @@ -import * as React from 'react'; -import 'jest'; +import * as commands from '@phosphor/commands'; import { shallow } from 'enzyme'; -import { GitExtension } from '../../src/model'; -import * as git from '../../src/git'; +import 'jest'; +import * as React from 'react'; import { Toolbar } from '../../src/components/Toolbar'; +import * as git from '../../src/git'; +import { CommandIDs } from '../../src/gitMenuCommands'; +import { GitExtension } from '../../src/model'; import { pullButtonClass, pushButtonClass, @@ -11,10 +13,15 @@ import { toolbarMenuButtonClass } from '../../src/style/Toolbar'; +jest.mock('@phoshpor/commands'); jest.mock('../../src/git'); -async function createModel() { - const model = new GitExtension(); +async function createModel(commands?: commands.CommandRegistry) { + const app = { + commands, + shell: null as any + }; + const model = new GitExtension(app as any); jest.spyOn(model, 'currentBranch', 'get').mockReturnValue({ is_current_branch: true, @@ -292,17 +299,25 @@ describe('Toolbar', () => { describe('pull changes', () => { let model: GitExtension; + let mockedExecute: any; beforeEach(async () => { + const mockedCommands = commands as jest.Mocked; + mockedExecute = jest.fn(); + mockedCommands.CommandRegistry.mockImplementation(() => { + return { + execute: mockedExecute + } as any; + }); + const registry = new commands.CommandRegistry(); + const mock = git as jest.Mocked; mock.httpGitRequest.mockImplementation(request); - model = await createModel(); + model = await createModel(registry); }); it('should pull changes when the button to pull the latest changes is clicked', () => { - const spy = jest.spyOn(GitExtension.prototype, 'pull'); - const props = { model: model, branching: false, @@ -312,26 +327,32 @@ describe('Toolbar', () => { const button = node.find(`.${pullButtonClass}`); button.simulate('click'); - expect(spy).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenCalledWith(undefined); - - spy.mockRestore(); + expect(mockedExecute).toHaveBeenCalledTimes(1); + expect(mockedExecute).toHaveBeenCalledWith(CommandIDs.gitPull); }); }); describe('push changes', () => { let model: GitExtension; + let mockedExecute: any; beforeEach(async () => { + const mockedCommands = commands as jest.Mocked; + mockedExecute = jest.fn(); + mockedCommands.CommandRegistry.mockImplementation(() => { + return { + execute: mockedExecute + } as any; + }); + const registry = new commands.CommandRegistry(); + const mock = git as jest.Mocked; mock.httpGitRequest.mockImplementation(request); - model = await createModel(); + model = await createModel(registry); }); it('should push changes when the button to push the latest changes is clicked', () => { - const spy = jest.spyOn(GitExtension.prototype, 'push'); - const props = { model: model, branching: false, @@ -341,10 +362,8 @@ describe('Toolbar', () => { const button = node.find(`.${pushButtonClass}`); button.simulate('click'); - expect(spy).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenCalledWith(undefined); - - spy.mockRestore(); + expect(mockedExecute).toHaveBeenCalledTimes(1); + expect(mockedExecute).toHaveBeenCalledWith(CommandIDs.gitPush); }); });