From 3a90f515887624e9ea3860de871ac5efbd6f8c2c Mon Sep 17 00:00:00 2001 From: krassowski Date: Wed, 24 Feb 2021 15:51:33 +0000 Subject: [PATCH] Allow to customize the keystroke, add hint in placeholder --- schema/plugin.json | 9 ++- src/commandsAndMenu.tsx | 18 ++++++ src/components/CommitBox.tsx | 70 ++++++++++++++++++------ src/components/GitPanel.tsx | 2 + tests/test-components/CommitBox.spec.tsx | 69 ++++++++++++++++++----- 5 files changed, 134 insertions(+), 34 deletions(-) diff --git a/schema/plugin.json b/schema/plugin.json index e695e8381..2f8062414 100644 --- a/schema/plugin.json +++ b/schema/plugin.json @@ -59,5 +59,12 @@ "description": "If true, use a simplified concept of staging. Only files with changes are shown (instead of showing staged/changed/untracked), and all files with changes will be automatically staged", "default": false } - } + }, + "jupyter.lab.shortcuts": [ + { + "command": "git:submit-commit", + "keys": ["Accel Enter"], + "selector": ".jp-git-CommitBox" + } + ] } diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index c9e9ec6ef..34b922f4f 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -87,6 +87,8 @@ export namespace CommandIDs { export const gitIgnoreExtension = 'git:context-ignoreExtension'; } +export const SUBMIT_COMMIT_COMMAND = 'git:submit-commit'; + /** * Add the commands for the git extension. */ @@ -99,6 +101,22 @@ export function addCommands( ) { const { commands, shell } = app; + /** + * Commit using a keystroke combination when in CommitBox. + * + * This command is not accessible from the user interface (not visible), + * as it is handled by a signal listener in the CommitBox component instead. + * The label and caption are given to ensure that the command will + * show up in the shortcut editor UI with a nice description. + */ + commands.addCommand(SUBMIT_COMMIT_COMMAND, { + label: 'Commit from the Commit Box', + caption: + 'Submit the commit using the summary and description from commit box', + execute: () => void 0, + isVisible: () => false + }); + /** * Add open terminal in the Git repository */ diff --git a/src/components/CommitBox.tsx b/src/components/CommitBox.tsx index 506c45d4f..5549420ba 100644 --- a/src/components/CommitBox.tsx +++ b/src/components/CommitBox.tsx @@ -6,11 +6,18 @@ import { commitDescriptionClass, commitButtonClass } from '../style/CommitBox'; +import { CommandRegistry } from '@lumino/commands'; +import { SUBMIT_COMMIT_COMMAND } from '../commandsAndMenu'; /** * Interface describing component properties. */ export interface ICommitBoxProps { + /** + * Jupyter App commands registry + */ + commands: CommandRegistry; + /** * Boolean indicating whether files currently exist which have changes to commit. */ @@ -61,24 +68,37 @@ export class CommitBox extends React.Component< }; } + componentDidMount(): void { + this.props.commands.commandExecuted.connect(this._handleCommand); + } + + componentWillUnmount(): void { + this.props.commands.commandExecuted.disconnect(this._handleCommand); + } + /** * Renders the component. * * @returns React element */ render(): React.ReactElement { - const disabled = !(this.props.hasFiles && this.state.summary); + const disabled = !this._canCommit(); const title = !this.props.hasFiles ? 'Disabled: No files are staged for commit' : !this.state.summary ? 'Disabled: No commit message summary' : 'Commit'; + + const shortcutHint = CommandRegistry.formatKeystroke( + this._getSubmitKeystroke() + ); + const summaryPlaceholder = 'Summary (' + shortcutHint + ' to commit)'; return ( -
+
); } /** - * Callback invoked upon clicking a commit message submit button. + * Whether a commit can be performed (files are staged and summary is not empty). */ - private _onCommitClick = (): void => { + private _canCommit(): boolean { + return !!(this.props.hasFiles && this.state.summary); + } + + /** + * Get keystroke configured to act as a submit action. + */ + private _getSubmitKeystroke = (): string => { + const binding = this.props.commands.keyBindings.find( + binding => binding.command === SUBMIT_COMMIT_COMMAND + ); + return binding.keys.join(' '); + }; + + /** + * Callback invoked upon clicking a commit message submit button or otherwise submitting the form. + */ + private _onCommitSubmit = (): void => { const msg = this.state.summary + '\n\n' + this.state.description + '\n'; this.props.onCommit(msg); @@ -144,31 +180,29 @@ export class CommitBox extends React.Component< * ## Notes * * - Prevents triggering a `'submit'` action when hitting the `ENTER` key while entering a commit message summary. - * - Triggers the `'submit'` action when hitting `Ctrl` + `ENTER` * * @param event - event object */ private _onSummaryKeyPress = (event: React.KeyboardEvent): void => { if (event.key === 'Enter') { event.preventDefault(); - if (event.getModifierState('Control')) { - this._onCommitClick(); - } } }; /** - * Callback invoked upon a `'keypress'` event when entering a commit message description. + * Callback invoked upon command execution activated when entering a commit message description. * * ## Notes * - * - Triggers the `'submit'` action when hitting `Ctrl` + `ENTER` + * - Triggers the `'submit'` action on appropriate command (and if commit is possible) * - * @param event - event object */ - private _onDescriptionKeyPress = (event: React.KeyboardEvent): void => { - if (event.key === 'Enter' && event.getModifierState('Control')) { - this._onCommitClick(); + private _handleCommand = ( + _: CommandRegistry, + commandArgs: CommandRegistry.ICommandExecutedArgs + ): void => { + if (commandArgs.id === SUBMIT_COMMIT_COMMAND && this._canCommit()) { + this._onCommitSubmit(); } }; diff --git a/src/components/GitPanel.tsx b/src/components/GitPanel.tsx index 0d00ee07d..16ad915f5 100644 --- a/src/components/GitPanel.tsx +++ b/src/components/GitPanel.tsx @@ -372,11 +372,13 @@ export class GitPanel extends React.Component { 0} onCommit={this.commitMarkedFiles} + commands={this.props.commands} /> ) : ( )} diff --git a/tests/test-components/CommitBox.spec.tsx b/tests/test-components/CommitBox.spec.tsx index 79084d84f..7aa78cf44 100644 --- a/tests/test-components/CommitBox.spec.tsx +++ b/tests/test-components/CommitBox.spec.tsx @@ -1,14 +1,25 @@ import * as React from 'react'; import 'jest'; import { shallow } from 'enzyme'; -import { CommitBox } from '../../src/components/CommitBox'; +import { CommitBox} from '../../src/components/CommitBox'; +import { CommandRegistry } from '@lumino/commands'; +import { SUBMIT_COMMIT_COMMAND } from '../../src/commandsAndMenu'; describe('CommitBox', () => { + + const defaultCommands = new CommandRegistry() + defaultCommands.addKeyBinding({ + keys: ['Accel Enter'], + command: SUBMIT_COMMIT_COMMAND, + selector: '.jp-git-CommitBox' + }) + describe('#constructor()', () => { it('should return a new instance', () => { const box = new CommitBox({ onCommit: async () => {}, - hasFiles: false + hasFiles: false, + commands: defaultCommands }); expect(box).toBeInstanceOf(CommitBox); }); @@ -16,7 +27,8 @@ describe('CommitBox', () => { it('should set the default commit message summary to an empty string', () => { const box = new CommitBox({ onCommit: async () => {}, - hasFiles: false + hasFiles: false, + commands: defaultCommands }); expect(box.state.summary).toEqual(''); }); @@ -24,7 +36,8 @@ describe('CommitBox', () => { it('should set the default commit message description to an empty string', () => { const box = new CommitBox({ onCommit: async () => {}, - hasFiles: false + hasFiles: false, + commands: defaultCommands }); expect(box.state.description).toEqual(''); }); @@ -34,17 +47,36 @@ describe('CommitBox', () => { it('should display placeholder text for the commit message summary', () => { const props = { onCommit: async () => {}, - hasFiles: false + hasFiles: false, + commands: defaultCommands + }; + const component = shallow(); + const node = component.find('input[type="text"]').first(); + expect(node.prop('placeholder')).toEqual('Summary (Ctrl+Enter to commit)'); + }); + + it('should adjust placeholder text for the commit message summary when keybinding changes', () => { + const adjustedCommands = new CommandRegistry() + adjustedCommands.addKeyBinding({ + keys: ['Shift Enter'], + command: SUBMIT_COMMIT_COMMAND, + selector: '.jp-git-CommitBox' + }) + const props = { + onCommit: async () => {}, + hasFiles: false, + commands: adjustedCommands }; const component = shallow(); const node = component.find('input[type="text"]').first(); - expect(node.prop('placeholder')).toEqual('Summary (required)'); + expect(node.prop('placeholder')).toEqual('Summary (Shift+Enter to commit)'); }); it('should set a `title` attribute on the input element to provide a commit message summary', () => { const props = { onCommit: async () => {}, - hasFiles: false + hasFiles: false, + commands: defaultCommands }; const component = shallow(); const node = component.find('input[type="text"]').first(); @@ -54,17 +86,19 @@ describe('CommitBox', () => { it('should display placeholder text for the commit message description', () => { const props = { onCommit: async () => {}, - hasFiles: false + hasFiles: false, + commands: defaultCommands }; const component = shallow(); const node = component.find('TextareaAutosize').first(); - expect(node.prop('placeholder')).toEqual('Description'); + expect(node.prop('placeholder')).toEqual('Description (optional)'); }); it('should set a `title` attribute on the input element to provide a commit message description', () => { const props = { onCommit: async () => {}, - hasFiles: false + hasFiles: false, + commands: defaultCommands }; const component = shallow(); const node = component.find('TextareaAutosize').first(); @@ -74,7 +108,8 @@ describe('CommitBox', () => { it('should display a button to commit changes', () => { const props = { onCommit: async () => {}, - hasFiles: false + hasFiles: false, + commands: defaultCommands }; const component = shallow(); const node = component.find('input[type="button"]').first(); @@ -84,7 +119,8 @@ describe('CommitBox', () => { it('should set a `title` attribute on the button to commit changes', () => { const props = { onCommit: async () => {}, - hasFiles: false + hasFiles: false, + commands: defaultCommands }; const component = shallow(); const node = component.find('input[type="button"]').first(); @@ -94,7 +130,8 @@ describe('CommitBox', () => { it('should apply a class to disable the commit button when no files have changes to commit', () => { const props = { onCommit: async () => {}, - hasFiles: false + hasFiles: false, + commands: defaultCommands }; const component = shallow(); const node = component.find('input[type="button"]').first(); @@ -105,7 +142,8 @@ describe('CommitBox', () => { it('should apply a class to disable the commit button when files have changes to commit, but the user has not entered a commit message summary', () => { const props = { onCommit: async () => {}, - hasFiles: true + hasFiles: true, + commands: defaultCommands }; const component = shallow(); const node = component.find('input[type="button"]').first(); @@ -116,7 +154,8 @@ describe('CommitBox', () => { it('should not apply a class to disable the commit button when files have changes to commit and the user has entered a commit message summary', () => { const props = { onCommit: async () => {}, - hasFiles: true + hasFiles: true, + commands: defaultCommands }; const component = shallow(); component.setState({