mirror of
https://github.com/Kong/insomnia.git
synced 2026-10-27 17:19:42 -04:00
refactor: transform git modals to fc (#5179)
* transform git modals to fc * second pass * add GitRepositorySettingsModal * fixes * tweaks * sit sync dropdown second pass * git branches second pass * git staging second pass * repo settings second pass * fix types * fix button color * fix refresh
This commit is contained in:
1 parent
4ca4b54e96
commit
1bf7b7bc2b
7 files changed
+782
-1067
No files matched your search
@@ -1,22 +1,20 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import classnames from 'classnames';
|
||||
import React, { Fragment, PureComponent, ReactNode } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { AnyAction, bindActionCreators, Dispatch } from 'redux';
|
||||
import React, { FC, Fragment, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useMount } from 'react-use';
|
||||
|
||||
import { SegmentEvent, trackSegmentEvent, vcsSegmentEventProperties } from '../../../common/analytics';
|
||||
import { AUTOBIND_CFG } from '../../../common/constants';
|
||||
import { database as db } from '../../../common/database';
|
||||
import { docsGitSync } from '../../../common/documentation';
|
||||
import { isNotNullOrUndefined } from '../../../common/misc';
|
||||
import * as models from '../../../models';
|
||||
import type { GitRepository } from '../../../models/git-repository';
|
||||
import type { Workspace } from '../../../models/workspace';
|
||||
import type { GitLogEntry, GitVCS } from '../../../sync/git/git-vcs';
|
||||
import { MemClient } from '../../../sync/git/mem-client';
|
||||
import { getOauth2FormatName } from '../../../sync/git/utils';
|
||||
import { initialize as initializeEntities } from '../../redux/modules/entities';
|
||||
import * as gitActions from '../../redux/modules/git';
|
||||
import { selectActiveGitRepository } from '../../redux/selectors';
|
||||
import { type DropdownHandle, Dropdown } from '../base/dropdown/dropdown';
|
||||
import { DropdownButton } from '../base/dropdown/dropdown-button';
|
||||
import { DropdownDivider } from '../base/dropdown/dropdown-divider';
|
||||
@@ -28,44 +26,37 @@ import { GitBranchesModal } from '../modals/git-branches-modal';
|
||||
import { GitLogModal } from '../modals/git-log-modal';
|
||||
import { GitStagingModal } from '../modals/git-staging-modal';
|
||||
|
||||
type Props = ReturnType<typeof mapDispatchToProps> & {
|
||||
interface Props {
|
||||
workspace: Workspace;
|
||||
vcs: GitVCS;
|
||||
gitRepository: GitRepository | null;
|
||||
className?: string;
|
||||
renderDropdownButton?: (children: ReactNode) => ReactNode;
|
||||
};
|
||||
}
|
||||
|
||||
interface State {
|
||||
initializing: boolean;
|
||||
loadingPush: boolean;
|
||||
loadingPull: boolean;
|
||||
log: GitLogEntry[];
|
||||
branch: string;
|
||||
branches: string[];
|
||||
}
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
class GitSyncDropdown extends PureComponent<Props, State> {
|
||||
_dropdown: DropdownHandle | null = null;
|
||||
|
||||
state: State = {
|
||||
initializing: false,
|
||||
export const GitSyncDropdown: FC<Props> = ({ workspace, vcs, className }) => {
|
||||
const [state, setState] = useState<State>({
|
||||
loadingPush: false,
|
||||
loadingPull: false,
|
||||
log: [],
|
||||
branch: '',
|
||||
branches: [],
|
||||
};
|
||||
});
|
||||
const dispatch = useDispatch();
|
||||
const gitRepository = useSelector(selectActiveGitRepository);
|
||||
const dropdownRef = useRef<DropdownHandle>(null);
|
||||
|
||||
_setDropdownRef(dropdown: DropdownHandle) {
|
||||
this._dropdown = dropdown;
|
||||
}
|
||||
useMount(() => {
|
||||
refreshState();
|
||||
});
|
||||
|
||||
async _refreshState(otherState?: Record<string, any>) {
|
||||
const { vcs, workspace } = this.props;
|
||||
async function refreshState() {
|
||||
const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspace._id);
|
||||
|
||||
// Clear cached items and return if no state
|
||||
if (!vcs.isInitialized() || !workspaceMeta.gitRepositoryId) {
|
||||
// Don't update unnecessarily
|
||||
@@ -74,107 +65,49 @@ class GitSyncDropdown extends PureComponent<Props, State> {
|
||||
workspaceMeta.cachedGitLastAuthor,
|
||||
workspaceMeta.cachedGitLastCommitTime,
|
||||
].some(isNotNullOrUndefined);
|
||||
|
||||
if (needsUpdate) {
|
||||
await models.workspaceMeta.updateByParentId(workspace._id, {
|
||||
models.workspaceMeta.updateByParentId(workspace._id, {
|
||||
cachedGitRepositoryBranch: null,
|
||||
cachedGitLastAuthor: null,
|
||||
cachedGitLastCommitTime: null,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const branch = await vcs.getBranch();
|
||||
const branches = await vcs.listBranches();
|
||||
const log = (await vcs.log()) || [];
|
||||
this.setState({ ...(otherState || {}), log, branch, branches });
|
||||
setState(state => ({ ...state, log, branch, branches }));
|
||||
const author = log[0] ? log[0].commit.author : null;
|
||||
const cachedGitRepositoryBranch = branch;
|
||||
const cachedGitLastAuthor = author ? author.name : null;
|
||||
// NOTE: We're converting timestamp to ms here
|
||||
const cachedGitLastCommitTime = author ? author.timestamp * 1000 : null;
|
||||
await models.workspaceMeta.updateByParentId(workspace._id, {
|
||||
cachedGitRepositoryBranch,
|
||||
cachedGitLastAuthor,
|
||||
cachedGitRepositoryBranch: branch,
|
||||
cachedGitLastAuthor: author?.name || null,
|
||||
cachedGitLastCommitTime,
|
||||
});
|
||||
}
|
||||
|
||||
async _handleOpen() {
|
||||
await this._refreshState();
|
||||
}
|
||||
|
||||
async _handlePull() {
|
||||
this.setState({
|
||||
loadingPull: true,
|
||||
});
|
||||
const { vcs, gitRepository } = this.props;
|
||||
|
||||
async function handlePush({ force }: { force: boolean }) {
|
||||
if (!gitRepository) {
|
||||
// Should never happen
|
||||
throw new Error('Tried to pull without configuring git repo');
|
||||
return;
|
||||
}
|
||||
|
||||
const bufferId = await db.bufferChanges();
|
||||
const providerName = getOauth2FormatName(gitRepository.credentials);
|
||||
try {
|
||||
await vcs.pull(gitRepository.credentials);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'pull'), providerName });
|
||||
} catch (err) {
|
||||
showError({
|
||||
title: 'Error Pulling Repository',
|
||||
error: err,
|
||||
});
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, vcsSegmentEventProperties('git', 'pull', err.message));
|
||||
}
|
||||
|
||||
await db.flushChanges(bufferId);
|
||||
this.setState({
|
||||
loadingPull: false,
|
||||
});
|
||||
}
|
||||
|
||||
async _handlePush(_e: unknown, force = false) {
|
||||
this.setState({
|
||||
loadingPush: true,
|
||||
});
|
||||
const { vcs, gitRepository } = this.props;
|
||||
|
||||
if (!gitRepository) {
|
||||
// Should never happen
|
||||
throw new Error('Tried to push without configuring git repo');
|
||||
}
|
||||
|
||||
setState(state => ({ ...state, loadingPush: true }));
|
||||
// Check if there is anything to push
|
||||
let canPush = false;
|
||||
|
||||
try {
|
||||
canPush = await vcs.canPush(gitRepository.credentials);
|
||||
} catch (err) {
|
||||
showError({
|
||||
title: 'Error Pushing Repository',
|
||||
error: err,
|
||||
});
|
||||
this.setState({
|
||||
loadingPush: false,
|
||||
});
|
||||
showError({ title: 'Error Pushing Repository', error: err });
|
||||
setState(state => ({ ...state, loadingPush: false }));
|
||||
return;
|
||||
}
|
||||
|
||||
// If nothing to push, display that to the user
|
||||
if (!canPush) {
|
||||
showAlert({
|
||||
title: 'Push Skipped',
|
||||
message: 'Everything up-to-date. Nothing was pushed to the remote',
|
||||
});
|
||||
this.setState({
|
||||
loadingPush: false,
|
||||
});
|
||||
showAlert({ title: 'Push Skipped', message: 'Everything up-to-date. Nothing was pushed to the remote' });
|
||||
setState(state => ({ ...state, loadingPush: false }));
|
||||
return;
|
||||
}
|
||||
|
||||
const bufferId = await db.bufferChanges();
|
||||
const providerName = getOauth2FormatName(gitRepository.credentials);
|
||||
try {
|
||||
@@ -182,236 +115,180 @@ class GitSyncDropdown extends PureComponent<Props, State> {
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', force ? 'force_push' : 'push'), providerName });
|
||||
} catch (err) {
|
||||
if (err.code === 'PushRejectedError') {
|
||||
this._dropdown?.hide();
|
||||
dropdownRef.current?.hide();
|
||||
showAlert({
|
||||
title: 'Push Rejected',
|
||||
message: 'Do you want to force push?',
|
||||
okLabel: 'Force Push',
|
||||
addCancel: true,
|
||||
onConfirm: () => {
|
||||
this._handlePush(null, true);
|
||||
handlePush({ force: true });
|
||||
},
|
||||
});
|
||||
} else {
|
||||
showError({
|
||||
title: 'Error Pushing Repository',
|
||||
error: err,
|
||||
});
|
||||
showError({ title: 'Error Pushing Repository', error: err });
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', force ? 'force_push' : 'push', err.message), providerName });
|
||||
}
|
||||
}
|
||||
|
||||
await db.flushChanges(bufferId);
|
||||
this.setState({
|
||||
loadingPush: false,
|
||||
});
|
||||
setState(state => ({ ...state, loadingPush: false }));
|
||||
}
|
||||
|
||||
_handleConfig() {
|
||||
const { gitRepository, workspace, updateGitRepository, setupGitRepository } = this.props;
|
||||
|
||||
if (gitRepository) {
|
||||
updateGitRepository({
|
||||
gitRepository,
|
||||
});
|
||||
} else {
|
||||
setupGitRepository({
|
||||
workspace,
|
||||
createFsClient: MemClient.createClient,
|
||||
});
|
||||
}
|
||||
const { log, branches, branch, loadingPull, loadingPush } = state;
|
||||
let iconClassName = '';
|
||||
const providerName = getOauth2FormatName(gitRepository?.credentials);
|
||||
if (providerName === 'github') {
|
||||
iconClassName = 'fa fa-github';
|
||||
}
|
||||
|
||||
_handleLog() {
|
||||
showModal(GitLogModal);
|
||||
if (providerName === 'gitlab') {
|
||||
iconClassName = 'fa fa-gitlab';
|
||||
}
|
||||
return (
|
||||
<div className={className}>
|
||||
<Dropdown className="wide tall" ref={dropdownRef}>
|
||||
{vcs.isInitialized() ?
|
||||
(<DropdownButton className="btn--clicky-small btn-sync">
|
||||
{iconClassName && <i className={classnames('space-right', iconClassName)} />}
|
||||
<div className="ellipsis">{branch}</div>
|
||||
<i className="fa fa-code-fork space-left" />
|
||||
</DropdownButton>) :
|
||||
(<DropdownButton className="btn--clicky-small btn-sync">
|
||||
<i className="fa fa-code-fork space-right" />
|
||||
Setup Git Sync
|
||||
</DropdownButton>)}
|
||||
|
||||
async _handleCommit() {
|
||||
const { gitRepository } = this.props;
|
||||
showModal(GitStagingModal, {
|
||||
onCommit: this._refreshState,
|
||||
gitRepository,
|
||||
});
|
||||
}
|
||||
<DropdownDivider>
|
||||
Git Sync
|
||||
<HelpTooltip>
|
||||
Sync and collaborate with Git{' '}
|
||||
<Link href={docsGitSync}>
|
||||
<span className="no-wrap">
|
||||
<br />
|
||||
Documentation <i className="fa fa-external-link" />
|
||||
</span>
|
||||
</Link>
|
||||
</HelpTooltip>
|
||||
</DropdownDivider>
|
||||
|
||||
_handleManageBranches() {
|
||||
showModal(GitBranchesModal, {
|
||||
onHide: this._refreshState,
|
||||
});
|
||||
}
|
||||
<DropdownItem
|
||||
onClick={() => {
|
||||
if (gitRepository) {
|
||||
dispatch(gitActions.updateGitRepository({
|
||||
gitRepository,
|
||||
}));
|
||||
} else {
|
||||
dispatch(gitActions.setupGitRepository({
|
||||
workspace,
|
||||
createFsClient: MemClient.createClient,
|
||||
}));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i className="fa fa-wrench" /> Repository Settings
|
||||
</DropdownItem>
|
||||
|
||||
async _handleCheckoutBranch(branch: string) {
|
||||
const { vcs, handleInitializeEntities } = this.props;
|
||||
const bufferId = await db.bufferChanges();
|
||||
{vcs.isInitialized() && (
|
||||
<Fragment>
|
||||
<DropdownItem onClick={() => showModal(GitBranchesModal, { onCheckout: refreshState })}>
|
||||
<i className="fa fa-code-fork" /> Branches
|
||||
</DropdownItem>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
try {
|
||||
await vcs.checkout(branch);
|
||||
} catch (err) {
|
||||
showError({
|
||||
title: 'Checkout Error',
|
||||
error: err,
|
||||
});
|
||||
}
|
||||
|
||||
await db.flushChanges(bufferId, true);
|
||||
await handleInitializeEntities();
|
||||
await this._refreshState();
|
||||
}
|
||||
|
||||
_getProviderIconClassName(): string | undefined {
|
||||
const { gitRepository } = this.props;
|
||||
const providerName = getOauth2FormatName(gitRepository?.credentials);
|
||||
|
||||
if (providerName === 'github') {
|
||||
return 'fa fa-github';
|
||||
}
|
||||
|
||||
if (providerName === 'gitlab') {
|
||||
return 'fa fa-gitlab';
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._refreshState();
|
||||
}
|
||||
|
||||
renderButton() {
|
||||
const { branch } = this.state;
|
||||
const { vcs, renderDropdownButton } = this.props;
|
||||
|
||||
const renderBtn =
|
||||
renderDropdownButton ||
|
||||
(children => (
|
||||
<DropdownButton className="btn btn--compact wide text-left overflow-hidden row-spaced" name="git-sync-dropdown-btn">
|
||||
{children}
|
||||
</DropdownButton>
|
||||
));
|
||||
|
||||
if (!vcs.isInitialized()) {
|
||||
return renderBtn(
|
||||
<Fragment>
|
||||
<i className="fa fa-code-fork space-right" />
|
||||
Setup Git Sync
|
||||
</Fragment>,
|
||||
);
|
||||
}
|
||||
|
||||
const initializing = false;
|
||||
const iconClassName = this._getProviderIconClassName();
|
||||
return renderBtn(
|
||||
<Fragment>
|
||||
{iconClassName && <i className={classnames('space-right', iconClassName)} />}
|
||||
<div className="ellipsis">{initializing ? 'Initializing...' : branch}</div>
|
||||
<i className="fa fa-code-fork space-left" />
|
||||
</Fragment>,
|
||||
);
|
||||
}
|
||||
|
||||
renderBranch(branch: string) {
|
||||
const { branch: currentBranch } = this.state;
|
||||
const icon =
|
||||
branch === currentBranch ? <i className="fa fa-tag" /> : <i className="fa fa-empty" />;
|
||||
const isCurrentBranch = branch === currentBranch;
|
||||
return (
|
||||
<DropdownItem
|
||||
key={branch}
|
||||
onClick={isCurrentBranch ? undefined : () => this._handleCheckoutBranch(branch)}
|
||||
className={classnames({
|
||||
bold: isCurrentBranch,
|
||||
})}
|
||||
title={isCurrentBranch ? '' : `Switch to "${branch}"`}
|
||||
>
|
||||
{icon}
|
||||
{branch}
|
||||
</DropdownItem>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, vcs } = this.props;
|
||||
const { log, branches, branch, loadingPull, loadingPush } = this.state;
|
||||
return (
|
||||
<div className={className}>
|
||||
<Dropdown className="wide tall" onOpen={this._handleOpen} ref={this._setDropdownRef}>
|
||||
{this.renderButton()}
|
||||
|
||||
<DropdownDivider>
|
||||
Git Sync
|
||||
<HelpTooltip>
|
||||
Sync and collaborate with Git{' '}
|
||||
<Link href={docsGitSync}>
|
||||
<span className="no-wrap">
|
||||
<br />
|
||||
Documentation <i className="fa fa-external-link" />
|
||||
</span>
|
||||
</Link>
|
||||
</HelpTooltip>
|
||||
</DropdownDivider>
|
||||
|
||||
<DropdownItem onClick={this._handleConfig}>
|
||||
<i className="fa fa-wrench" /> Repository Settings
|
||||
</DropdownItem>
|
||||
|
||||
{vcs.isInitialized() && (
|
||||
<Fragment>
|
||||
<DropdownItem onClick={this._handleManageBranches}>
|
||||
<i className="fa fa-code-fork" /> Branches
|
||||
</DropdownItem>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
{vcs.isInitialized() && (
|
||||
<Fragment>
|
||||
<DropdownDivider>Branches</DropdownDivider>
|
||||
{branches.map(this.renderBranch)}
|
||||
|
||||
<DropdownDivider>{branch}</DropdownDivider>
|
||||
|
||||
<DropdownItem onClick={this._handleCommit}>
|
||||
<i className="fa fa-check" /> Commit
|
||||
</DropdownItem>
|
||||
{log.length > 0 && (
|
||||
<DropdownItem onClick={this._handlePush} stayOpenAfterClick>
|
||||
<i
|
||||
className={classnames({
|
||||
fa: true,
|
||||
'fa-spin fa-refresh': loadingPush,
|
||||
'fa-cloud-upload': !loadingPush,
|
||||
})}
|
||||
/>{' '}
|
||||
Push
|
||||
{vcs.isInitialized() && (
|
||||
<Fragment>
|
||||
<DropdownDivider>Branches</DropdownDivider>
|
||||
{branches.map(branch => {
|
||||
const { branch: currentBranch } = state;
|
||||
const icon = branch === currentBranch ? <i className="fa fa-tag" /> : <i className="fa fa-empty" />;
|
||||
const isCurrentBranch = branch === currentBranch;
|
||||
return (
|
||||
<DropdownItem
|
||||
key={branch}
|
||||
onClick={async () => {
|
||||
if (isCurrentBranch) {
|
||||
return;
|
||||
}
|
||||
const bufferId = await db.bufferChanges();
|
||||
try {
|
||||
await vcs.checkout(branch);
|
||||
} catch (error) {
|
||||
showError({ title: 'Checkout Error', error });
|
||||
}
|
||||
await db.flushChanges(bufferId, true);
|
||||
await dispatch(initializeEntities());
|
||||
refreshState();
|
||||
}}
|
||||
className={classnames({ bold: isCurrentBranch })}
|
||||
title={isCurrentBranch ? '' : `Switch to "${branch}"`}
|
||||
>
|
||||
{icon}
|
||||
{branch}
|
||||
</DropdownItem>
|
||||
)}
|
||||
<DropdownItem onClick={this._handlePull} stayOpenAfterClick>
|
||||
);
|
||||
})}
|
||||
|
||||
<DropdownDivider>{branch}</DropdownDivider>
|
||||
|
||||
<DropdownItem
|
||||
onClick={() => showModal(GitStagingModal, {
|
||||
onCommit: refreshState,
|
||||
gitRepository,
|
||||
})}
|
||||
>
|
||||
<i className="fa fa-check" /> Commit
|
||||
</DropdownItem>
|
||||
{log.length > 0 && (
|
||||
<DropdownItem onClick={() => handlePush({ force: false })} stayOpenAfterClick>
|
||||
<i
|
||||
className={classnames({
|
||||
fa: true,
|
||||
'fa-spin fa-refresh': loadingPull,
|
||||
'fa-cloud-download': !loadingPull,
|
||||
'fa-spin fa-refresh': loadingPush,
|
||||
'fa-cloud-upload': !loadingPush,
|
||||
})}
|
||||
/>{' '}
|
||||
Pull
|
||||
Push
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={this._handleLog} disabled={log.length === 0}>
|
||||
<i className="fa fa-clock-o" /> History ({log.length})
|
||||
</DropdownItem>
|
||||
</Fragment>
|
||||
)}
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch<AnyAction>) {
|
||||
const boundGitActions = bindActionCreators({ ...gitActions, initializeEntities }, dispatch);
|
||||
return {
|
||||
setupGitRepository: boundGitActions.setupGitRepository,
|
||||
updateGitRepository: boundGitActions.updateGitRepository,
|
||||
handleInitializeEntities: boundGitActions.initializeEntities,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(GitSyncDropdown);
|
||||
)}
|
||||
<DropdownItem
|
||||
onClick={async () => {
|
||||
setState(state => ({ ...state, loadingPull: true }));
|
||||
if (!gitRepository) {
|
||||
// Should never happen
|
||||
throw new Error('Tried to pull without configuring git repo');
|
||||
}
|
||||
const bufferId = await db.bufferChanges();
|
||||
const providerName = getOauth2FormatName(gitRepository.credentials);
|
||||
try {
|
||||
await vcs.pull(gitRepository.credentials);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'pull'), providerName });
|
||||
} catch (err) {
|
||||
showError({
|
||||
title: 'Error Pulling Repository',
|
||||
error: err,
|
||||
});
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, vcsSegmentEventProperties('git', 'pull', err.message));
|
||||
}
|
||||
await db.flushChanges(bufferId);
|
||||
setState(state => ({ ...state, loadingPull: false }));
|
||||
}}
|
||||
stayOpenAfterClick
|
||||
>
|
||||
<i
|
||||
className={classnames({
|
||||
fa: true,
|
||||
'fa-spin fa-refresh': loadingPull,
|
||||
'fa-cloud-download': !loadingPull,
|
||||
})}
|
||||
/>{' '}
|
||||
Pull
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={() => showModal(GitLogModal)} disabled={log.length === 0}>
|
||||
<i className="fa fa-clock-o" /> History ({log.length})
|
||||
</DropdownItem>
|
||||
</Fragment>
|
||||
)}
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,318 +1,261 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import classnames from 'classnames';
|
||||
import React, { Fragment, PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { AnyAction, bindActionCreators, Dispatch } from 'redux';
|
||||
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { SegmentEvent, trackSegmentEvent, vcsSegmentEventProperties } from '../../../common/analytics';
|
||||
import { AUTOBIND_CFG } from '../../../common/constants';
|
||||
import { database as db } from '../../../common/database';
|
||||
import type { GitRepository } from '../../../models/git-repository';
|
||||
import { GitVCS } from '../../../sync/git/git-vcs';
|
||||
import { getOauth2FormatName } from '../../../sync/git/utils';
|
||||
import { initialize as initializeEntities } from '../../redux/modules/entities';
|
||||
import { type ModalHandle, Modal } from '../base/modal';
|
||||
import { selectActiveGitRepository } from '../../redux/selectors';
|
||||
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
|
||||
import { ModalBody } from '../base/modal-body';
|
||||
import { ModalFooter } from '../base/modal-footer';
|
||||
import { ModalHeader } from '../base/modal-header';
|
||||
import { PromptButton } from '../base/prompt-button';
|
||||
|
||||
type Props = ReturnType<typeof mapDispatchToProps> & {
|
||||
type Props = ModalProps & {
|
||||
vcs: GitVCS;
|
||||
gitRepository: GitRepository;
|
||||
};
|
||||
|
||||
interface State {
|
||||
export interface GitBranchesModalOptions {
|
||||
onCheckout: () => void;
|
||||
}
|
||||
export interface State {
|
||||
error: string;
|
||||
branches: string[];
|
||||
remoteBranches: string[];
|
||||
branch: string;
|
||||
newBranchName: string;
|
||||
onCheckout: () => void;
|
||||
}
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
export class GitBranchesModalClass extends PureComponent<Props, State> {
|
||||
modal: ModalHandle | null = null;
|
||||
input: HTMLInputElement | null = null;
|
||||
_onHide?: (() => void) | null;
|
||||
|
||||
state: State = {
|
||||
export interface GitBranchesModalHandle {
|
||||
show: (options: GitBranchesModalOptions) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
export const GitBranchesModal = forwardRef<GitBranchesModalHandle, Props>(({ vcs }, ref) => {
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
const [state, setState] = useState<State>({
|
||||
error: '',
|
||||
branch: '??',
|
||||
branches: [],
|
||||
remoteBranches: [],
|
||||
newBranchName: '',
|
||||
};
|
||||
|
||||
_setModalRef(ref: ModalHandle) {
|
||||
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?.show();
|
||||
// Focus input when modal shows
|
||||
setTimeout(() => {
|
||||
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 } = this.props;
|
||||
onCheckout: () => {},
|
||||
});
|
||||
const dispatch = useDispatch();
|
||||
const gitRepository = useSelector(selectActiveGitRepository);
|
||||
const refreshState = useCallback(async () => {
|
||||
const branch = await vcs.getBranch();
|
||||
const branches = await vcs.listBranches();
|
||||
const remoteBranches = await vcs.listRemoteBranches();
|
||||
this.setState({
|
||||
setState(state => ({
|
||||
...state,
|
||||
branch,
|
||||
branches,
|
||||
remoteBranches,
|
||||
...newState,
|
||||
});
|
||||
}
|
||||
}));
|
||||
}, [vcs]);
|
||||
|
||||
_handleClearError() {
|
||||
this.setState({
|
||||
error: '',
|
||||
});
|
||||
}
|
||||
useImperativeHandle(ref, () => ({
|
||||
hide: () => {
|
||||
modalRef.current?.hide();
|
||||
},
|
||||
show: async ({ onCheckout }) => {
|
||||
setState(state => ({ ...state, newBranchName: '', onCheckout }));
|
||||
await refreshState();
|
||||
modalRef.current?.show({ onHide: onCheckout });
|
||||
// Do a fetch of remotes and refresh again. NOTE: we're doing this
|
||||
// last because it's super slow
|
||||
await vcs.fetch(false, 1, gitRepository?.credentials);
|
||||
await refreshState();
|
||||
},
|
||||
}), [refreshState, vcs, gitRepository?.credentials]);
|
||||
|
||||
_handleHide() {
|
||||
if (this._onHide) {
|
||||
this._onHide();
|
||||
}
|
||||
}
|
||||
|
||||
async _errorHandler(cb: () => Promise<void>) {
|
||||
const handleCheckout = async (branch: string) => {
|
||||
try {
|
||||
return await cb();
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _handleCreate(event: React.SyntheticEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
await this._errorHandler(async () => {
|
||||
const { vcs, gitRepository } = this.props;
|
||||
const { newBranchName } = this.state;
|
||||
const providerName = getOauth2FormatName(gitRepository.credentials);
|
||||
await vcs.checkout(newBranchName);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'create_branch'), providerName });
|
||||
await this._refreshState({
|
||||
newBranchName: '',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async _handleMerge(branch: string) {
|
||||
await this._errorHandler(async () => {
|
||||
const { vcs, gitRepository } = this.props;
|
||||
const providerName = getOauth2FormatName(gitRepository.credentials);
|
||||
await vcs.merge(branch);
|
||||
// Apparently merge doesn't update the working dir so need to checkout too
|
||||
await this._handleCheckout(branch);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'merge_branch'), providerName });
|
||||
});
|
||||
}
|
||||
|
||||
async _handleDelete(branch: string) {
|
||||
await this._errorHandler(async () => {
|
||||
const { vcs, gitRepository } = this.props;
|
||||
const providerName = getOauth2FormatName(gitRepository.credentials);
|
||||
await vcs.deleteBranch(branch);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'delete_branch'), providerName });
|
||||
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, gitRepository, handleInitializeEntities } = this.props;
|
||||
const bufferId = await db.bufferChanges();
|
||||
const providerName = getOauth2FormatName(gitRepository.credentials);
|
||||
const providerName = getOauth2FormatName(gitRepository?.credentials);
|
||||
await vcs.checkout(branch);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'checkout_branch'), providerName });
|
||||
await db.flushChanges(bufferId, true);
|
||||
await handleInitializeEntities();
|
||||
await this._refreshState();
|
||||
});
|
||||
}
|
||||
|
||||
_updateNewBranchName(event: React.SyntheticEvent<HTMLInputElement>) {
|
||||
this.setState({
|
||||
newBranchName: event.currentTarget.value,
|
||||
});
|
||||
}
|
||||
|
||||
hide() {
|
||||
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>
|
||||
await dispatch(initializeEntities());
|
||||
await refreshState();
|
||||
state.onCheckout?.();
|
||||
} catch (err) {
|
||||
setState(state => ({ ...state, error: err.message }));
|
||||
}
|
||||
};
|
||||
const { branch: currentBranch, branches, remoteBranches, newBranchName, error, onCheckout } = state;
|
||||
const remoteOnlyBranches = remoteBranches.filter(b => !branches.includes(b));
|
||||
return (
|
||||
<Modal ref={modalRef}>
|
||||
<ModalHeader>Local Branches</ModalHeader>
|
||||
<ModalBody className="pad">
|
||||
{error && (
|
||||
<p className="notice error margin-bottom-sm no-margin-top">
|
||||
<button className="pull-right icon" onClick={() => setState({ ...state, error: '' })}>
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<form
|
||||
onSubmit={async event => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const providerName = getOauth2FormatName(gitRepository?.credentials);
|
||||
await vcs.checkout(state.newBranchName);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'create_branch'), providerName });
|
||||
setState({ ...state, newBranchName: '' });
|
||||
refreshState();
|
||||
onCheckout?.();
|
||||
} catch (err) {
|
||||
setState(state => ({ ...state, error: err.message }));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="form-row">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
New Branch Name
|
||||
<input
|
||||
type="text"
|
||||
onChange={event => {
|
||||
setState({ ...state, newBranchName: event.currentTarget.value });
|
||||
}}
|
||||
placeholder="testing-branch"
|
||||
value={newBranchName}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
<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"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{branches.map(branch => (
|
||||
<tr key={branch} className="table--no-outline-row">
|
||||
<td>
|
||||
<span
|
||||
className={classnames({
|
||||
bold: branch === currentBranch,
|
||||
})}
|
||||
>
|
||||
{branch}
|
||||
</span>
|
||||
{branch === currentBranch ? (
|
||||
<span className="txt-sm space-left">(current)</span>
|
||||
) : null}
|
||||
</td>
|
||||
<td className="text-right">
|
||||
{branch !== currentBranch && (
|
||||
<>
|
||||
<PromptButton
|
||||
className="btn btn--micro btn--outlined space-left"
|
||||
doneMessage="Merged"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const providerName = getOauth2FormatName(gitRepository?.credentials);
|
||||
await vcs.merge(branch);
|
||||
// Apparently merge doesn't update the working dir so need to checkout too
|
||||
await handleCheckout(branch);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'merge_branch'), providerName });
|
||||
} catch (err) {
|
||||
setState(state => ({ ...state, error: err.message }));
|
||||
}
|
||||
}}
|
||||
>
|
||||
Merge
|
||||
</PromptButton>
|
||||
<PromptButton
|
||||
className="btn btn--micro btn--outlined space-left"
|
||||
doneMessage="Deleted"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const providerName = getOauth2FormatName(gitRepository?.credentials);
|
||||
await vcs.deleteBranch(branch);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'delete_branch'), providerName });
|
||||
refreshState();
|
||||
} catch (err) {
|
||||
setState(state => ({ ...state, error: err.message }));
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</PromptButton>
|
||||
<button
|
||||
className="btn btn--micro btn--outlined space-left"
|
||||
onClick={() => handleCheckout(branch)}
|
||||
>
|
||||
Checkout
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{remoteOnlyBranches.length > 0 && (
|
||||
<div className="pad-top">
|
||||
<table className="table--fancy table--outlined">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left">Branches</th>
|
||||
<th className="text-left">Remote Branches</th>
|
||||
<th className="text-right"> </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>
|
||||
{remoteOnlyBranches.map(branch => (
|
||||
<tr key={branch} className="table--no-outline-row">
|
||||
<td>{branch}</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>
|
||||
)}
|
||||
<button
|
||||
className="btn btn--micro btn--outlined space-left"
|
||||
onClick={async () => {
|
||||
try {
|
||||
// First fetch more history to make sure we have lots
|
||||
await vcs.fetch(true, 20, gitRepository?.credentials);
|
||||
handleCheckout(branch);
|
||||
} catch (err) {
|
||||
setState(state => ({ ...state, error: err.message }));
|
||||
}
|
||||
}}
|
||||
>
|
||||
Checkout
|
||||
</button>
|
||||
</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"> </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">
|
||||
<i className="fa fa-code-fork" /> {currentBranch}
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn" onClick={this.hide}>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
function mapDispatchToProps(dispatch: Dispatch<AnyAction>) {
|
||||
const boundGitActions = bindActionCreators({ initializeEntities }, dispatch);
|
||||
return {
|
||||
handleInitializeEntities: boundGitActions.initializeEntities,
|
||||
};
|
||||
}
|
||||
|
||||
export const GitBranchesModal = connect(null, mapDispatchToProps, null, { forwardRef: true })(GitBranchesModalClass);
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div className="margin-left italic txt-sm">
|
||||
<i className="fa fa-code-fork" /> {currentBranch}
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn" onClick={() => modalRef.current?.hide()}>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
GitBranchesModal.displayName = 'GitBranchesModal';
|
||||
@@ -1,104 +1,93 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
|
||||
|
||||
import { AUTOBIND_CFG } from '../../../common/constants';
|
||||
import type { GitLogEntry, GitVCS } from '../../../sync/git/git-vcs';
|
||||
import { type ModalHandle, Modal } from '../base/modal';
|
||||
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
|
||||
import { ModalBody } from '../base/modal-body';
|
||||
import { ModalFooter } from '../base/modal-footer';
|
||||
import { ModalHeader } from '../base/modal-header';
|
||||
import { TimeFromNow } from '../time-from-now';
|
||||
import { Tooltip } from '../tooltip';
|
||||
|
||||
interface Props {
|
||||
type Props = ModalProps & {
|
||||
vcs: GitVCS;
|
||||
}
|
||||
};
|
||||
|
||||
interface State {
|
||||
interface GitLogModalOptions {
|
||||
logs: GitLogEntry[];
|
||||
branch: string;
|
||||
}
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
export class GitLogModal extends PureComponent<Props, State> {
|
||||
modal: ModalHandle | null = null;
|
||||
|
||||
state: State = {
|
||||
export interface GitLogModalHandle {
|
||||
show: () => void;
|
||||
hide: () => void;
|
||||
}
|
||||
export const GitLogModal = forwardRef<GitLogModalHandle, Props>(({ vcs }, ref) => {
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
const [state, setState] = useState<GitLogModalOptions>({
|
||||
logs: [],
|
||||
branch: '??',
|
||||
};
|
||||
});
|
||||
|
||||
_setModalRef(ref: ModalHandle) {
|
||||
this.modal = ref;
|
||||
}
|
||||
useImperativeHandle(ref, () => ({
|
||||
hide: () => {
|
||||
modalRef.current?.hide();
|
||||
},
|
||||
show: async () => {
|
||||
const logs = await vcs.log();
|
||||
const branch = await vcs.getBranch();
|
||||
setState({ logs, branch });
|
||||
modalRef.current?.show();
|
||||
},
|
||||
}), [vcs]);
|
||||
|
||||
async show() {
|
||||
const { vcs } = this.props;
|
||||
const logs = await vcs.log();
|
||||
const branch = await vcs.getBranch();
|
||||
this.setState({
|
||||
logs,
|
||||
branch,
|
||||
});
|
||||
this.modal?.show();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.modal?.hide();
|
||||
}
|
||||
|
||||
renderLogEntryRow(entry: GitLogEntry) {
|
||||
const {
|
||||
commit: { author, message },
|
||||
oid,
|
||||
} = entry;
|
||||
return (
|
||||
<tr key={oid}>
|
||||
<td>{message}</td>
|
||||
<td>
|
||||
<TimeFromNow
|
||||
className="no-wrap"
|
||||
timestamp={author.timestamp * 1000}
|
||||
intervalSeconds={30}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Tooltip message={`${author.name} <${author.email}>`} delay={800}>
|
||||
{author.name}
|
||||
</Tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { logs, branch } = this.state;
|
||||
return (
|
||||
<Modal ref={this._setModalRef}>
|
||||
<ModalHeader>Git History ({logs.length})</ModalHeader>
|
||||
<ModalBody className="pad">
|
||||
<table className="table--fancy table--striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left">Message</th>
|
||||
<th className="text-left">When</th>
|
||||
<th className="text-left">Author</th>
|
||||
const { logs, branch } = state;
|
||||
return (
|
||||
<Modal ref={modalRef}>
|
||||
<ModalHeader>Git History ({logs.length})</ModalHeader>
|
||||
<ModalBody className="pad">
|
||||
<table className="table--fancy table--striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left">Message</th>
|
||||
<th className="text-left">When</th>
|
||||
<th className="text-left">Author</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{logs.map(entry => {
|
||||
const {
|
||||
commit: { author, message },
|
||||
oid,
|
||||
} = entry;
|
||||
return (
|
||||
<tr key={oid}>
|
||||
<td>{message}</td>
|
||||
<td>
|
||||
<TimeFromNow
|
||||
className="no-wrap"
|
||||
timestamp={author.timestamp * 1000}
|
||||
intervalSeconds={30}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Tooltip message={`${author.name} <${author.email}>`} delay={800}>
|
||||
{author.name}
|
||||
</Tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{logs.map(this.renderLogEntryRow)}</tbody>
|
||||
</table>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div className="margin-left italic txt-sm">
|
||||
<i className="fa fa-code-fork" /> {branch}
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn" onClick={this.hide}>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
})}</tbody>
|
||||
</table>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div className="margin-left italic txt-sm">
|
||||
<i className="fa fa-code-fork" /> {branch}
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn" onClick={() => modalRef.current?.hide()}>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
GitLogModal.displayName = 'GitLogModal';
|
||||
+40
-63
@@ -1,13 +1,11 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import React, { PureComponent, useState } from 'react';
|
||||
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
|
||||
import { AUTOBIND_CFG } from '../../../../common/constants';
|
||||
import { docsGitSync } from '../../../../common/documentation';
|
||||
import type { GitRepository, OauthProviderName } from '../../../../models/git-repository';
|
||||
import { deleteGitRepository } from '../../../../models/helpers/git-repository-operations';
|
||||
import { Link } from '../../base/link';
|
||||
import { type ModalHandle, Modal } from '../../base/modal';
|
||||
import { type ModalHandle, Modal, ModalProps } from '../../base/modal';
|
||||
import { ModalBody } from '../../base/modal-body';
|
||||
import { ModalFooter } from '../../base/modal-footer';
|
||||
import { ModalHeader } from '../../base/modal-header';
|
||||
@@ -17,70 +15,49 @@ import { CustomRepositorySettingsFormGroup } from './custom-repository-settings-
|
||||
import { GitHubRepositorySetupFormGroup } from './github-repository-settings-form-group';
|
||||
import { GitLabRepositorySetupFormGroup } from './gitlab-repository-settings-form-group';
|
||||
|
||||
interface State {
|
||||
interface GitRepositorySettingsModalOptions {
|
||||
gitRepository: GitRepository | null;
|
||||
onSubmitEdits: (gitRepoPatch: Partial<GitRepository>) => void;
|
||||
}
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
export class GitRepositorySettingsModal extends PureComponent<{}, State> {
|
||||
modal: ModalHandle | null = null;
|
||||
_onSubmitEdits?: ((repo: Partial<GitRepository>) => any) | null;
|
||||
|
||||
state: State = {
|
||||
export interface GitRepositorySettingsModalHandle {
|
||||
show: (options: GitRepositorySettingsModalOptions) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
export const GitRepositorySettingsModal = forwardRef<GitRepositorySettingsModalHandle, ModalProps>((_, ref) => {
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
const [state, setState] = useState<GitRepositorySettingsModalOptions>({
|
||||
gitRepository: null,
|
||||
};
|
||||
onSubmitEdits: () => {},
|
||||
});
|
||||
|
||||
_setModalRef(modal: ModalHandle) {
|
||||
this.modal = modal;
|
||||
}
|
||||
useImperativeHandle(ref, () => ({
|
||||
hide: () => {
|
||||
modalRef.current?.hide();
|
||||
},
|
||||
show: options => {
|
||||
setState(options);
|
||||
modalRef.current?.show();
|
||||
},
|
||||
}), []);
|
||||
|
||||
show(options: {
|
||||
gitRepository: GitRepository | null;
|
||||
onSubmitEdits: (repo: Partial<GitRepository>) => any;
|
||||
}) {
|
||||
this._onSubmitEdits = options.onSubmitEdits;
|
||||
const { gitRepository } = options;
|
||||
|
||||
this.setState({ gitRepository });
|
||||
this.modal?.show();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.modal?.hide();
|
||||
}
|
||||
|
||||
render() {
|
||||
const _handleReset = async () => {
|
||||
const { gitRepository } = this.state;
|
||||
|
||||
if (!gitRepository) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteGitRepository(gitRepository);
|
||||
this.hide();
|
||||
};
|
||||
|
||||
const _handleSubmitEdit = async (patch: Partial<GitRepository>) => {
|
||||
if (this._onSubmitEdits) {
|
||||
this._onSubmitEdits({ ...this.state.gitRepository, ...patch });
|
||||
}
|
||||
|
||||
this.hide();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal ref={this._setModalRef} {...this.props}>
|
||||
<ModalForm
|
||||
onSubmit={patch => _handleSubmitEdit(patch)}
|
||||
onReset={_handleReset}
|
||||
gitRepository={this.state.gitRepository}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
const { gitRepository, onSubmitEdits } = state;
|
||||
return (
|
||||
<Modal ref={modalRef}>
|
||||
<ModalForm
|
||||
onSubmit={patch => {
|
||||
onSubmitEdits({ ...gitRepository, ...patch });
|
||||
modalRef.current?.hide();
|
||||
}}
|
||||
onReset={() => {
|
||||
gitRepository && deleteGitRepository(gitRepository);
|
||||
modalRef.current?.hide();
|
||||
}}
|
||||
gitRepository={gitRepository}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
GitRepositorySettingsModal.displayName = 'GitRepositorySettingsModal';
|
||||
|
||||
interface Props {
|
||||
gitRepository: GitRepository | null;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import classnames from 'classnames';
|
||||
import path from 'path';
|
||||
import React, { Fragment, PureComponent } from 'react';
|
||||
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { useMount } from 'react-use';
|
||||
import YAML from 'yaml';
|
||||
|
||||
import { SegmentEvent, trackSegmentEvent, vcsSegmentEventProperties } from '../../../common/analytics';
|
||||
import { AUTOBIND_CFG } from '../../../common/constants';
|
||||
import { database as db } from '../../../common/database';
|
||||
import { strings } from '../../../common/strings';
|
||||
import * as models from '../../../models';
|
||||
@@ -17,7 +16,7 @@ import { GIT_INSOMNIA_DIR, GIT_INSOMNIA_DIR_NAME, GitVCS } from '../../../sync/g
|
||||
import parseGitPath from '../../../sync/git/parse-git-path';
|
||||
import { getOauth2FormatName } from '../../../sync/git/utils';
|
||||
import { IndeterminateCheckbox } from '../base/indeterminate-checkbox';
|
||||
import { type ModalHandle, Modal } from '../base/modal';
|
||||
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
|
||||
import { ModalBody } from '../base/modal-body';
|
||||
import { ModalFooter } from '../base/modal-footer';
|
||||
import { ModalHeader } from '../base/modal-header';
|
||||
@@ -33,132 +32,41 @@ interface Item {
|
||||
editable: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
type Props = ModalProps & {
|
||||
workspace: Workspace;
|
||||
vcs: GitVCS;
|
||||
gitRepository: GitRepository | null;
|
||||
}
|
||||
};
|
||||
|
||||
interface State {
|
||||
export interface GitStagingModalOptions {
|
||||
loading: boolean;
|
||||
branch: string;
|
||||
message: string;
|
||||
items: Record<string, Item>;
|
||||
onCommit: () => Promise<void>;
|
||||
statusNames: Record<string, string>;
|
||||
}
|
||||
export interface GitStagingModalHandle {
|
||||
show: (options: GitStagingModalOptions) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
export const GitStagingModal = forwardRef<GitStagingModalHandle, Props>(({ vcs, workspace, gitRepository }, ref) => {
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
const [state, setState] = useState<GitStagingModalOptions>({
|
||||
loading: false,
|
||||
branch: '',
|
||||
message: '',
|
||||
items: {},
|
||||
onCommit: async () => { },
|
||||
statusNames: {},
|
||||
});
|
||||
|
||||
const INITIAL_STATE: State = {
|
||||
loading: false,
|
||||
branch: '',
|
||||
message: '',
|
||||
items: {},
|
||||
};
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
export class GitStagingModal extends PureComponent<Props, State> {
|
||||
modal: ModalHandle | null = null;
|
||||
statusNames?: Record<string, string>;
|
||||
textarea: HTMLTextAreaElement | null = null;
|
||||
onCommit: null | (() => void);
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = INITIAL_STATE;
|
||||
this.onCommit = null;
|
||||
}
|
||||
|
||||
_setModalRef(ref: ModalHandle) {
|
||||
this.modal = ref;
|
||||
}
|
||||
|
||||
_setTextareaRef(ref: HTMLTextAreaElement) {
|
||||
this.textarea = ref;
|
||||
}
|
||||
|
||||
async _handleMessageChange(event: React.SyntheticEvent<HTMLTextAreaElement>) {
|
||||
this.setState({
|
||||
message: event.currentTarget.value,
|
||||
});
|
||||
}
|
||||
|
||||
async _handleCommit() {
|
||||
const { vcs, gitRepository } = this.props;
|
||||
const { items, message } = this.state;
|
||||
|
||||
// Set the stage
|
||||
for (const p of Object.keys(items)) {
|
||||
const item = items[p];
|
||||
|
||||
if (!item.staged) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.status.includes('deleted')) {
|
||||
await vcs.remove(item.path);
|
||||
} else {
|
||||
await vcs.add(item.path);
|
||||
}
|
||||
}
|
||||
|
||||
await vcs.commit(message);
|
||||
const providerName = getOauth2FormatName(gitRepository?.credentials);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'commit'), providerName });
|
||||
this.modal?.hide();
|
||||
|
||||
if (typeof this.onCommit === 'function') {
|
||||
this.onCommit();
|
||||
}
|
||||
}
|
||||
|
||||
_hideModal() {
|
||||
this.modal?.hide();
|
||||
}
|
||||
|
||||
async _toggleAll(items: Item[], forceAdd = false) {
|
||||
const allStaged = items.every(i => i.staged);
|
||||
const doStage = !allStaged;
|
||||
const newItems = { ...this.state.items };
|
||||
|
||||
for (const { path: p } of items) {
|
||||
if (!newItems[p].editable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
newItems[p].staged = doStage || forceAdd;
|
||||
}
|
||||
|
||||
const providerName = getOauth2FormatName(this.props.gitRepository?.credentials);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', doStage ? 'stage_all' : 'unstage_all'), providerName });
|
||||
this.setState({
|
||||
items: newItems,
|
||||
});
|
||||
}
|
||||
|
||||
async _handleToggleOne(event: React.SyntheticEvent<HTMLInputElement>) {
|
||||
const newItems = { ...this.state.items };
|
||||
const gitPath = event.currentTarget.name;
|
||||
|
||||
if (!newItems[gitPath] || !newItems[gitPath].editable) {
|
||||
return;
|
||||
}
|
||||
|
||||
newItems[gitPath].staged = !newItems[gitPath].staged;
|
||||
|
||||
const providerName = getOauth2FormatName(this.props.gitRepository?.credentials);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', newItems[gitPath].staged ? 'stage' : 'unstage'), providerName });
|
||||
this.setState({
|
||||
items: newItems,
|
||||
});
|
||||
}
|
||||
|
||||
async getAllPaths(): Promise<string[]> {
|
||||
const { vcs } = this.props;
|
||||
const getAllPaths = useCallback(async (): Promise<string[]> => {
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
const f = vcs.getFs().promises;
|
||||
const fsPaths: string[] = [];
|
||||
|
||||
for (const type of await f.readdir(GIT_INSOMNIA_DIR)) {
|
||||
const typeDir = path.join(GIT_INSOMNIA_DIR, type);
|
||||
|
||||
for (const name of await f.readdir(typeDir)) {
|
||||
// NOTE: git paths don't start with '/' so we're omitting
|
||||
// it here too.
|
||||
@@ -166,85 +74,53 @@ export class GitStagingModal extends PureComponent<Props, State> {
|
||||
fsPaths.push(path.join(gitPath));
|
||||
}
|
||||
}
|
||||
|
||||
// To get all possible paths, we need to combine the paths already in Git
|
||||
// with the paths on the FS. This is required to cover the case where a
|
||||
// file can be deleted from FS or from Git.
|
||||
const gitPaths = await vcs.listFiles();
|
||||
const uniquePaths = new Set([...fsPaths, ...gitPaths]);
|
||||
return Array.from(uniquePaths).sort();
|
||||
}
|
||||
}, [vcs]);
|
||||
|
||||
async show(options: { onCommit?: () => void }) {
|
||||
this.onCommit = options.onCommit || null;
|
||||
this.modal?.show();
|
||||
// Reset state
|
||||
this.setState(INITIAL_STATE);
|
||||
await this._refresh(() => {
|
||||
this.textarea?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
async _refresh(callback?: () => void) {
|
||||
const { vcs, workspace } = this.props;
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
const refresh = useCallback(async () => {
|
||||
setState(state => ({ ...state, loading: true }));
|
||||
// Get and set branch name
|
||||
const branch = await vcs.getBranch();
|
||||
this.setState({
|
||||
branch,
|
||||
});
|
||||
setState(state => ({ ...state, branch }));
|
||||
// Cache status names
|
||||
const docs = await db.withDescendants(workspace);
|
||||
const allPaths = await this.getAllPaths();
|
||||
this.statusNames = {};
|
||||
|
||||
const allPaths = await getAllPaths();
|
||||
const statusNames: Record<string, string> = {};
|
||||
for (const doc of docs) {
|
||||
const name = (isApiSpec(doc) && doc.fileName) || doc.name || '';
|
||||
this.statusNames[path.join(GIT_INSOMNIA_DIR_NAME, doc.type, `${doc._id}.json`)] = name;
|
||||
this.statusNames[path.join(GIT_INSOMNIA_DIR_NAME, doc.type, `${doc._id}.yml`)] = name;
|
||||
statusNames[path.join(GIT_INSOMNIA_DIR_NAME, doc.type, `${doc._id}.json`)] = name;
|
||||
statusNames[path.join(GIT_INSOMNIA_DIR_NAME, doc.type, `${doc._id}.yml`)] = name;
|
||||
}
|
||||
|
||||
// Create status items
|
||||
const items: Record<string, Item> = {};
|
||||
const log = (await vcs.log(1)) || [];
|
||||
|
||||
for (const gitPath of allPaths) {
|
||||
const status = await vcs.status(gitPath);
|
||||
|
||||
if (status === 'unmodified') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this.statusNames[gitPath] && log.length > 0) {
|
||||
if (!statusNames[gitPath] && log.length > 0) {
|
||||
const docYML = await vcs.readObjFromTree(log[0].commit.tree, gitPath);
|
||||
|
||||
if (!docYML) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
const doc = YAML.parse(docYML);
|
||||
this.statusNames[gitPath] = doc.name || '';
|
||||
} catch (err) {
|
||||
// Nothing here
|
||||
if (docYML) {
|
||||
try {
|
||||
statusNames[gitPath] = YAML.parse(docYML.toString()).name || '';
|
||||
} catch (err) {}
|
||||
}
|
||||
}
|
||||
|
||||
// We know that type is in the path; extract it. If the model is not found, set to Unknown.
|
||||
let { type } = parseGitPath(gitPath);
|
||||
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
if (!models.types().includes(type)) {
|
||||
type = 'Unknown';
|
||||
}
|
||||
|
||||
const added = status.includes('added');
|
||||
let staged = !added;
|
||||
let editable = true;
|
||||
|
||||
// We want to enforce that workspace changes are always committed because otherwise
|
||||
// others won't be able to clone from it. We also make fundamental migrations to the
|
||||
// scope property which need to be committed.
|
||||
@@ -253,7 +129,6 @@ export class GitStagingModal extends PureComponent<Props, State> {
|
||||
editable = false;
|
||||
staged = true;
|
||||
}
|
||||
|
||||
items[gitPath] = {
|
||||
type: type as any,
|
||||
staged,
|
||||
@@ -263,50 +138,62 @@ export class GitStagingModal extends PureComponent<Props, State> {
|
||||
path: gitPath,
|
||||
};
|
||||
}
|
||||
setState(state => ({
|
||||
...state,
|
||||
items,
|
||||
loading: false,
|
||||
statusNames,
|
||||
}));
|
||||
}, [getAllPaths, vcs, workspace]);
|
||||
|
||||
this.setState(
|
||||
{
|
||||
items,
|
||||
useImperativeHandle(ref, () => ({
|
||||
hide: () => {
|
||||
modalRef.current?.hide();
|
||||
},
|
||||
show: async ({ onCommit }) => {
|
||||
modalRef.current?.show();
|
||||
// Reset state
|
||||
setState({
|
||||
loading: false,
|
||||
},
|
||||
callback,
|
||||
);
|
||||
}
|
||||
|
||||
renderOperation(item: Item) {
|
||||
let child: JSX.Element | null = null;
|
||||
let message = '';
|
||||
let type = item.type;
|
||||
|
||||
if (item.status.includes('added')) {
|
||||
child = <i className="fa fa-plus-circle success" />;
|
||||
message = 'Added';
|
||||
} else if (item.status.includes('modified')) {
|
||||
child = <i className="fa fa-circle faded" />;
|
||||
message = 'Modified';
|
||||
} else if (item.status.includes('deleted')) {
|
||||
child = <i className="fa fa-minus-circle danger" />;
|
||||
message = 'Deleted';
|
||||
} else {
|
||||
child = <i className="fa fa-question-circle info" />;
|
||||
message = 'Unknown';
|
||||
branch: '',
|
||||
message: '',
|
||||
items: {},
|
||||
statusNames: {},
|
||||
onCommit,
|
||||
});
|
||||
refresh();
|
||||
},
|
||||
}), [refresh]);
|
||||
useMount(() => {
|
||||
refresh();
|
||||
});
|
||||
const toggleAll = async (items: Item[], forceAdd = false) => {
|
||||
const allStaged = items.every(i => i.staged);
|
||||
const doStage = !allStaged;
|
||||
const newItems = { ...state.items };
|
||||
for (const { path: p } of items) {
|
||||
if (newItems[p].editable) {
|
||||
newItems[p].staged = doStage || forceAdd;
|
||||
}
|
||||
}
|
||||
const providerName = getOauth2FormatName(gitRepository?.credentials);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', doStage ? 'stage_all' : 'unstage_all'), providerName });
|
||||
setState(state => ({ ...state, items: newItems }));
|
||||
};
|
||||
|
||||
if (type === models.workspace.type) {
|
||||
type = strings.document.singular;
|
||||
const handleToggleOne = async (event: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
const newItems = { ...state.items };
|
||||
const gitPath = event.currentTarget.name;
|
||||
if (!newItems[gitPath] || !newItems[gitPath].editable) {
|
||||
return;
|
||||
}
|
||||
newItems[gitPath].staged = !newItems[gitPath].staged;
|
||||
const providerName = getOauth2FormatName(gitRepository?.credentials);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', newItems[gitPath].staged ? 'stage' : 'unstage'), providerName });
|
||||
setState(state => ({ ...state, items: newItems }));
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Tooltip message={message}>
|
||||
{child} {type}
|
||||
</Tooltip>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
async _handleRollback(items: Item[]) {
|
||||
const { vcs } = this.props;
|
||||
const handleRollback = async (items: Item[]) => {
|
||||
const files = items
|
||||
.filter(i => i.editable) // only rollback if editable
|
||||
.map(i => ({
|
||||
@@ -314,158 +201,208 @@ export class GitStagingModal extends PureComponent<Props, State> {
|
||||
status: i.status,
|
||||
}));
|
||||
await gitRollback(vcs, files);
|
||||
await this._refresh();
|
||||
}
|
||||
refresh();
|
||||
};
|
||||
|
||||
async _handleRollbackSingle(item: Item) {
|
||||
await this._handleRollback([item]);
|
||||
|
||||
const providerName = getOauth2FormatName(this.props.gitRepository?.credentials);
|
||||
const handleRollbackSingle = (item: Item) => {
|
||||
handleRollback([item]);
|
||||
const providerName = getOauth2FormatName(gitRepository?.credentials);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'rollback'), providerName });
|
||||
}
|
||||
};
|
||||
|
||||
async _handleRollbackAll(items: Item[]) {
|
||||
await this._handleRollback(items);
|
||||
|
||||
const providerName = getOauth2FormatName(this.props.gitRepository?.credentials);
|
||||
const handleRollbackAll = (items: Item[]) => {
|
||||
handleRollback(items);
|
||||
const providerName = getOauth2FormatName(gitRepository?.credentials);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'rollback_all'), providerName });
|
||||
}
|
||||
|
||||
renderItem(item: Item) {
|
||||
const { path: gitPath, staged, editable } = item;
|
||||
// TODO: unsound non-null assertion
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const docName = this.statusNames![gitPath] || 'n/a';
|
||||
return (
|
||||
<tr key={gitPath} className="table--no-outline-row">
|
||||
<td>
|
||||
<label className="no-pad wide">
|
||||
<input
|
||||
disabled={!editable}
|
||||
className="space-right"
|
||||
type="checkbox"
|
||||
checked={staged}
|
||||
name={gitPath}
|
||||
onChange={this._handleToggleOne}
|
||||
/>{' '}
|
||||
{docName}
|
||||
</label>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
{item.editable && <Tooltip message={item.added ? 'Delete' : 'Rollback'}>
|
||||
<button
|
||||
className="btn btn--micro space-right"
|
||||
onClick={() => this._handleRollbackSingle(item)}
|
||||
>
|
||||
<i className={classnames('fa', item.added ? 'fa-trash' : 'fa-undo')} />
|
||||
</button>
|
||||
</Tooltip>}
|
||||
{this.renderOperation(item)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
renderTable(title: string, items: Item[], rollbackLabel: string) {
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const allStaged = items.every(i => i.staged);
|
||||
const allUnstaged = items.every(i => !i.staged);
|
||||
return (
|
||||
<div className="pad-top">
|
||||
<strong>{title}</strong>
|
||||
<PromptButton
|
||||
className="btn pull-right btn--micro"
|
||||
onClick={() => this._handleRollbackAll(items)}
|
||||
>
|
||||
{rollbackLabel}
|
||||
</PromptButton>
|
||||
<table className="table--fancy table--outlined margin-top-sm">
|
||||
<thead>
|
||||
<tr className="table--no-outline-row">
|
||||
<th>
|
||||
<label className="wide no-pad">
|
||||
<span className="txt-md">
|
||||
<IndeterminateCheckbox
|
||||
className="space-right"
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
type="checkbox"
|
||||
checked={allStaged}
|
||||
onChange={() => this._toggleAll(items, !allStaged)}
|
||||
indeterminate={!allStaged && !allUnstaged}
|
||||
/>
|
||||
</span>{' '}
|
||||
name
|
||||
</label>
|
||||
</th>
|
||||
<th className="text-right">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{items.map(this.renderItem)}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderEmpty() {
|
||||
const { loading } = this.state;
|
||||
|
||||
if (loading) {
|
||||
return <>Loading...</>;
|
||||
}
|
||||
|
||||
return <>No changes to commit.</>;
|
||||
}
|
||||
|
||||
_renderItems(items: Item[]) {
|
||||
const { message } = this.state;
|
||||
const newItems = items.filter(i => i.status.includes('added'));
|
||||
const existingItems = items.filter(i => !i.status.includes('added'));
|
||||
return (
|
||||
<>
|
||||
<div className="form-control form-control--outlined">
|
||||
<textarea
|
||||
ref={this._setTextareaRef}
|
||||
rows={3}
|
||||
required
|
||||
placeholder="A descriptive message to describe changes made"
|
||||
defaultValue={message}
|
||||
onChange={this._handleMessageChange}
|
||||
};
|
||||
const { items, branch, loading, statusNames, message, onCommit } = state;
|
||||
const hasChanges = !!Object.values(items).length;
|
||||
return (
|
||||
<Modal ref={modalRef}>
|
||||
<ModalHeader>Commit Changes</ModalHeader>
|
||||
<ModalBody className="wide pad">
|
||||
{hasChanges ? <>
|
||||
<div className="form-control form-control--outlined">
|
||||
<textarea
|
||||
rows={3}
|
||||
required
|
||||
placeholder="A descriptive message to describe changes made"
|
||||
defaultValue={state.message}
|
||||
onChange={event => setState({ ...state, message: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<ChangesTable
|
||||
title="Modified Objects"
|
||||
rollbackLabel='Rollback all'
|
||||
items={Object.values(items).filter(i => !i.status.includes('added'))}
|
||||
statusNames={statusNames}
|
||||
rollbackAll={handleRollbackAll}
|
||||
rollbackOne={handleRollbackSingle}
|
||||
toggleAll={toggleAll}
|
||||
toggleOne={handleToggleOne}
|
||||
/>
|
||||
<ChangesTable
|
||||
title="Unversioned Objects"
|
||||
rollbackLabel='Delete all'
|
||||
items={Object.values(items).filter(i => i.status.includes('added'))}
|
||||
statusNames={statusNames}
|
||||
rollbackAll={handleRollbackAll}
|
||||
rollbackOne={handleRollbackSingle}
|
||||
toggleAll={toggleAll}
|
||||
toggleOne={handleToggleOne}
|
||||
/>
|
||||
</>
|
||||
: state.loading ? <>Loading...</> : <>No changes to commit.</>}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div className="margin-left italic txt-sm">
|
||||
<i className="fa fa-code-fork" /> {branch}{' '}
|
||||
{loading && <i className="fa fa-refresh fa-spin" />}
|
||||
</div>
|
||||
{this.renderTable('Modified Objects', existingItems, 'Rollback all')}
|
||||
{this.renderTable('Unversioned Objects', newItems, 'Delete all')}
|
||||
</>
|
||||
);
|
||||
}
|
||||
<div>
|
||||
<button className="btn" onClick={() => modalRef.current?.hide()}>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={async () => {
|
||||
for (const item of Object.values(items)) {
|
||||
if (item.staged) {
|
||||
item.status.includes('deleted') ? await vcs.remove(item.path) : await vcs.add(item.path);
|
||||
}
|
||||
}
|
||||
await vcs.commit(message);
|
||||
const providerName = getOauth2FormatName(gitRepository?.credentials);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'commit'), providerName });
|
||||
modalRef.current?.hide();
|
||||
if (typeof onCommit === 'function') {
|
||||
onCommit();
|
||||
}
|
||||
}}
|
||||
disabled={loading || !hasChanges}
|
||||
>
|
||||
Commit
|
||||
</button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
GitStagingModal.displayName = 'GitStagingModal';
|
||||
|
||||
render() {
|
||||
const { items, branch, loading } = this.state;
|
||||
const itemsList = Object.keys(items).map(k => items[k]);
|
||||
const hasChanges = !!itemsList.length;
|
||||
const OperationTooltip = ({ item }: { item: Item }) => {
|
||||
const type = item.type === models.workspace.type ? strings.document.singular : item.type;
|
||||
if (item.status.includes('added')) {
|
||||
return (
|
||||
<Modal ref={this._setModalRef}>
|
||||
<ModalHeader>Commit Changes</ModalHeader>
|
||||
<ModalBody className="wide pad">
|
||||
{hasChanges ? this._renderItems(itemsList) : this._renderEmpty()}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div className="margin-left italic txt-sm">
|
||||
<i className="fa fa-code-fork" /> {branch}{' '}
|
||||
{loading && <i className="fa fa-refresh fa-spin" />}
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn" onClick={this._hideModal}>
|
||||
Close
|
||||
</button>
|
||||
<button className="btn" onClick={this._handleCommit} disabled={loading || !hasChanges}>
|
||||
Commit
|
||||
</button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
<Tooltip message="Added">
|
||||
<i className="fa fa-plus-circle success" /> {type}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
if (item.status.includes('modified')) {
|
||||
return (
|
||||
<Tooltip message="Modified">
|
||||
<i className="fa fa-plus-circle faded" /> {type}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
if (item.status.includes('deleted')) {
|
||||
return (
|
||||
<Tooltip message="Deleted">
|
||||
<i className="fa fa-minus-circle danger" /> {type}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tooltip message="Unknown">
|
||||
<i className="fa fa-question-circle info" /> {type}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
interface ChangeTableProps {
|
||||
items: Item[];
|
||||
title: string;
|
||||
rollbackLabel: string;
|
||||
statusNames: Record<string, string>;
|
||||
rollbackAll: (items: Item[]) => void;
|
||||
rollbackOne: (item: Item) => void;
|
||||
toggleAll: (items: Item[], forceAdd: boolean) => void;
|
||||
toggleOne: (event: React.SyntheticEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
const ChangesTable = ({
|
||||
items,
|
||||
title,
|
||||
rollbackLabel,
|
||||
statusNames,
|
||||
rollbackAll,
|
||||
rollbackOne,
|
||||
toggleAll,
|
||||
toggleOne,
|
||||
}: ChangeTableProps) => {
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const allStaged = items.every(i => i.staged);
|
||||
const allUnstaged = items.every(i => !i.staged);
|
||||
return (
|
||||
<div className="pad-top">
|
||||
<strong>{title}</strong>
|
||||
<PromptButton
|
||||
className="btn pull-right btn--micro"
|
||||
onClick={() => rollbackAll(items)}
|
||||
>
|
||||
{rollbackLabel}
|
||||
</PromptButton>
|
||||
<table className="table--fancy table--outlined margin-top-sm">
|
||||
<thead>
|
||||
<tr className="table--no-outline-row">
|
||||
<th>
|
||||
<label className="wide no-pad">
|
||||
<span className="txt-md">
|
||||
<IndeterminateCheckbox
|
||||
className="space-right"
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
type="checkbox"
|
||||
checked={allStaged}
|
||||
onChange={() => toggleAll(items, !allStaged)}
|
||||
indeterminate={!allStaged && !allUnstaged}
|
||||
/>
|
||||
</span>{' '}
|
||||
name
|
||||
</label>
|
||||
</th>
|
||||
<th className="text-right">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{items.map(item => (
|
||||
<tr key={item.path} className="table--no-outline-row">
|
||||
<td>
|
||||
<label className="no-pad wide">
|
||||
<input
|
||||
disabled={!item.editable}
|
||||
className="space-right"
|
||||
type="checkbox"
|
||||
checked={item.staged}
|
||||
name={item.path}
|
||||
onChange={toggleOne}
|
||||
/>{' '}
|
||||
{statusNames?.[item.path] || 'n/a'}
|
||||
</label>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
{item.editable && <Tooltip message={item.added ? 'Delete' : 'Rollback'}>
|
||||
<button
|
||||
className="btn btn--micro space-right"
|
||||
onClick={() => rollbackOne(item)}
|
||||
>
|
||||
<i className={classnames('fa', item.added ? 'fa-trash' : 'fa-undo')} />
|
||||
</button>
|
||||
</Tooltip>}
|
||||
<OperationTooltip item={item} />
|
||||
</td>
|
||||
</tr>
|
||||
))}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -13,8 +13,7 @@ import { setActiveActivity } from '../redux/modules/global';
|
||||
import { selectActiveActivity, selectActiveApiSpec, selectActiveGitRepository, selectActiveProject, selectActiveProjectName, selectActiveWorkspace, selectIsLoggedIn } from '../redux/selectors';
|
||||
import { ActivityToggle } from './activity-toggle/activity-toggle';
|
||||
import { AppHeader } from './app-header';
|
||||
import { DropdownButton } from './base/dropdown/dropdown-button';
|
||||
import GitSyncDropdown from './dropdowns/git-sync-dropdown';
|
||||
import { GitSyncDropdown } from './dropdowns/git-sync-dropdown';
|
||||
import { SyncDropdown } from './dropdowns/sync-dropdown';
|
||||
import { WorkspaceDropdown } from './dropdowns/workspace-dropdown';
|
||||
import { showModal } from './modals';
|
||||
@@ -128,13 +127,7 @@ export const WorkspacePageHeader: FC = () => {
|
||||
/> : isGitSync ? <GitSyncDropdown
|
||||
className="margin-left"
|
||||
workspace={activeWorkspace}
|
||||
gitRepository={activeGitRepository}
|
||||
vcs={gitVCS}
|
||||
renderDropdownButton={children => (
|
||||
<DropdownButton className="btn--clicky-small btn-sync">
|
||||
{children}
|
||||
</DropdownButton>
|
||||
)}
|
||||
/> : null}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -208,17 +208,16 @@ const App = () => {
|
||||
|
||||
<EnvironmentEditModal ref={instance => registerModal(instance, 'EnvironmentEditModal')} />
|
||||
|
||||
<GitRepositorySettingsModal ref={registerModal} />
|
||||
<GitRepositorySettingsModal ref={instance => registerModal(instance, 'GitRepositorySettingsModal')} />
|
||||
|
||||
{activeWorkspace && gitVCS ? (
|
||||
<Fragment>
|
||||
<GitStagingModal ref={registerModal} workspace={activeWorkspace} vcs={gitVCS} gitRepository={activeGitRepository} />
|
||||
<GitLogModal ref={registerModal} vcs={gitVCS} />
|
||||
<GitStagingModal ref={instance => registerModal(instance, 'GitStagingModal')} workspace={activeWorkspace} vcs={gitVCS} gitRepository={activeGitRepository} />
|
||||
<GitLogModal ref={instance => registerModal(instance, 'GitLogModal')} vcs={gitVCS} />
|
||||
{activeGitRepository !== null && (
|
||||
<GitBranchesModal
|
||||
ref={registerModal}
|
||||
ref={instance => registerModal(instance, 'GitBranchesModal')}
|
||||
vcs={gitVCS}
|
||||
gitRepository={activeGitRepository}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
|
||||
Reference in new issue
Block a user