From 126389f38b42fc7b5c4553f5c60bd1476f4e86de Mon Sep 17 00:00:00 2001 From: Athan Reines Date: Tue, 26 May 2020 17:13:08 -0700 Subject: [PATCH] Provide UI feedback when switching branches --- src/components/BranchMenu.tsx | 274 ++++++++++++++++++++++------------ src/components/Toolbar.tsx | 1 + 2 files changed, 183 insertions(+), 92 deletions(-) diff --git a/src/components/BranchMenu.tsx b/src/components/BranchMenu.tsx index 56f5d9d3b..0dc2850a3 100644 --- a/src/components/BranchMenu.tsx +++ b/src/components/BranchMenu.tsx @@ -3,7 +3,10 @@ import { classes } from 'typestyle'; import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; import ClearIcon from '@material-ui/icons/Clear'; +import Modal from '@material-ui/core/Modal'; +import CircularProgress from '@material-ui/core/CircularProgress'; import { Dialog, showDialog, showErrorMessage } from '@jupyterlab/apputils'; +import { sleep } from '../utils'; import { Git, IGitExtension } from '../tokens'; import { activeListItemClass, @@ -17,11 +20,58 @@ import { newBranchButtonClass, wrapperClass } from '../style/BranchMenu'; +import { fullscreenProgressClass } from '../style/progress'; import { NewBranchDialog } from './NewBranchDialog'; const CHANGES_ERR_MSG = 'The current branch contains files with uncommitted changes. Please commit or discard these changes before switching to or creating another branch.'; +/** + * Callback invoked upon encountering an error when switching branches. + * + * @private + * @param err - error + */ +function onBranchError(err: any): void { + if (err.message.includes('following files would be overwritten')) { + showDialog({ + title: 'Unable to switch branch', + body: ( + +

+ Your changes to the following files would be overwritten by + switching: +

+ + {err.message + .split('\n') + .slice(1, -3) + .map(renderFileName)} + + + Please commit, stash, or discard your changes before you switch + branches. + +
+ ), + buttons: [Dialog.okButton({ label: 'Dismiss' })] + }); + } else { + showErrorMessage('Error switching branch', err.message); + } +} + +/** + * Renders a file name. + * + * @private + * @param filename - file name + * @returns React element + */ +function renderFileName(filename: string): React.ReactElement { + return {filename}; +} + /** * Interface describing component properties. */ @@ -35,6 +85,11 @@ export interface IBranchMenuProps { * Boolean indicating whether branching is disabled. */ branching: boolean; + + /** + * Boolean indicating whether to enable UI suspension. + */ + suspend: boolean; } /** @@ -60,6 +115,11 @@ export interface IBranchMenuState { * Current list of branches. */ branches: Git.IBranch[]; + + /** + * Boolean indicating whether UI interaction should be suspended (e.g., due to pending command). + */ + suspend: boolean; } /** @@ -84,7 +144,8 @@ export class BranchMenu extends React.Component< filter: '', branchDialog: false, current: repo ? this.props.model.currentBranch.name : '', - branches: repo ? this.props.model.branches : [] + branches: repo ? this.props.model.branches : [], + suspend: false }; } @@ -110,46 +171,65 @@ export class BranchMenu extends React.Component< render(): React.ReactElement { return (
-
-
- - {this.state.filter ? ( - - ) : null} -
+ {this._renderFilter()} + {this._renderBranchList()} + {this._renderNewBranchDialog()} + {this._renderFeedback()} +
+ ); + } + + /** + * Renders a branch input filter. + * + * @returns React element + */ + private _renderFilter(): React.ReactElement { + return ( +
+
+ {this.state.filter ? ( + + ) : null}
-
- {this._renderItems()} -
-
); } + /** + * Renders a + * + * @returns React element + */ + private _renderBranchList(): React.ReactElement { + return ( +
+ {this._renderItems()} +
+ ); + } + /** * Renders menu items. * @@ -191,6 +271,37 @@ export class BranchMenu extends React.Component< ); } + /** + * Renders a dialog for creating a new branch. + * + * @returns React element + */ + private _renderNewBranchDialog(): React.ReactElement { + return ( + + ); + } + + /** + * Renders a component to provide UI feedback. + * + * @returns React element + */ + private _renderFeedback(): React.ReactElement | null { + if (this.props.suspend === false || this.state.suspend === false) { + return null; + } + return ( + + + + ); + } + /** * Adds model listeners. */ @@ -221,6 +332,19 @@ export class BranchMenu extends React.Component< }); } + /** + * Sets the suspension state. + * + * @param bool - boolean indicating whether to suspend UI interaction + */ + private _suspend(bool: boolean): void { + if (this.props.suspend) { + this.setState({ + suspend: bool + }); + } + } + /** * Callback invoked upon a change to the menu filter. * @@ -280,8 +404,10 @@ export class BranchMenu extends React.Component< * * @private * @param event - event object + * @returns promise which resolves upon attempting to switch branches */ - function onClick(): void { + async function onClick(): Promise { + let result: Array; if (!self.props.branching) { showErrorMessage('Switching branches is disabled', CHANGES_ERR_MSG); return; @@ -289,66 +415,30 @@ export class BranchMenu extends React.Component< const opts = { branchname: branch }; - self.props.model - .checkout(opts) - .then(onResolve) - .catch(onError); - } - - /** - * Callback invoked upon promise resolution. - * - * @private - * @param result - result - */ - function onResolve(result: any): void { - if (result.code !== 0) { - showErrorMessage('Error switching branch', result.message); + self._suspend(true); + try { + result = await Promise.all([ + sleep(1000), + self.props.model.checkout(opts) + ]); + } catch (err) { + self._suspend(false); + return onBranchError(err); } - } - - /** - * Callback invoked upon encountering an error. - * - * @private - * @param err - error - */ - function onError(err: any): void { - if (err.message.includes('following files would be overwritten')) { - showDialog({ - title: 'Unable to switch branch', - body: ( - -

- Your changes to the following files would be overwritten by - switching: -

- - {err.message - .split('\n') - .slice(1, -3) - .map(renderFileName)} - - - Please commit, stash, or discard your changes before you switch - branches. - -
- ), - buttons: [Dialog.okButton({ label: 'Dismiss' })] - }); - } else { - showErrorMessage('Error switching branch', err.message); + self._suspend(false); + const res = result[1] as Git.ICheckoutResult; + if (res.code !== 0) { + showErrorMessage('Error switching branch', res.message); } } - - /** - * Render a filename into a list - * @param filename - * @returns ReactElement - */ - function renderFileName(filename: string): React.ReactElement { - return {filename}; - } } + + /** + * Callback invoked upon clicking on the feedback modal. + * + * @param event - event object + */ + private _onFeedbackModalClick = (): void => { + this._suspend(false); + }; } diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index f46fdc126..e9393d540 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -299,6 +299,7 @@ export class Toolbar extends React.Component { ) : null}