UNPKG

@jupyterlab/git

Version:

A JupyterLab extension for version control using git

204 lines (203 loc) 8.16 kB
import { Dialog, Notification, showDialog, showErrorMessage } from '@jupyterlab/apputils'; import ClearIcon from '@mui/icons-material/Clear'; import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; import * as React from 'react'; import { FixedSizeList } from 'react-window'; import { showError } from '../notifications'; import { filterClass, filterClearClass, filterInputClass, filterWrapperClass, listItemClass, listItemIconClass, nameClass, newBranchButtonClass, wrapperClass } from '../style/BranchMenu'; import { tagIcon } from '../style/icons'; import { NewTagDialogBox } from './NewTagDialog'; const ITEM_HEIGHT = 24.8; // HTML element height for a single tag const MIN_HEIGHT = 150; // Minimal HTML element height for the tags list const MAX_HEIGHT = 400; // Maximal HTML element height for the tags list /** * Callback invoked upon encountering an error when switching tags. * * @private * @param error - error * @param id - notificatin id * @param trans - Translation object */ function onTagError(error, id, trans) { if (error.message.includes('following files would be overwritten')) { // Empty log message to hide the executing alert Notification.dismiss(id); showDialog({ title: trans.__('Unable to checkout tag'), body: (React.createElement(React.Fragment, null, React.createElement("p", null, trans.__('Your changes to the following files would be overwritten by switching:')), React.createElement(List, null, error.message.split('\n').slice(1, -3).map(renderFileName)), React.createElement("span", null, trans.__('Please commit, stash, or discard your changes before you checkout tags.')))), buttons: [Dialog.okButton({ label: trans.__('Dismiss') })] }); } else { Notification.update({ id, message: trans.__('Failed to checkout tag.'), type: 'error', ...showError(error, trans) }); } } /** * Renders a file name. * * @private * @param filename - file name * @returns React element */ function renderFileName(filename) { return React.createElement(ListItem, { key: filename }, filename); } /** * React component for rendering a tag menu. */ export class TagMenu extends React.Component { /** * Returns a React component for rendering a tag menu. * * @param props - component properties * @returns React component */ constructor(props) { super(props); /** * Renders a menu item. * * @param props Row properties * @returns React element */ this._renderItem = (props) => { const { data, index, style } = props; const tag = data[index]; return (React.createElement(ListItem, { button: true, title: this.props.trans.__('Checkout to tag: %1', tag.name), className: listItemClass, onClick: this._onTagClickFactory(tag.name), style: style }, React.createElement(tagIcon.react, { className: listItemIconClass, tag: "span" }), React.createElement("span", { className: nameClass }, tag.name))); }; /** * Callback invoked upon a change to the menu filter. * * @param event - event object */ this._onFilterChange = (event) => { this.setState({ filter: event.target.value }); }; /** * Callback invoked to reset the menu filter. */ this._resetFilter = () => { this.setState({ filter: '' }); }; /** * Callback invoked upon clicking a button to create a new tag. * * @param event - event object */ this._onNewTagClick = () => { if (!this.props.branching) { showErrorMessage(this.props.trans.__('Creating a new tag is disabled'), this.props.trans.__('Error in creating new tag')); return; } this.setState({ tagDialog: true }); }; /** * Callback invoked upon closing a dialog to create a new tag. */ this._onNewTagDialogClose = () => { this.setState({ tagDialog: false }); }; this.state = { filter: '', tagDialog: false }; } /** * Renders the component. * * @returns React element */ render() { return (React.createElement("div", { className: wrapperClass }, this._renderFilter(), this._renderTagList(), this._renderNewTagDialog())); } /** * Renders a tag input filter. * * @returns React element */ _renderFilter() { return (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 tag 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), React.createElement("input", { className: newBranchButtonClass, type: "button", title: this.props.trans.__('Create a new tag'), value: this.props.trans.__('New Tag'), onClick: this._onNewTagClick }))); } /** * Renders list of tags. * * @returns React element */ _renderTagList() { // Perform a "simple" filter... (TODO: consider implementing fuzzy filtering) const filter = this.state.filter; const tags = this.props.tagsList.filter(tag => !filter || tag.name.includes(filter)); return (React.createElement(FixedSizeList, { height: Math.min(Math.max(MIN_HEIGHT, tags.length * ITEM_HEIGHT), MAX_HEIGHT), itemCount: tags.length, itemData: tags, itemKey: (index, data) => data[index].name, itemSize: ITEM_HEIGHT, style: { overflowX: 'hidden', paddingTop: 0, paddingBottom: 0 }, width: 'auto' }, this._renderItem)); } /** * Renders a dialog for creating a new tag. * * @returns React element */ _renderNewTagDialog() { const isSingleCommit = false; return (React.createElement(NewTagDialogBox, { pastCommits: this.props.pastCommits, model: this.props.model, trans: this.props.trans, open: this.state.tagDialog, onClose: this._onNewTagDialogClose, isSingleCommit: isSingleCommit })); } /** * Returns a callback which is invoked upon clicking a tag. * * @param tag - tag * @returns callback */ _onTagClickFactory(tag) { /** * Callback invoked upon clicking a tag. * * @private * @param event - event object * @returns promise which resolves upon attempting to switch tags */ const onClick = async () => { if (!this.props.branching) { showErrorMessage(this.props.trans.__('Checkout tags is disabled'), this.props.trans.__('The repository contains files with uncommitted changes. Please commit or discard these changes before switching to a tag.')); return; } const id = Notification.emit(this.props.trans.__('Checking tag out…'), 'in-progress', { autoClose: false }); try { await this.props.model.checkoutTag(tag); } catch (err) { return onTagError(err, id, this.props.trans); } Notification.update({ id, message: this.props.trans.__('Tag checkout.'), type: 'success', autoClose: 5000 }); }; return onClick; } }