Skip to content
This repository was archived by the owner on Dec 15, 2022. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b9e652a
:arrow_up: enzyme :heavy_plus_sign: React 1.5 adapter
smashwilson Apr 11, 2018
2d191b3
Activate the React 15 adapter
smashwilson Apr 11, 2018
6114214
CommitController :white_check_mark:
smashwilson Apr 11, 2018
c2ea0f8
EditorConflictController :white_check_mark:
smashwilson Apr 11, 2018
a9c45b6
:fire: whitespace :art:
smashwilson Apr 11, 2018
d10c960
Use a ref instead of findDOMNode
smashwilson Apr 11, 2018
0aa67ac
FilePatchController :white_check_mark:
smashwilson Apr 11, 2018
001dda8
GitTabController :white_check_mark:
smashwilson Apr 11, 2018
e1d45fc
RepositoryConflictController :white_check_mark:
smashwilson Apr 11, 2018
25f3369
RootController :white_check_mark:
smashwilson Apr 11, 2018
af53496
StatusBarTileController :white_check_mark:
smashwilson Apr 11, 2018
a3cd3c5
ObserveModelDecorator :white_check_mark:
smashwilson Apr 11, 2018
6c5ddd2
CloneDialog :white_check_mark:
smashwilson Apr 11, 2018
cfdc6c5
CoAuthorForm :white_check_mark:
smashwilson Apr 11, 2018
388d8dd
CommitView :white_check_mark:
smashwilson Apr 11, 2018
ecedd74
All but one of the FilePatchView tests
smashwilson Apr 11, 2018
e262ecc
InitDialog :white_check_mark:
smashwilson Apr 11, 2018
5fa3b3c
Merge branch 'master' of github.com:atom/github into aw/enzyme-up
smashwilson Apr 12, 2018
e9bb35c
RefHolder to operate on refs captured by parent components
smashwilson Apr 12, 2018
406622b
Use a RefHolder in Commands
smashwilson Apr 12, 2018
e693a44
RefHolder prop type
smashwilson Apr 12, 2018
911ab2d
Use a RefHolder to pass a DOM node from FilePatchView
smashwilson Apr 12, 2018
efce5f0
Is that test flaky now?
smashwilson Apr 12, 2018
9016e87
Log progress
smashwilson Apr 12, 2018
29d5547
Missed an await :eyes:
smashwilson Apr 12, 2018
5fe71a1
More console.logs
smashwilson Apr 12, 2018
1e11dae
Log the hunks we're rendering
smashwilson Apr 12, 2018
265f814
Cut down the iterations
smashwilson Apr 12, 2018
030b034
Report which FilePatch we're rendering when
smashwilson Apr 12, 2018
e84bad6
Utility to await a full repository refresh
smashwilson Apr 12, 2018
fafc1d3
:hocho: console.logs
smashwilson Apr 12, 2018
7bdc8ed
:fire: more diagnostics
smashwilson Apr 12, 2018
8dc266e
Use repository.refresh() to re-render with an updated patch
smashwilson Apr 12, 2018
d30c684
Missed a necessary refresh
smashwilson Apr 12, 2018
d4d245b
:fire: console.log
smashwilson Apr 12, 2018
7ee82e7
Merge branch 'master' into aw/enzyme-up
smashwilson Apr 12, 2018
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
18 changes: 16 additions & 2 deletions lib/controllers/file-patch-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,18 @@ export default class FilePatchController extends React.Component {
const staged = this.isStaged();
let filePatch = await this.getFilePatchForPath(this.props.filePath, staged);
const isPartiallyStaged = await repository.isPartiallyStaged(this.props.filePath);

const onFinish = () => {
this.props.switchboard.didFinishRepositoryRefresh();
};

if (filePatch) {
this.resolveFilePatchLoadedPromise();
if (!this.destroyed) { this.setState({filePatch, isPartiallyStaged}); }
if (!this.destroyed) {
this.setState({filePatch, isPartiallyStaged}, onFinish);
} else {
onFinish();
}
} else {
const oldFilePatch = this.state.filePatch;
if (oldFilePatch) {
Expand All @@ -140,7 +149,11 @@ export default class FilePatchController extends React.Component {
newFile: oldFilePatch.newFile.clone({mode: null, symlink: null}),
patch: oldFilePatch.getPatch().clone({hunks: []}),
});
if (!this.destroyed) { this.setState({filePatch, isPartiallyStaged}); }
if (!this.destroyed) {
this.setState({filePatch, isPartiallyStaged}, onFinish);
} else {
onFinish();
}
}
}
}
Expand Down Expand Up @@ -262,6 +275,7 @@ export default class FilePatchController extends React.Component {
await this.repositoryObserver.getActiveModel().applyPatchToIndex(
this.state.filePatch.getStagePatchForHunk(hunk),
);

this.props.switchboard.didFinishStageOperation({stage: true, hunk: true});
}

Expand Down
86 changes: 86 additions & 0 deletions lib/models/ref-holder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {Emitter} from 'event-kit';

/*
* Allow child components to operate on refs captured by a parent component.
*
* React does not guarantee that refs are available until the component has finished mounting (before
* componentDidMount() is called), but a component does not finish mounting until all of its children are mounted. This
* causes problems when a child needs to consume a DOM node from its parent to interact with the Atom API, like we do in
* the `Tooltip` and `Commands` components.
*
* To pass a ref to a child, capture it in a RefHolder in the parent, and pass the RefHolder to the child:
*
* class Parent extends React.Component {
* constructor() {
* this.theRef = new RefHolder();
* }
*
* render() {
* return (
* <div ref={this.theRef.setter}>
* <Child theRef={this.theRef} />
* </div>
* )
* }
* }
*
* In the child, use the `observe()` method to defer operations that need the DOM node to proceed:
*
* class Child extends React.Component {
*
* componentDidMount() {
* this.props.theRef.observe(domNode => this.register(domNode))
* }
*
* render() {
* return null;
* }
*
* register(domNode) {
* console.log('Hey look I have a real DOM node', domNode);
* }
* }
*/
export default class RefHolder {
constructor() {
this.emitter = new Emitter();
this.value = undefined;
}

isEmpty() {
return this.value === undefined;
}

get() {
if (this.isEmpty()) {
throw new Error('RefHolder is empty');
}
return this.value;
}

setter = value => {
if (value === null || value === undefined) { return; }
const oldValue = this.value;
this.value = value;
if (value !== oldValue) {
this.emitter.emit('did-update', value);
}
}

observe(callback) {
if (!this.isEmpty()) {
callback(this.value);
}
return this.emitter.on('did-update', callback);
}

static on(valueOrHolder) {
if (valueOrHolder instanceof this) {
return valueOrHolder;
} else {
const holder = new this();
holder.setter(valueOrHolder);
return holder;
}
}
}
7 changes: 7 additions & 0 deletions lib/prop-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,10 @@ export const RelayConnectionPropType = nodePropType => PropTypes.shape({
}),
totalCount: PropTypes.number,
});

export const RefHolderPropType = PropTypes.shape({
isEmpty: PropTypes.func.isRequired,
get: PropTypes.func.isRequired,
setter: PropTypes.func.isRequired,
observe: PropTypes.func.isRequired,
});
1 change: 1 addition & 0 deletions lib/switchboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export default class Switchboard {
'FinishActiveContextUpdate',
'FinishRender',
'FinishContextChangeRender',
'FinishRepositoryRefresh',
].forEach(eventName => {
Switchboard.prototype[`did${eventName}`] = function(payload) {
this.did(eventName, payload);
Expand Down
34 changes: 24 additions & 10 deletions lib/views/commands.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Disposable} from 'event-kit';

import {DOMNodePropType} from '../prop-types';
import {DOMNodePropType, RefHolderPropType} from '../prop-types';
import RefHolder from '../models/ref-holder';

export default class Commands extends React.Component {
static propTypes = {
registry: PropTypes.object.isRequired,
target: PropTypes.oneOfType([
PropTypes.string,
DOMNodePropType,
RefHolderPropType,
]).isRequired,
children: PropTypes.oneOfType([
PropTypes.element,
Expand All @@ -34,30 +37,41 @@ export class Command extends React.Component {
target: PropTypes.oneOfType([
PropTypes.string,
DOMNodePropType,
RefHolderPropType,
]),
command: PropTypes.string.isRequired,
callback: PropTypes.func.isRequired,
}

constructor(props, context) {
super(props, context);
this.subTarget = new Disposable();
this.subCommand = new Disposable();
}

componentDidMount() {
this.registerCommand(this.props);
this.observeTarget(this.props);
}

componentWillReceiveProps(newProps) {
for (const prop of ['registry', 'target', 'command', 'callback']) {
if (newProps[prop] !== this.props[prop]) {
this.disposable.dispose();
this.registerCommand(newProps);
}
if (['registry', 'target', 'command', 'callback'].some(p => newProps[p] !== this.props[p])) {
this.observeTarget(newProps);
}
}

componentWillUnmount() {
this.disposable.dispose();
this.subTarget.dispose();
this.subCommand.dispose();
}

observeTarget(props) {
this.subTarget.dispose();
this.subTarget = RefHolder.on(props.target).observe(t => this.registerCommand(t, props));
}

registerCommand({registry, target, command, callback}) {
this.disposable = registry.add(target, command, callback);
registerCommand(target, {registry, command, callback}) {
this.subCommand.dispose();
this.subCommand = registry.add(target, command, callback);
}

render() {
Expand Down
20 changes: 7 additions & 13 deletions lib/views/file-patch-view.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import ReactDom from 'react-dom';
import PropTypes from 'prop-types';

import {CompositeDisposable, Disposable} from 'event-kit';
Expand All @@ -11,6 +10,7 @@ import SimpleTooltip from './simple-tooltip';
import Commands, {Command} from './commands';
import FilePatchSelection from './file-patch-selection';
import Switchboard from '../switchboard';
import RefHolder from '../models/ref-holder';

const executableText = {
100644: 'non executable <code>100644</code>',
Expand Down Expand Up @@ -62,18 +62,16 @@ export default class FilePatchView extends React.Component {
this.mouseSelectionInProgress = false;
this.disposables = new CompositeDisposable();

this.refElement = new RefHolder();

this.state = {
selection: new FilePatchSelection(this.props.hunks),
domNode: null,
};
}

componentDidMount() {
window.addEventListener('mouseup', this.mouseup);
this.disposables.add(new Disposable(() => window.removeEventListener('mouseup', this.mouseup)));
this.setState({
domNode: ReactDom.findDOMNode(this),
});
}

componentWillReceiveProps(nextProps) {
Expand Down Expand Up @@ -128,10 +126,6 @@ export default class FilePatchView extends React.Component {
return true;
}

if (this.state.domNode !== nextState.domNode) {
return true;
}

return false;
}

Expand Down Expand Up @@ -213,9 +207,9 @@ export default class FilePatchView extends React.Component {
className={cx('github-FilePatchView', {'is-staged': !unstaged, 'is-unstaged': unstaged})}
tabIndex="-1"
onMouseUp={this.mouseup}
ref={e => { this.element = e; }}>
ref={this.refElement.setter}>

{this.state.domNode && this.registerCommands()}
{this.registerCommands()}

<header className="github-FilePatchView-header">
<span className="github-FilePatchView-title">
Expand All @@ -238,7 +232,7 @@ export default class FilePatchView extends React.Component {
registerCommands() {
return (
<div>
<Commands registry={this.props.commandRegistry} target={this.state.domNode}>
<Commands registry={this.props.commandRegistry} target={this.refElement}>
<Command command="github:toggle-patch-selection-mode" callback={this.togglePatchSelectionMode} />
<Command command="core:confirm" callback={this.didConfirm} />
<Command command="core:move-up" callback={this.selectPrevious} />
Expand Down Expand Up @@ -696,7 +690,7 @@ export default class FilePatchView extends React.Component {

@autobind
focus() {
this.element.focus();
this.refElement.get().focus();
}

@autobind
Expand Down
Loading