Skip to content
This repository was archived by the owner on Dec 15, 2022. It is now read-only.

Convert the FilePatchController hierarchy to React #617

Merged
merged 31 commits into from
Apr 1, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6b1aaa3
Make FilePatchSelection immutable
smashwilson Mar 21, 2017
8688aa7
Port the HunkView and its tests to React
smashwilson Mar 23, 2017
dbd794f
FilePatchView :point_right: React
smashwilson Mar 23, 2017
e94187d
Centralize the "resolve this promise the next time X occurs" pattern.
smashwilson Mar 27, 2017
50815ea
Ensure getXyzPromise() and resolveXyzPromise() methods align
smashwilson Mar 27, 2017
13e1596
Support repeated events
smashwilson Mar 27, 2017
c47e837
Handle duplicate events correctly.
smashwilson Mar 27, 2017
c3bd8b8
Boolean logic is hard, okay
smashwilson Mar 27, 2017
261f00a
FilePatchController tests, so far
smashwilson Mar 27, 2017
40766ed
FilePatchController tests are :white_check_mark:
smashwilson Mar 28, 2017
61fc620
Oh hey I _did_ use that function
smashwilson Mar 28, 2017
4e8efd7
RootController tests are :white_check_mark:
smashwilson Mar 28, 2017
bba25e4
:fire: registerHunkView
smashwilson Mar 28, 2017
b345897
:burn: usage of `setStatePromise`
smashwilson Mar 28, 2017
0235eb8
RootController integration test to exercise actual PaneItem mounting
smashwilson Mar 28, 2017
a2bdf8d
Portal.getView() to construct a view-compatible item
smashwilson Mar 28, 2017
7210d26
Default PaneItem's item to a view
smashwilson Mar 28, 2017
8fb4b0f
Use the default PaneItem getter
smashwilson Mar 28, 2017
a1068df
Judicious use of `event.persist`
smashwilson Mar 28, 2017
aac603c
Correct FilePatchController focus
smashwilson Mar 28, 2017
6efa684
Always destroy PaneItems on unmount.
smashwilson Mar 28, 2017
2649a99
Work with Enzyme events
smashwilson Mar 28, 2017
89ae013
Mock the FilePatch pane
smashwilson Mar 29, 2017
de03a5e
Use either item.destroy() or activePane.destroyItem()
smashwilson Mar 29, 2017
b9ee6d5
:fire: setStatePromise
smashwilson Apr 1, 2017
5f5800d
Event subscriptions in componentDidMount
smashwilson Apr 1, 2017
48acdb8
selectAll needs to return a Promise
smashwilson Apr 1, 2017
60f2f1b
My // were showing
smashwilson Apr 1, 2017
028d16b
:fire: duplicate .destroy() check
smashwilson Apr 1, 2017
f8704ac
Implement onDidDestroy and destroy again
smashwilson Apr 1, 2017
e5c871d
Merge branch 'master' into aw-file-patch-to-react
smashwilson Apr 1, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 67 additions & 73 deletions lib/controllers/file-patch-controller.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,47 @@
/** @jsx etch.dom */
/* eslint react/no-unknown-property: "off" */
import React from 'react';

import {Emitter, Point} from 'atom';
import etch from 'etch';
import {Point, Emitter} from 'atom';
import {autobind} from 'core-decorators';

import EventWatcher from '../event-watcher';
import FilePatchView from '../views/file-patch-view';

export default class FilePatchController {
constructor(props) {
this.props = props;
this.emitter = new Emitter();
this.stagingOperationInProgress = false;
etch.initialize(this);
export default class FilePatchController extends React.Component {
static propTypes = {
repository: React.PropTypes.object,
commandRegistry: React.PropTypes.object.isRequired,
filePatch: React.PropTypes.object.isRequired,
stagingStatus: React.PropTypes.oneOf(['unstaged', 'staged']).isRequired,
isPartiallyStaged: React.PropTypes.bool.isRequired,
isAmending: React.PropTypes.bool.isRequired,
discardLines: React.PropTypes.func.isRequired,
didSurfaceFile: React.PropTypes.func.isRequired,
didDiveIntoFilePath: React.PropTypes.func.isRequired,
quietlySelectItem: React.PropTypes.func.isRequired,
undoLastDiscard: React.PropTypes.func.isRequired,
openFiles: React.PropTypes.func.isRequired,
eventWatcher: React.PropTypes.instanceOf(EventWatcher),
}

serialize() {
return null;
static defaultProps = {
eventWatcher: new EventWatcher(),
}

update(props) {
// If we update the active repository to null and that gets passed to this
// component, we'll instead hold on to our current repository (since it will
// not change for a given file).
const repository = props.repository || this.props.repository;
this.props = {...this.props, ...props, repository};
this.emitter.emit('did-change-title', this.getTitle());
return etch.update(this);
constructor(props, context) {
super(props, context);

this.stagingOperationInProgress = false;
this.emitter = new Emitter();
}

destroy() {
this.emitter.emit('did-destroy');
return etch.destroy(this);
serialize() {
return null;
}

getRepository() {
return this.props.repository;
componentWillReceiveProps(nextProps) {
if (this.getTitle(nextProps) !== this.getTitle()) {
this.emitter.emit('did-change-title', this.getTitle(nextProps));
}
}

render() {
Expand All @@ -53,17 +59,16 @@ export default class FilePatchController {
return (
<div className="github-PaneView pane-item">
<FilePatchView
ref="filePatchView"
commandRegistry={this.props.commandRegistry}
attemptLineStageOperation={this.attemptLineStageOperation}
didSurfaceFile={this.didSurfaceFile}
didDiveIntoCorrespondingFilePatch={this.diveIntoCorrespondingFilePatch}
attemptHunkStageOperation={this.attemptHunkStageOperation}
hunks={hunks}
filePath={filePath}
stagingStatus={this.props.stagingStatus}
isPartiallyStaged={this.props.isPartiallyStaged}
registerHunkView={this.props.registerHunkView}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very happy to see we can drop this 🙇

attemptLineStageOperation={this.attemptLineStageOperation}
attemptHunkStageOperation={this.attemptHunkStageOperation}
didSurfaceFile={this.didSurfaceFile}
didDiveIntoCorrespondingFilePatch={this.diveIntoCorrespondingFilePatch}
eventWatcher={this.props.eventWatcher}
openCurrentFile={this.openCurrentFile}
discardLines={this.props.discardLines}
undoLastDiscard={this.undoLastDiscard}
Expand All @@ -74,16 +79,26 @@ export default class FilePatchController {
}
}

stageHunk(hunk) {
return this.props.repository.applyPatchToIndex(
onDidChangeTitle(callback) {
return this.emitter.on('did-change-title', callback);
}

onDidDestroy(callback) {
return this.emitter.on('did-destroy', callback);
}

async stageHunk(hunk) {
await this.props.repository.applyPatchToIndex(
this.props.filePatch.getStagePatchForHunk(hunk),
);
this.props.eventWatcher.resolveStageOperationPromise();
}

async unstageHunk(hunk) {
await this.props.repository.applyPatchToIndex(
this.props.filePatch.getUnstagePatchForHunk(hunk),
);
this.props.eventWatcher.resolveStageOperationPromise();
}

stageOrUnstageHunk(hunk) {
Expand All @@ -99,33 +114,31 @@ export default class FilePatchController {
@autobind
attemptHunkStageOperation(hunk) {
if (this.stagingOperationInProgress) {
return {
stageOperationPromise: Promise.resolve(),
selectionUpdatePromise: Promise.resolve(),
};
return;
}

this.stagingOperationInProgress = true;

const hunkUpdatePromise = this.refs.filePatchView.getNextHunkUpdatePromise();
const stageOperationPromise = this.stageOrUnstageHunk(hunk);
const selectionUpdatePromise = hunkUpdatePromise.then(() => {
this.props.eventWatcher.getPatchChangedPromise().then(() => {
this.stagingOperationInProgress = false;
});

return {stageOperationPromise, selectionUpdatePromise};
this.stageOrUnstageHunk(hunk);
}

stageLines(lines) {
return this.props.repository.applyPatchToIndex(
async stageLines(lines) {
await this.props.repository.applyPatchToIndex(
this.props.filePatch.getStagePatchForLines(lines),
);

this.props.eventWatcher.resolveStageOperationPromise();
}

async unstageLines(lines) {
await this.props.repository.applyPatchToIndex(
this.props.filePatch.getUnstagePatchForLines(lines),
);

this.props.eventWatcher.resolveStageOperationPromise();
}

stageOrUnstageLines(lines) {
Expand All @@ -141,38 +154,24 @@ export default class FilePatchController {
@autobind
attemptLineStageOperation(lines) {
if (this.stagingOperationInProgress) {
return {
stageOperationPromise: Promise.resolve(),
selectionUpdatePromise: Promise.resolve(),
};
return;
}

this.stagingOperationInProgress = true;

const hunkUpdatePromise = this.refs.filePatchView.getNextHunkUpdatePromise();
const stageOperationPromise = this.stageOrUnstageLines(lines);
const selectionUpdatePromise = hunkUpdatePromise.then(() => {
this.props.eventWatcher.getPatchChangedPromise().then(() => {
this.stagingOperationInProgress = false;
});

return {stageOperationPromise, selectionUpdatePromise};
this.stageOrUnstageLines(lines);
}

getTitle() {
let title = this.props.stagingStatus === 'staged' ? 'Staged' : 'Unstaged';
getTitle(props = this.props) {
let title = props.stagingStatus === 'staged' ? 'Staged' : 'Unstaged';
title += ' Changes: ';
title += this.props.filePatch.getPath();
title += props.filePatch.getPath();
return title;
}

onDidChangeTitle(callback) {
return this.emitter.on('did-change-title', callback);
}

onDidDestroy(callback) {
return this.emitter.on('did-destroy', callback);
}

@autobind
didSurfaceFile() {
if (this.props.didSurfaceFile) {
Expand All @@ -188,15 +187,6 @@ export default class FilePatchController {
return this.props.didDiveIntoFilePath(filePath, stagingStatus, {amending: this.props.isAmending});
}

didUpdateFilePatch() {
// FilePatch was mutated so all we need to do is re-render
return etch.update(this);
}

didDestroyFilePatch() {
this.destroy();
}

focus() {
if (this.refs.filePatchView) {
this.refs.filePatchView.focus();
Expand All @@ -221,4 +211,8 @@ export default class FilePatchController {
hasUndoHistory() {
return this.props.repository.hasDiscardHistory(this.props.filePatch.getPath());
}

destroy() {
this.emitter.emit('did-destroy');
}
}
41 changes: 20 additions & 21 deletions lib/controllers/root-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import {GitError} from '../git-shell-out-strategy';
const nullFilePatchState = {
filePath: null,
filePatch: null,
stagingStatus: null,
partiallyStaged: null,
stagingStatus: 'unstaged',
partiallyStaged: false,
};

export default class RootController extends React.Component {
Expand Down Expand Up @@ -195,26 +195,23 @@ export default class RootController extends React.Component {
</Commands>
<PaneItem
workspace={this.props.workspace}
getItem={({subtree}) => subtree.getWrappedComponent()}
ref={c => { this.filePatchControllerPane = c; }}
onDidCloseItem={() => { this.setState({...nullFilePatchState}); }}>
<EtchWrapper ref={c => { this.filePatchController = c; }} reattachDomNode={false}>
<FilePatchController
repository={this.props.repository}
commandRegistry={this.props.commandRegistry}
filePatch={this.state.filePatch}
stagingStatus={this.state.stagingStatus}
isAmending={this.state.amending}
isPartiallyStaged={this.state.partiallyStaged}
onRepoRefresh={this.onRepoRefresh}
didSurfaceFile={this.surfaceFromFileAtPath}
didDiveIntoFilePath={this.diveIntoFilePatchForPath}
quietlySelectItem={this.quietlySelectItem}
openFiles={this.openFiles}
discardLines={this.discardLines}
undoLastDiscard={this.undoLastDiscard}
/>
</EtchWrapper>
<FilePatchController
repository={this.props.repository}
commandRegistry={this.props.commandRegistry}
filePatch={this.state.filePatch}
stagingStatus={this.state.stagingStatus}
isAmending={this.state.amending}
isPartiallyStaged={this.state.partiallyStaged}
onRepoRefresh={this.onRepoRefresh}
didSurfaceFile={this.surfaceFromFileAtPath}
didDiveIntoFilePath={this.diveIntoFilePatchForPath}
quietlySelectItem={this.quietlySelectItem}
openFiles={this.openFiles}
discardLines={this.discardLines}
undoLastDiscard={this.undoLastDiscard}
/>
</PaneItem>
</div>
);
Expand Down Expand Up @@ -436,7 +433,9 @@ export default class RootController extends React.Component {

@autobind
focusFilePatchView() {
this.filePatchController.getWrappedComponent().focus();
const item = this.filePatchControllerPane.getPaneItem();
const viewElement = item.getElement().querySelector('[tabindex]');
viewElement.focus();
}

@autobind
Expand Down
87 changes: 87 additions & 0 deletions lib/event-watcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Construct Promises to wait for the next occurrence of specific events that occur throughout the data refresh
* and rendering cycle. Resolve those promises when the corresponding events have been observed.
*/
export default class EventWatcher {
constructor() {
this.promises = new Map();
}

/*
* Retrieve a Promise that will be resolved the next time a desired event is observed.
*
* In general, you should prefer the more specific `getXyzPromise()` methods instead to avoid a proliferation of
* "magic strings."
*/
getPromise(eventName) {
const existing = this.promises.get(eventName);
if (existing !== undefined) {
return existing.promise;
}

let resolver, rejecter;
const created = new Promise((resolve, reject) => {
resolver = resolve;
rejecter = reject;
});
this.promises.set(eventName, {
promise: created,
resolver,
rejecter,
});
return created;
}

/*
* Indicate that a named event has been observed, resolving any Promises that were created for this event. Optionally
* provide a payload.
*
* In general, you should prefer the more specific `resolveXyzPromise()` methods.
*/
resolvePromise(eventName, payload) {
const existing = this.promises.get(eventName);
if (existing !== undefined) {
this.promises.delete(eventName);
existing.resolver(payload);
}
}

/*
* Indicate that a named event has had some kind of terrible problem.
*/
rejectPromise(eventName, error) {
const existing = this.promises.get(eventName);
if (existing !== undefined) {
this.promises.delete(eventName);
existing.rejecter(error);
}
}

/*
* Notified when a hunk or line stage or unstage operation has completed.
*/
getStageOperationPromise() {
return this.getPromise('stage-operation');
}

/*
* Notified when an open FilePatchView's hunks have changed.
*/
getPatchChangedPromise() {
return this.getPromise('patch-changed');
}

/*
* A hunk or line stage or unstage operation has completed.
*/
resolveStageOperationPromise(payload) {
this.resolvePromise('stage-operation', payload);
}

/*
* An open FilePatchView's hunks have changed.
*/
resolvePatchChangedPromise(payload) {
this.resolvePromise('patch-changed', payload);
}
}
Loading