Skip to content

Commit

Permalink
Provide UI feedback when switching branches
Browse files Browse the repository at this point in the history
  • Loading branch information
kgryte committed May 27, 2020
1 parent b1ec725 commit 126389f
Show file tree
Hide file tree
Showing 2 changed files with 183 additions and 92 deletions.
274 changes: 182 additions & 92 deletions src/components/BranchMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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: (
<React.Fragment>
<p>
Your changes to the following files would be overwritten by
switching:
</p>
<List>
{err.message
.split('\n')
.slice(1, -3)
.map(renderFileName)}
</List>
<span>
Please commit, stash, or discard your changes before you switch
branches.
</span>
</React.Fragment>
),
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 <ListItem key={filename}>{filename}</ListItem>;
}

/**
* Interface describing component properties.
*/
Expand All @@ -35,6 +85,11 @@ export interface IBranchMenuProps {
* Boolean indicating whether branching is disabled.
*/
branching: boolean;

/**
* Boolean indicating whether to enable UI suspension.
*/
suspend: boolean;
}

/**
Expand All @@ -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;
}

/**
Expand All @@ -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
};
}

Expand All @@ -110,46 +171,65 @@ export class BranchMenu extends React.Component<
render(): React.ReactElement {
return (
<div className={wrapperClass}>
<div className={filterWrapperClass}>
<div className={filterClass}>
<input
className={filterInputClass}
type="text"
onChange={this._onFilterChange}
value={this.state.filter}
placeholder="Filter"
title="Filter branch menu"
/>
{this.state.filter ? (
<button className={filterClearClass}>
<ClearIcon
titleAccess="Clear the current filter"
fontSize="small"
onClick={this._resetFilter}
/>
</button>
) : null}
</div>
{this._renderFilter()}
{this._renderBranchList()}
{this._renderNewBranchDialog()}
{this._renderFeedback()}
</div>
);
}

/**
* Renders a branch input filter.
*
* @returns React element
*/
private _renderFilter(): React.ReactElement {
return (
<div className={filterWrapperClass}>
<div className={filterClass}>
<input
className={newBranchButtonClass}
type="button"
title="Create a new branch"
value="New Branch"
onClick={this._onNewBranchClick}
className={filterInputClass}
type="text"
onChange={this._onFilterChange}
value={this.state.filter}
placeholder="Filter"
title="Filter branch menu"
/>
{this.state.filter ? (
<button className={filterClearClass}>
<ClearIcon
titleAccess="Clear the current filter"
fontSize="small"
onClick={this._resetFilter}
/>
</button>
) : null}
</div>
<div className={listWrapperClass}>
<List disablePadding>{this._renderItems()}</List>
</div>
<NewBranchDialog
open={this.state.branchDialog}
model={this.props.model}
onClose={this._onNewBranchDialogClose}
<input
className={newBranchButtonClass}
type="button"
title="Create a new branch"
value="New Branch"
onClick={this._onNewBranchClick}
/>
</div>
);
}

/**
* Renders a
*
* @returns React element
*/
private _renderBranchList(): React.ReactElement {
return (
<div className={listWrapperClass}>
<List disablePadding>{this._renderItems()}</List>
</div>
);
}

/**
* Renders menu items.
*
Expand Down Expand Up @@ -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 (
<NewBranchDialog
open={this.state.branchDialog}
model={this.props.model}
onClose={this._onNewBranchDialogClose}
/>
);
}

/**
* 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 (
<Modal open={this.state.suspend} onClick={this._onFeedbackModalClick}>
<CircularProgress className={fullscreenProgressClass} color="inherit" />
</Modal>
);
}

/**
* Adds model listeners.
*/
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -280,75 +404,41 @@ 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<void> {
let result: Array<any>;
if (!self.props.branching) {
showErrorMessage('Switching branches is disabled', CHANGES_ERR_MSG);
return;
}
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: (
<React.Fragment>
<p>
Your changes to the following files would be overwritten by
switching:
</p>
<List>
{err.message
.split('\n')
.slice(1, -3)
.map(renderFileName)}
</List>
<span>
Please commit, stash, or discard your changes before you switch
branches.
</span>
</React.Fragment>
),
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 <ListItem key={filename}>{filename}</ListItem>;
}
}

/**
* Callback invoked upon clicking on the feedback modal.
*
* @param event - event object
*/
private _onFeedbackModalClick = (): void => {
this._suspend(false);
};
}
1 change: 1 addition & 0 deletions src/components/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ export class Toolbar extends React.Component<IToolbarProps, IToolbarState> {
<BranchMenu
model={this.props.model}
branching={this.props.branching}
suspend={this.props.suspend}
/>
) : null}
</div>
Expand Down

0 comments on commit 126389f

Please sign in to comment.