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

Commit

Permalink
Merge pull request #1386 from atom/aw/enzyme-up
Browse files Browse the repository at this point in the history
Upgrade to Enzyme 3
  • Loading branch information
smashwilson committed Apr 12, 2018
2 parents 502cc1f + 7ee82e7 commit 5f0bf0e
Show file tree
Hide file tree
Showing 24 changed files with 612 additions and 303 deletions.
18 changes: 16 additions & 2 deletions lib/controllers/file-patch-controller.js
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
@@ -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
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
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
@@ -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
@@ -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

0 comments on commit 5f0bf0e

Please sign in to comment.