{this.props.children}
diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js
index 9899daca097..4ec74fde668 100644
--- a/lib/views/git-tab-view.js
+++ b/lib/views/git-tab-view.js
@@ -70,6 +70,7 @@ export default class GitTabView extends React.Component {
this.subscriptions = new CompositeDisposable();
this.refCommitController = new RefHolder();
+ this.refRecentCommitsController = new RefHolder();
}
componentDidMount() {
@@ -101,7 +102,7 @@ export default class GitTabView extends React.Component {
);
} else if (this.props.repository.hasDirectory() &&
- !isValidWorkdir(this.props.repository.getWorkingDirectoryPath())) {
+ !isValidWorkdir(this.props.repository.getWorkingDirectoryPath())) {
return (
@@ -193,9 +194,13 @@ export default class GitTabView extends React.Component {
updateSelectedCoAuthors={this.props.updateSelectedCoAuthors}
/>
);
@@ -219,27 +224,22 @@ export default class GitTabView extends React.Component {
this.props.initializeRepo(initPath);
}
- rememberFocus(event) {
- let currentFocus = null;
-
- currentFocus = this.props.refStagingView.map(view => view.rememberFocus(event)).getOr(null);
-
- if (!currentFocus) {
- currentFocus = this.refCommitController.map(controller => controller.rememberFocus(event)).getOr(null);
+ getFocus(element) {
+ for (const ref of [this.props.refStagingView, this.refCommitController, this.refRecentCommitsController]) {
+ const focus = ref.map(sub => sub.getFocus(element)).getOr(null);
+ if (focus !== null) {
+ return focus;
+ }
}
-
- return currentFocus;
+ return null;
}
setFocus(focus) {
- if (this.props.refStagingView.map(view => view.setFocus(focus)).getOr(false)) {
- return true;
- }
-
- if (this.refCommitController.map(controller => controller.setFocus(focus)).getOr(false)) {
- return true;
+ for (const ref of [this.props.refStagingView, this.refCommitController, this.refRecentCommitsController]) {
+ if (ref.map(sub => sub.setFocus(focus)).getOr(false)) {
+ return true;
+ }
}
-
return false;
}
@@ -248,39 +248,34 @@ export default class GitTabView extends React.Component {
}
async advanceFocus(evt) {
- // Advance focus within the CommitView if it's there
- if (this.refCommitController.map(c => c.advanceFocus(evt)).getOr(false)) {
- return;
- }
+ const currentFocus = this.getFocus(document.activeElement);
+ let nextSeen = false;
- // Advance focus to the next staging view list, if it's there
- if (await this.props.refStagingView.map(view => view.activateNextList()).getOr(false)) {
- evt.stopPropagation();
- return;
- }
-
- // Advance focus from the staging view lists to the CommitView
- if (this.refCommitController.map(c => c.setFocus(GitTabView.focus.COMMIT_PREVIEW_BUTTON)).getOr(false)) {
- evt.stopPropagation();
+ for (const subHolder of [this.props.refStagingView, this.refCommitController, this.refRecentCommitsController]) {
+ const next = await subHolder.map(sub => sub.advanceFocusFrom(currentFocus)).getOr(null);
+ if (next !== null && !nextSeen) {
+ nextSeen = true;
+ evt.stopPropagation();
+ if (next !== currentFocus) {
+ this.setFocus(next);
+ }
+ }
}
}
async retreatFocus(evt) {
- // Retreat focus within the CommitView if it's there
- if (this.refCommitController.map(c => c.retreatFocus(evt)).getOr(false)) {
- return;
- }
+ const currentFocus = this.getFocus(document.activeElement);
+ let previousSeen = false;
- if (this.refCommitController.map(c => c.hasFocusAtBeginning()).getOr(false)) {
- // Retreat focus from the beginning of the CommitView to the end of the StagingView
- if (await this.props.refStagingView.map(view => view.activateLastList()).getOr(null)) {
- this.setFocus(GitTabView.focus.STAGING);
+ for (const subHolder of [this.refRecentCommitsController, this.refCommitController, this.props.refStagingView]) {
+ const previous = await subHolder.map(sub => sub.retreatFocusFrom(currentFocus)).getOr(null);
+ if (previous !== null && !previousSeen) {
+ previousSeen = true;
evt.stopPropagation();
+ if (previous !== currentFocus) {
+ this.setFocus(previous);
+ }
}
- } else if (await this.props.refStagingView.map(c => c.activatePreviousList()).getOr(null)) {
- // Retreat focus within the StagingView
- this.setFocus(GitTabView.focus.STAGING);
- evt.stopPropagation();
}
}
@@ -289,6 +284,10 @@ export default class GitTabView extends React.Component {
this.setFocus(GitTabView.focus.STAGING);
}
+ focusAndSelectRecentCommit() {
+ this.setFocus(RecentCommitsController.focus.RECENT_COMMIT);
+ }
+
focusAndSelectCommitPreviewButton() {
this.setFocus(GitTabView.focus.COMMIT_PREVIEW_BUTTON);
}
diff --git a/lib/views/hunk-header-view.js b/lib/views/hunk-header-view.js
index cb72feb36ac..a2ed357015f 100644
--- a/lib/views/hunk-header-view.js
+++ b/lib/views/hunk-header-view.js
@@ -7,6 +7,9 @@ import {RefHolderPropType} from '../prop-types';
import RefHolder from '../models/ref-holder';
import Tooltip from '../atom/tooltip';
import Keystroke from '../atom/keystroke';
+import ChangedFileItem from '../items/changed-file-item';
+import CommitPreviewItem from '../items/commit-preview-item';
+import CommitDetailItem from '../items/commit-detail-item';
function theBuckStopsHere(event) {
event.stopPropagation();
@@ -17,22 +20,23 @@ export default class HunkHeaderView extends React.Component {
refTarget: RefHolderPropType.isRequired,
hunk: PropTypes.object.isRequired,
isSelected: PropTypes.bool.isRequired,
- stagingStatus: PropTypes.oneOf(['unstaged', 'staged']).isRequired,
+ stagingStatus: PropTypes.oneOf(['unstaged', 'staged']),
selectionMode: PropTypes.oneOf(['hunk', 'line']).isRequired,
- toggleSelectionLabel: PropTypes.string.isRequired,
- discardSelectionLabel: PropTypes.string.isRequired,
+ toggleSelectionLabel: PropTypes.string,
+ discardSelectionLabel: PropTypes.string,
tooltips: PropTypes.object.isRequired,
keymaps: PropTypes.object.isRequired,
- toggleSelection: PropTypes.func.isRequired,
- discardSelection: PropTypes.func.isRequired,
+ toggleSelection: PropTypes.func,
+ discardSelection: PropTypes.func,
mouseDown: PropTypes.func.isRequired,
+ itemType: PropTypes.oneOf([ChangedFileItem, CommitPreviewItem, CommitDetailItem]).isRequired,
};
constructor(props) {
super(props);
- autobind(this, 'didMouseDown');
+ autobind(this, 'didMouseDown', 'renderButtons');
this.refDiscardButton = new RefHolder();
}
@@ -48,32 +52,44 @@ export default class HunkHeaderView extends React.Component {
{this.props.hunk.getHeader().trim()} {this.props.hunk.getSectionHeading().trim()}
-
- {this.props.stagingStatus === 'unstaged' && (
-
-
-
-
- )}
+ {this.renderButtons()}
);
}
+ renderButtons() {
+ if (this.props.itemType === CommitDetailItem) {
+ return null;
+ } else {
+ return (
+
+
+ {this.props.stagingStatus === 'unstaged' && (
+
+
+
+
+ )}
+
+ );
+ }
+ }
+
didMouseDown(event) {
return this.props.mouseDown(event, this.props.hunk);
}
diff --git a/lib/views/issueish-detail-view.js b/lib/views/issueish-detail-view.js
index 55d5a148e75..2b631638498 100644
--- a/lib/views/issueish-detail-view.js
+++ b/lib/views/issueish-detail-view.js
@@ -81,6 +81,7 @@ export class BareIssueishDetailView extends React.Component {
}),
).isRequired,
}).isRequired,
+ openCommit: PropTypes.func.isRequired,
}
state = {
@@ -143,6 +144,10 @@ export class BareIssueishDetailView extends React.Component {
}
renderPullRequestBody(issueish, childProps) {
+ const {checkoutOp} = this.props;
+ const reason = checkoutOp.why();
+ const onBranch = reason && !reason({hidden: true, default: false}); // is there a more direct way than this?
+
return (
@@ -184,7 +189,7 @@ export class BareIssueishDetailView extends React.Component {
{/* commits */}
-
+
);
diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js
index 0ff63f97b39..5d1407e4963 100644
--- a/lib/views/multi-file-patch-view.js
+++ b/lib/views/multi-file-patch-view.js
@@ -18,6 +18,7 @@ import HunkHeaderView from './hunk-header-view';
import RefHolder from '../models/ref-holder';
import ChangedFileItem from '../items/changed-file-item';
import CommitPreviewItem from '../items/commit-preview-item';
+import CommitDetailItem from '../items/commit-detail-item';
import File from '../models/patch/file';
const executableText = {
@@ -31,7 +32,7 @@ const BLANK_LABEL = () => NBSP_CHARACTER;
export default class MultiFilePatchView extends React.Component {
static propTypes = {
- stagingStatus: PropTypes.oneOf(['staged', 'unstaged']).isRequired,
+ stagingStatus: PropTypes.oneOf(['staged', 'unstaged']),
isPartiallyStaged: PropTypes.bool,
multiFilePatch: MultiFilePatchPropType.isRequired,
selectionMode: PropTypes.oneOf(['hunk', 'line']).isRequired,
@@ -46,20 +47,19 @@ export default class MultiFilePatchView extends React.Component {
tooltips: PropTypes.object.isRequired,
config: PropTypes.object.isRequired,
- selectedRowsChanged: PropTypes.func.isRequired,
-
- diveIntoMirrorPatch: PropTypes.func.isRequired,
- surface: PropTypes.func.isRequired,
- openFile: PropTypes.func.isRequired,
- toggleFile: PropTypes.func.isRequired,
- toggleRows: PropTypes.func.isRequired,
- toggleModeChange: PropTypes.func.isRequired,
- toggleSymlinkChange: PropTypes.func.isRequired,
- undoLastDiscard: PropTypes.func.isRequired,
- discardRows: PropTypes.func.isRequired,
-
+ selectedRowsChanged: PropTypes.func,
+
+ diveIntoMirrorPatch: PropTypes.func,
+ surface: PropTypes.func,
+ openFile: PropTypes.func,
+ toggleFile: PropTypes.func,
+ toggleRows: PropTypes.func,
+ toggleModeChange: PropTypes.func,
+ toggleSymlinkChange: PropTypes.func,
+ undoLastDiscard: PropTypes.func,
+ discardRows: PropTypes.func,
refInitialFocus: RefHolderPropType,
- itemType: PropTypes.oneOf([ChangedFileItem, CommitPreviewItem]).isRequired,
+ itemType: PropTypes.oneOf([ChangedFileItem, CommitPreviewItem, CommitDetailItem]).isRequired,
}
constructor(props) {
@@ -186,6 +186,15 @@ export default class MultiFilePatchView extends React.Component {
}
renderCommands() {
+ if (this.props.itemType === CommitDetailItem) {
+ return (
+
+
+
+
+ );
+ }
+
let stageModeCommand = null;
let stageSymlinkCommand = null;
@@ -205,11 +214,11 @@ export default class MultiFilePatchView extends React.Component {
return (
+
+
-
-
@@ -352,6 +361,7 @@ export default class MultiFilePatchView extends React.Component {
title="Mode change"
actionIcon={attrs.actionIcon}
actionText={attrs.actionText}
+ itemType={this.props.itemType}
action={() => this.props.toggleModeChange(filePatch)}>
File changed mode
@@ -435,6 +445,7 @@ export default class MultiFilePatchView extends React.Component {
title={title}
actionIcon={attrs.actionIcon}
actionText={attrs.actionText}
+ itemType={this.props.itemType}
action={() => this.props.toggleSymlinkChange(filePatch)}>
{detail}
@@ -493,6 +504,7 @@ export default class MultiFilePatchView extends React.Component {
toggleSelection={() => this.toggleHunkSelection(hunk, containsSelection)}
discardSelection={() => this.discardHunkSelection(hunk, containsSelection)}
mouseDown={this.didMouseDownOnHeader}
+ itemType={this.props.itemType}
/>
diff --git a/lib/views/open-commit-dialog.js b/lib/views/open-commit-dialog.js
new file mode 100644
index 00000000000..47ab2d3ec21
--- /dev/null
+++ b/lib/views/open-commit-dialog.js
@@ -0,0 +1,138 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {CompositeDisposable} from 'event-kit';
+
+import Commands, {Command} from '../atom/commands';
+import {autobind} from '../helpers';
+
+// const COMMIT_SHA_REGEX = /^(?:https?:\/\/)?github.com\/([^/]+)\/([^/]+)\/(?:issues|pull)\/(\d+)/;
+
+export default class OpenCommitDialog extends React.Component {
+ static propTypes = {
+ commandRegistry: PropTypes.object.isRequired,
+ didAccept: PropTypes.func,
+ didCancel: PropTypes.func,
+ }
+
+ static defaultProps = {
+ didAccept: () => {},
+ didCancel: () => {},
+ }
+
+ constructor(props, context) {
+ super(props, context);
+ autobind(this, 'accept', 'cancel', 'editorRefs', 'didChangeCommitSha');
+
+ this.state = {
+ cloneDisabled: false,
+ };
+
+ this.subs = new CompositeDisposable();
+ }
+
+ componentDidMount() {
+ if (this.commitShaElement) {
+ setTimeout(() => this.commitShaElement.focus());
+ }
+ }
+
+ render() {
+ return this.renderDialog();
+ }
+
+ renderDialog() {
+ return (
+
+
+
+
+
+
+
+ {this.state.error && {this.state.error}}
+
+
+
+
+
+
+ );
+ }
+
+ accept() {
+ if (this.getCommitSha().length === 0) {
+ return;
+ }
+
+ const parsed = this.parseSha();
+ if (!parsed) {
+ this.setState({
+ error: 'That is not a valid commit sha.',
+ });
+ return;
+ }
+ const {sha} = parsed;
+
+ this.props.didAccept({sha});
+ }
+
+ cancel() {
+ this.props.didCancel();
+ }
+
+ editorRefs(baseName) {
+ const elementName = `${baseName}Element`;
+ const modelName = `${baseName}Editor`;
+ const subName = `${baseName}Subs`;
+ const changeMethodName = `didChange${baseName[0].toUpperCase()}${baseName.substring(1)}`;
+
+ return element => {
+ if (!element) {
+ return;
+ }
+
+ this[elementName] = element;
+ const editor = element.getModel();
+ if (this[modelName] !== editor) {
+ this[modelName] = editor;
+
+ if (this[subName]) {
+ this[subName].dispose();
+ this.subs.remove(this[subName]);
+ }
+
+ this[subName] = editor.onDidChange(this[changeMethodName]);
+ this.subs.add(this[subName]);
+ }
+ };
+ }
+
+ didChangeCommitSha() {
+ this.setState({error: null});
+ }
+
+ parseSha() {
+ const sha = this.getCommitSha();
+ // const matches = url.match(ISSUEISH_URL_REGEX);
+ // if (!matches) {
+ // return false;
+ // }
+ // const [_full, repoOwner, repoName, issueishNumber] = matches; // eslint-disable-line no-unused-vars
+ return {sha};
+ }
+
+ getCommitSha() {
+ return this.commitShaEditor ? this.commitShaEditor.getText() : '';
+ }
+}
diff --git a/lib/views/pr-commit-view.js b/lib/views/pr-commit-view.js
index 88d879a3cbf..e1fa7302799 100644
--- a/lib/views/pr-commit-view.js
+++ b/lib/views/pr-commit-view.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import {emojify} from 'node-emoji';
import moment from 'moment';
import {graphql, createFragmentContainer} from 'react-relay';
+import cx from 'classnames';
import {autobind} from '../helpers';
@@ -18,9 +19,12 @@ export class PrCommitView extends React.Component {
}).isRequired,
messageBody: PropTypes.string,
messageHeadline: PropTypes.string.isRequired,
- abbreviatedOid: PropTypes.string.isRequired,
+ shortSha: PropTypes.string.isRequired,
+ sha: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
}).isRequired,
+ onBranch: PropTypes.bool.isRequired,
+ openCommit: PropTypes.func.isRequired,
}
constructor(props) {
@@ -37,14 +41,21 @@ export class PrCommitView extends React.Component {
return moment(date).fromNow();
}
+ openCommitDetailItem = () => {
+ return this.props.onBranch ? this.props.openCommit({sha: this.props.item.sha}) : null;
+ }
+
render() {
- const {messageHeadline, messageBody, abbreviatedOid, url} = this.props.item;
+ const {messageHeadline, messageBody, shortSha, url} = this.props.item;
const {avatarUrl, name, date} = this.props.item.committer;
return (
@@ -86,7 +97,8 @@ export default createFragmentContainer(PrCommitView, {
}
messageHeadline
messageBody
- abbreviatedOid
+ shortSha: abbreviatedOid
+ sha: oid
url
}`,
});
diff --git a/lib/views/pr-commits-view.js b/lib/views/pr-commits-view.js
index 32ad87ce5cc..3e4bc0f0ecd 100644
--- a/lib/views/pr-commits-view.js
+++ b/lib/views/pr-commits-view.js
@@ -32,6 +32,8 @@ export class PrCommitsView extends React.Component {
}),
),
}),
+ onBranch: PropTypes.bool.isRequired,
+ openCommit: PropTypes.func.isRequired,
}
constructor(props) {
@@ -71,6 +73,8 @@ export class PrCommitsView extends React.Component {
);
});
}
diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js
index 855fad6515a..ac64db3d404 100644
--- a/lib/views/recent-commits-view.js
+++ b/lib/views/recent-commits-view.js
@@ -4,6 +4,10 @@ import moment from 'moment';
import cx from 'classnames';
import {emojify} from 'node-emoji';
+import Commands, {Command} from '../atom/commands';
+import RefHolder from '../models/ref-holder';
+
+import CommitView from './commit-view';
import Timeago from './timeago';
class RecentCommitView extends React.Component {
@@ -11,14 +15,40 @@ class RecentCommitView extends React.Component {
commit: PropTypes.object.isRequired,
undoLastCommit: PropTypes.func.isRequired,
isMostRecent: PropTypes.bool.isRequired,
+ openCommit: PropTypes.func.isRequired,
+ isSelected: PropTypes.bool.isRequired,
};
+ constructor(props) {
+ super(props);
+
+ this.refRoot = new RefHolder();
+ }
+
+ componentDidMount() {
+ if (this.props.isSelected) {
+ this.refRoot.map(root => root.scrollIntoViewIfNeeded(false));
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.isSelected && !prevProps.isSelected) {
+ this.refRoot.map(root => root.scrollIntoViewIfNeeded(false));
+ }
+ }
+
render() {
const authorMoment = moment(this.props.commit.getAuthorDate() * 1000);
const fullMessage = this.props.commit.getFullMessage();
return (
-
+
{this.renderAuthors()}
+ onClick={this.undoLastCommit}>
Undo
)}
@@ -73,18 +103,65 @@ class RecentCommitView extends React.Component {
);
}
+
+ undoLastCommit = event => {
+ event.stopPropagation();
+ this.props.undoLastCommit();
+ }
}
export default class RecentCommitsView extends React.Component {
static propTypes = {
+ // Model state
commits: PropTypes.arrayOf(PropTypes.object).isRequired,
isLoading: PropTypes.bool.isRequired,
+ selectedCommitSha: PropTypes.string.isRequired,
+
+ // Atom environment
+ commandRegistry: PropTypes.object.isRequired,
+
+ // Action methods
undoLastCommit: PropTypes.func.isRequired,
+ openCommit: PropTypes.func.isRequired,
+ selectNextCommit: PropTypes.func.isRequired,
+ selectPreviousCommit: PropTypes.func.isRequired,
+ };
+
+ static focus = {
+ RECENT_COMMIT: Symbol('recent_commit'),
};
+ static firstFocus = RecentCommitsView.focus.RECENT_COMMIT;
+
+ static lastFocus = RecentCommitsView.focus.RECENT_COMMIT;
+
+ constructor(props) {
+ super(props);
+ this.refRoot = new RefHolder();
+ }
+
+ setFocus(focus) {
+ if (focus === this.constructor.focus.RECENT_COMMIT) {
+ return this.refRoot.map(element => element.focus()).getOr(false);
+ }
+
+ return false;
+ }
+
+ getFocus(element) {
+ return this.refRoot.map(e => e.contains(element)).getOr(false)
+ ? this.constructor.focus.RECENT_COMMIT
+ : null;
+ }
+
render() {
return (
-
+
+
+
+
+
+
{this.renderCommits()}
);
@@ -115,12 +192,31 @@ export default class RecentCommitsView extends React.Component {
isMostRecent={i === 0}
commit={commit}
undoLastCommit={this.props.undoLastCommit}
+ openCommit={() => this.props.openCommit({sha: commit.getSha(), preserveFocus: true})}
+ isSelected={this.props.selectedCommitSha === commit.getSha()}
/>
);
})}
);
}
+ }
+
+ openSelectedCommit = () => this.props.openCommit({sha: this.props.selectedCommitSha, preserveFocus: false})
+
+ advanceFocusFrom(focus) {
+ if (focus === this.constructor.focus.RECENT_COMMIT) {
+ return Promise.resolve(this.constructor.focus.RECENT_COMMIT);
+ }
+
+ return Promise.resolve(null);
+ }
+
+ retreatFocusFrom(focus) {
+ if (focus === this.constructor.focus.RECENT_COMMIT) {
+ return Promise.resolve(CommitView.lastFocus);
+ }
+ return Promise.resolve(null);
}
}
diff --git a/lib/views/staging-view.js b/lib/views/staging-view.js
index 8629ff89534..474db1aaa0c 100644
--- a/lib/views/staging-view.js
+++ b/lib/views/staging-view.js
@@ -12,6 +12,7 @@ import ObserveModel from './observe-model';
import MergeConflictListItemView from './merge-conflict-list-item-view';
import CompositeListSelection from '../models/composite-list-selection';
import ResolutionProgress from '../models/conflicts/resolution-progress';
+import CommitView from './commit-view';
import RefHolder from '../models/ref-holder';
import ChangedFileItem from '../items/changed-file-item';
import Commands, {Command} from '../atom/commands';
@@ -43,7 +44,7 @@ function calculateTruncatedLists(lists) {
}, {source: {}});
}
-const noop = () => {};
+const noop = () => { };
const MAXIMUM_LISTED_ENTRIES = 1000;
@@ -76,6 +77,10 @@ export default class StagingView extends React.Component {
STAGING: Symbol('staging'),
};
+ static firstFocus = StagingView.focus.STAGING;
+
+ static lastFocus = StagingView.focus.STAGING;
+
constructor(props) {
super(props);
autobind(
@@ -226,12 +231,12 @@ export default class StagingView extends React.Component {
{this.renderTruncatedMessage(this.props.unstagedChanges)}
- { this.renderMergeConflicts() }
+ {this.renderMergeConflicts()}
- Staged Changes
+ Staged Changes