@jupyterlab/git
Version:
A JupyterLab extension for version control using git
251 lines (250 loc) • 11.1 kB
JavaScript
import { Notification } from '@jupyterlab/apputils';
import ClearIcon from '@mui/icons-material/Clear';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import ListItem from '@mui/material/ListItem';
import * as React from 'react';
import { VariableSizeList } from 'react-window';
import { classes } from 'typestyle';
import { actionsWrapperClass, activeListItemClass, branchDialogClass, buttonClass, cancelButtonClass, closeButtonClass, contentWrapperClass, createButtonClass, errorMessageClass, filterClass, filterClearClass, filterInputClass, filterWrapperClass, listItemBoldTitleClass, listItemClass, listItemContentClass, listItemDescClass, listItemIconClass, listItemTitleClass, listWrapperClass, nameInputClass, titleClass, titleWrapperClass } from '../style/NewBranchDialog';
import { branchIcon } from '../style/icons';
const ITEM_HEIGHT = 27.5; // HTML element height for a single branch
const CURRENT_BRANCH_HEIGHT = 66.5; // HTML element height for the current branch with description
const HEIGHT = 200; // HTML element height for the branches list
/**
* React component for rendering a dialog to create a new branch.
*/
export class NewBranchDialog extends React.Component {
/**
* Returns a React component for rendering a branch menu.
*
* @param props - component properties
* @returns React component
*/
constructor(props) {
super(props);
/**
* Renders a branch menu item.
*
* @param props Row properties
* @returns React element
*/
this._renderItem = (props) => {
const { data, index, style } = props;
const branch = data[index];
const isBase = branch.name === this.state.base;
const isCurrent = branch.name === this.props.currentBranch;
let isBold;
let desc;
if (isCurrent) {
isBold = true;
desc = this.props.trans.__('The current branch. Pick this if you want to build on work done in this branch.');
}
else if (['main', 'main'].includes(branch.name)) {
isBold = true;
desc = this.props.trans.__('The default branch. Pick this if you want to start fresh from the default branch.');
}
return (React.createElement(ListItem, { button: true, title: this.props.trans.__('Create a new branch based on: %1', branch.name), className: classes(listItemClass, isBase ? activeListItemClass : null), onClick: this._onBranchClickFactory(branch.name), style: style },
React.createElement(branchIcon.react, { className: listItemIconClass, tag: "span" }),
React.createElement("div", { className: listItemContentClass },
React.createElement("p", { className: classes(listItemTitleClass, isBold ? listItemBoldTitleClass : null) }, branch.name),
desc ? (React.createElement("p", { className: listItemDescClass }, this.props.trans.__(desc))) : null)));
};
/**
* Callback invoked upon closing the dialog.
*
* @param event - event object
*/
this._onClose = () => {
this.props.onClose();
this.setState({
name: '',
filter: '',
error: ''
});
};
/**
* Callback invoked upon a change to the menu filter.
*
* @param event - event object
*/
this._onFilterChange = (event) => {
var _a;
(_a = this._branchList.current) === null || _a === void 0 ? void 0 : _a.resetAfterIndex(0);
this.setState({
filter: event.target.value
});
};
/**
* Callback invoked to reset the menu filter.
*/
this._resetFilter = () => {
var _a;
(_a = this._branchList.current) === null || _a === void 0 ? void 0 : _a.resetAfterIndex(0);
this.setState({
filter: ''
});
};
/**
* Callback invoked upon a change to the branch name input element.
*
* @param event - event object
*/
this._onNameChange = (event) => {
this.setState({
name: event.target.value,
error: ''
});
};
this._branchList = React.createRef();
this.state = {
name: '',
base: props.currentBranch || '',
filter: '',
error: ''
};
}
/**
* Renders a dialog for creating a new branch.
*
* @returns React element
*/
render() {
return (React.createElement(Dialog, { classes: {
paper: branchDialogClass
}, open: this.props.open, onClose: this._onClose },
React.createElement("div", { className: titleWrapperClass },
React.createElement("p", { className: titleClass }, this.props.trans.__('Create a Branch')),
React.createElement("button", { className: closeButtonClass },
React.createElement(ClearIcon, { titleAccess: this.props.trans.__('Close this dialog'), fontSize: "small", onClick: this._onClose }))),
React.createElement("div", { className: contentWrapperClass },
this.state.error ? (React.createElement("p", { className: errorMessageClass }, this.state.error)) : null,
React.createElement("p", null, this.props.trans.__('Name')),
React.createElement("input", { className: nameInputClass, type: "text", onChange: this._onNameChange, value: this.state.name, placeholder: "", title: this.props.trans.__('Enter a branch name') }),
React.createElement("p", null, this.props.trans.__('Create branch based on…')),
React.createElement("div", { className: filterWrapperClass },
React.createElement("div", { className: filterClass },
React.createElement("input", { className: filterInputClass, type: "text", onChange: this._onFilterChange, value: this.state.filter, placeholder: this.props.trans.__('Filter'), title: this.props.trans.__('Filter branch menu') }),
this.state.filter ? (React.createElement("button", { className: filterClearClass },
React.createElement(ClearIcon, { titleAccess: this.props.trans.__('Clear the current filter'), fontSize: "small", onClick: this._resetFilter }))) : null)),
this._renderItems()),
React.createElement(DialogActions, { className: actionsWrapperClass },
React.createElement("input", { className: classes(buttonClass, cancelButtonClass), type: "button", title: this.props.trans.__('Close this dialog without creating a new branch'), value: this.props.trans.__('Cancel'), onClick: this._onClose }),
React.createElement("input", { className: classes(buttonClass, createButtonClass), type: "button", title: this.props.trans.__('Create a new branch'), value: this.props.trans.__('Create Branch'), onClick: () => {
this._createBranch();
}, disabled: this.state.name === '' || this.state.error !== '' }))));
}
/**
* Renders branch menu items.
*
* @returns array of React elements
*/
_renderItems() {
const current = this.props.currentBranch;
// Perform a "simple" filter... (TODO: consider implementing fuzzy filtering)
const filter = this.state.filter;
const branches = this.props.branches
.filter(branch => !filter || branch.name.includes(filter))
.slice()
.sort(comparator);
return (React.createElement(VariableSizeList, { className: listWrapperClass, height: HEIGHT, estimatedItemSize: ITEM_HEIGHT, itemCount: branches.length, itemData: branches, itemKey: (index, data) => data[index].name, itemSize: index => {
const branch = branches[index];
return [this.props.currentBranch, 'main', 'main'].includes(branch.name)
? CURRENT_BRANCH_HEIGHT
: ITEM_HEIGHT;
}, ref: this._branchList, style: { overflowX: 'hidden' }, width: 'auto' }, this._renderItem));
/**
* Comparator function for sorting branches.
*
* @private
* @param a - first branch
* @param b - second branch
* @returns integer indicating sort order
*/
function comparator(a, b) {
if (a.name === current) {
return -1;
}
else if (b.name === current) {
return 1;
}
if (a.name === 'main') {
return -1;
}
else if (b.name === 'main') {
return 1;
}
if (a.name === 'main') {
return -1;
}
else if (b.name === 'main') {
return 1;
}
return 0;
}
}
/**
* Returns a callback which is invoked upon clicking a branch name.
*
* @param branch - branch name
* @returns callback
*/
_onBranchClickFactory(branch) {
/**
* Callback invoked upon clicking a branch name.
*
* @private
* @param event - event object
*/
const onClick = () => {
this.setState({
base: branch
});
};
return onClick;
}
/**
* Creates a new branch.
*
* @param branch - branch name
* @returns promise which resolves upon attempting to create a new branch
*/
async _createBranch() {
var _a;
const opts = {
newBranch: true,
branchname: this.state.name,
startpoint: this.state.base
};
const id = Notification.emit(this.props.trans.__('Creating branch…'), 'in-progress', { autoClose: false });
try {
await this.props.model.checkout(opts);
}
catch (err) {
this.setState({
error: err.message.replace(/^fatal:/, '')
});
Notification.update({
id,
message: this.props.trans.__('Failed to create branch.'),
type: 'error',
autoClose: false
});
return;
}
Notification.update({
id,
message: this.props.trans.__('Branch created.'),
type: 'success',
autoClose: 5000
});
// Close the branch dialog:
this.props.onClose();
// Reset the branch name and filter:
(_a = this._branchList.current) === null || _a === void 0 ? void 0 : _a.resetAfterIndex(0);
this.setState({
name: '',
filter: ''
});
}
}