insomnia/packages/insomnia-app/app/ui/components/modals/git-branches-modal.tsx
2021-05-19 08:32:18 +12:00

297 lines
9.1 KiB
TypeScript

import React, { Fragment, PureComponent } from 'react';
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import { AUTOBIND_CFG } from '../../../common/constants';
import Modal from '../base/modal';
import ModalBody from '../base/modal-body';
import ModalHeader from '../base/modal-header';
import { GitVCS } from '../../../sync/git/git-vcs';
import classnames from 'classnames';
import PromptButton from '../base/prompt-button';
import { database as db } from '../../../common/database';
import type { GitRepository } from '../../../models/git-repository';
import ModalFooter from '../base/modal-footer';
interface Props {
vcs: GitVCS;
gitRepository: GitRepository;
handleInitializeEntities: () => Promise<void>;
handleGitBranchChanged: (branch: string) => void;
}
interface State {
error: string;
branches: string[];
remoteBranches: string[];
branch: string;
newBranchName: string;
}
@autoBindMethodsForReact(AUTOBIND_CFG)
class GitBranchesModal extends PureComponent<Props, State> {
modal: Modal | null = null;
input: HTMLInputElement | null = null;
_onHide: (() => void) | null;
state: State = {
error: '',
branch: '??',
branches: [],
remoteBranches: [],
newBranchName: '',
}
_setModalRef(ref: Modal) {
this.modal = ref;
}
_setInputRef(ref: HTMLInputElement) {
this.input = ref;
}
async show(
options: {
onHide?: () => void;
} = {},
) {
await this._refreshState({
newBranchName: '',
});
this._onHide = options.onHide || null;
this.modal && this.modal.show();
// Focus input when modal shows
setTimeout(() => {
this.input && this.input.focus();
}, 100);
// Do a fetch of remotes and refresh again. NOTE: we're doing this
// last because it's super slow
const { vcs, gitRepository } = this.props;
await vcs.fetch(false, 1, gitRepository.credentials);
await this._refreshState();
}
async _refreshState(newState?: Record<string, any>) {
const { vcs, handleGitBranchChanged } = this.props;
const branch = await vcs.getBranch();
const branches = await vcs.listBranches();
const remoteBranches = await vcs.listRemoteBranches();
this.setState({
branch,
branches,
remoteBranches,
...newState,
});
handleGitBranchChanged(branch);
}
_handleClearError() {
this.setState({
error: '',
});
}
_handleHide() {
if (this._onHide) {
this._onHide();
}
}
async _errorHandler(cb: () => Promise<void>) {
try {
return await cb();
} catch (err) {
this.setState({
error: err.message,
});
}
}
async _handleCreate(e: React.SyntheticEvent<HTMLFormElement>) {
e.preventDefault();
await this._errorHandler(async () => {
const { vcs } = this.props;
const { newBranchName } = this.state;
await vcs.checkout(newBranchName);
await this._refreshState({
newBranchName: '',
});
});
}
async _handleMerge(branch: string) {
await this._errorHandler(async () => {
const { vcs } = this.props;
await vcs.merge(branch);
// Apparently merge doesn't update the working dir so need to checkout too
await this._handleCheckout(branch);
});
}
async _handleDelete(branch: string) {
await this._errorHandler(async () => {
const { vcs } = this.props;
await vcs.deleteBranch(branch);
await this._refreshState();
});
}
async _handleRemoteCheckout(branch: string) {
await this._errorHandler(async () => {
const { vcs, gitRepository } = this.props;
// First fetch more history to make sure we have lots
await vcs.fetch(true, 20, gitRepository.credentials);
await this._handleCheckout(branch);
});
}
async _handleCheckout(branch: string) {
await this._errorHandler(async () => {
const { vcs, handleInitializeEntities } = this.props;
const bufferId = await db.bufferChanges();
await vcs.checkout(branch);
await db.flushChanges(bufferId, true);
await handleInitializeEntities();
await this._refreshState();
});
}
_updateNewBranchName(e: React.SyntheticEvent<HTMLInputElement>) {
this.setState({
newBranchName: e.currentTarget.value,
});
}
hide() {
this.modal && this.modal.hide();
}
render() {
const { branch: currentBranch, branches, remoteBranches, newBranchName, error } = this.state;
const remoteOnlyBranches = remoteBranches.filter(b => !branches.includes(b));
return (
<Modal ref={this._setModalRef} onHide={this._handleHide}>
<ModalHeader>Local Branches</ModalHeader>
<ModalBody className="pad">
{error && (
<p className="notice error margin-bottom-sm no-margin-top">
<button className="pull-right icon" onClick={this._handleClearError}>
<i className="fa fa-times" />
</button>
{error}
</p>
)}
<form onSubmit={this._handleCreate}>
<div className="form-row">
<div className="form-control form-control--outlined">
<label>
New Branch Name
<input
type="text"
ref={this._setInputRef}
onChange={this._updateNewBranchName}
placeholder="testing-branch"
value={newBranchName}
/>
</label>
</div>
<div className="form-control form-control--no-label width-auto">
<button type="submit" className="btn btn--clicky" disabled={!newBranchName}>
Create
</button>
</div>
</div>
</form>
<div className="pad-top">
<table className="table--fancy table--outlined">
<thead>
<tr>
<th className="text-left">Branches</th>
<th className="text-right">&nbsp;</th>
</tr>
</thead>
<tbody>
{branches.map(name => (
<tr key={name} className="table--no-outline-row">
<td>
<span
className={classnames({
bold: name === currentBranch,
})}>
{name}
</span>
{name === currentBranch ? (
<span className="txt-sm space-left">(current)</span>
) : null}
</td>
<td className="text-right">
{name !== currentBranch && (
<Fragment>
<PromptButton
className="btn btn--micro btn--outlined space-left"
doneMessage="Merged"
onClick={() => this._handleMerge(name)}>
Merge
</PromptButton>
<PromptButton
className="btn btn--micro btn--outlined space-left"
doneMessage="Deleted"
onClick={() => this._handleDelete(name)}>
Delete
</PromptButton>
<button
className="btn btn--micro btn--outlined space-left"
onClick={() => this._handleCheckout(name)}>
Checkout
</button>
</Fragment>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{remoteOnlyBranches.length > 0 && (
<div className="pad-top">
<table className="table--fancy table--outlined">
<thead>
<tr>
<th className="text-left">Remote Branches</th>
<th className="text-right">&nbsp;</th>
</tr>
</thead>
<tbody>
{remoteOnlyBranches.map(name => (
<tr key={name} className="table--no-outline-row">
<td>{name}</td>
<td className="text-right">
<button
className="btn btn--micro btn--outlined space-left"
onClick={() => this._handleRemoteCheckout(name)}>
Checkout
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</ModalBody>
<ModalFooter>
<div className="margin-left italic txt-sm tall">
<i className="fa fa-code-fork" /> {currentBranch}
</div>
<div>
<button className="btn" onClick={this.hide}>
Done
</button>
</div>
</ModalFooter>
</Modal>
);
}
}
export default GitBranchesModal;