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
27 commits
Select commit Hold shift + click to select a range
dfd2c12
Move <PaneItem> to a new lib/atom/ directory
smashwilson Apr 23, 2018
b911ef0
URI pattern matching
smashwilson Apr 23, 2018
4678cc9
Okay I went a little overboard on URI matching
smashwilson Apr 23, 2018
4281d28
Avoid passing other arguments to asPart
smashwilson Apr 23, 2018
f4a2713
Manage open pane items
smashwilson Apr 23, 2018
0cd460b
Pass matched parameters to the render prop
smashwilson Apr 23, 2018
84abd5c
Remember the full matched URI
smashwilson Apr 23, 2018
6027264
URI-decode matched parameters
smashwilson Apr 23, 2018
7b53bed
Accept a URI in createItem()
smashwilson Apr 23, 2018
fd4b26b
Move URI pattern and construction to statics
smashwilson Apr 23, 2018
7c69a21
Reorganize RootController's render method
smashwilson Apr 23, 2018
133d7d6
Use the new PaneItem for FilePatchControllers
smashwilson Apr 23, 2018
f17cc9f
Hydrate stubs already present in the workspace at launch
smashwilson Apr 23, 2018
d8f8b47
Turns out openers are only kind of asynchronous
smashwilson Apr 23, 2018
244d18d
Lowercase
smashwilson Apr 23, 2018
f8311df
Remove old FilePatch opener
smashwilson Apr 23, 2018
4102a94
Render item portals within Fragments with keys
smashwilson Apr 23, 2018
6da403f
Use FilePatchController to build URI
smashwilson Apr 23, 2018
5ad31b0
FilePatchControllers are not managed that way
smashwilson Apr 23, 2018
50a7672
workdir is URI-encoded
smashwilson Apr 23, 2018
f3b666d
That should be a non-default import
smashwilson Apr 23, 2018
d2019f5
Convert IssueishPaneItem to use the PaneItem component
smashwilson Apr 23, 2018
24720e4
Convert GitTimingsView
smashwilson Apr 24, 2018
6e62a03
These tests are subsumed by the PaneItem
smashwilson Apr 24, 2018
249b8ea
Merge branch 'master' into aw/pane-item-pain
smashwilson Apr 24, 2018
341a2c2
Document the PaneItem and URIPattern
smashwilson Apr 24, 2018
70b3085
Merge branch 'master' into aw/pane-item-pain
smashwilson Apr 28, 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
103 changes: 38 additions & 65 deletions lib/atom-items/issueish-pane-item.js
Original file line number Diff line number Diff line change
@@ -1,85 +1,67 @@
import {Emitter} from 'event-kit';

import url from 'url';

import React from 'react';
import ReactDom from 'react-dom';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {autobind} from 'core-decorators';

import IssueishPaneItemController from '../controllers/issueish-pane-item-controller';
import GithubLoginModel from '../models/github-login-model';

function getPropsFromUri(uri) {
// atom-github://issueish/https://github-host.tld/owner/repo/issueishNumber
const {protocol, hostname, pathname} = url.parse(uri);
if (protocol === 'atom-github:' && hostname === 'issueish') {
const [scheme, host, owner, repo, issueishNum] = pathname.split('/').filter(s => s);
if (!scheme || !host || !owner || !repo || !issueishNum) { return null; }
const issueishNumber = parseInt(issueishNum, 10);
if (isNaN(issueishNumber)) { return null; }
return {owner, repo, issueishNumber, host: `${scheme}//${host}`};
export default class IssueishPaneItem extends Component {
static propTypes = {
uri: PropTypes.string.isRequired,
host: PropTypes.string.isRequired,
owner: PropTypes.string.isRequired,
repo: PropTypes.string.isRequired,
issueishNumber: PropTypes.number.isRequired,
}
return null;
}

export default class IssueishPaneItem {
static opener(uri) {
const props = getPropsFromUri(uri);
if (props) {
return IssueishPaneItem.create(props, uri);
} else {
return null;
}
}
static uriPattern = 'atom-github://issueish/{host}/{owner}/{repo}/{number}'

static create({owner, repo, issueishNumber, host}, uri) {
return new IssueishPaneItem({owner, repo, issueishNumber, host}, uri);
static buildURI(host, owner, repo, number) {
return 'atom-github://issueish/' +
encodeURIComponent(host) + '/' +
encodeURIComponent(owner) + '/' +
encodeURIComponent(repo) + '/' +
encodeURIComponent(number);
}

constructor({owner, repo, issueishNumber, host}, uri) {
this.emitter = new Emitter();
constructor(props) {
super(props);

this.owner = owner;
this.repo = repo;
this.issueishNumber = issueishNumber;
this.host = host;
this.uri = uri;
this.title = `${this.owner}/${this.repo}#${this.issueishNumber}`;
this.emitter = new Emitter();
this.title = `${this.props.owner}/${this.props.repo}#${this.props.issueishNumber}`;
this.loginModel = GithubLoginModel.get();

this.createComponent();
this.state = {};
}

createComponent() {
this.element = document.createElement('div');
const loginModel = GithubLoginModel.get();
this.props = {
owner: this.owner,
repo: this.repo,
issueishNumber: this.issueishNumber,
host: this.host,
loginModel,
static getDerivedStateFromProps(nextProps, prevState) {
return {
host: nextProps.host,
owner: nextProps.owner,
repo: nextProps.repo,
issueishNumber: nextProps.issueishNumber,
};
this.render();
}

render() {
this.component = ReactDom.render(
return (
<IssueishPaneItemController
{...this.props}
host={this.state.host}
owner={this.state.owner}
repo={this.state.repo}
issueishNumber={this.state.issueishNumber}
loginModel={this.loginModel}
onTitleChange={this.handleTitleChanged}
switchToIssueish={this.switchToIssueish}
/>,
this.element,
/>
);
}

@autobind
switchToIssueish(owner, repo, issueishNumber) {
this.owner = owner;
this.repo = repo;
this.issueishNumber = issueishNumber;
this.props = {...this.props, owner, repo, issueishNumber};
this.render();
this.setState({owner, repo, issueishNumber});
}

@autobind
Expand All @@ -94,27 +76,18 @@ export default class IssueishPaneItem {
return this.emitter.on('did-change-title', cb);
}

getElement() {
return this.element;
}

serialize() {
return {
uri: this.uri,
uri: this.props.uri,
deserializer: 'IssueishPaneItem',
};
}

getURI() {
return this.uri;
return this.props.uri;
}

getTitle() {
return this.title;
}

destroy() {
this.emitter.dispose();
ReactDom.unmountComponentAtNode(this.getElement());
}
}
155 changes: 155 additions & 0 deletions lib/atom/pane-item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import React, {Fragment} from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import {CompositeDisposable} from 'event-kit';

import URIPattern, {nonURIMatch} from './uri-pattern';
import RefHolder from '../models/ref-holder';
import {createItem} from '../helpers';

/**
* PaneItem registers an opener with the current Atom workspace as long as this component is mounted. The opener will
* trigger on URIs that match a specified pattern and render a subtree returned by a render prop.
*
* The render prop can receive three arguments:
*
* * itemHolder: A RefHolder. If used as the target for a ref, the referenced component will be used as the "item" of
* the pane item - its `getTitle()`, `getIconName()`, and other methods will be used by the pane.
* * params: An object containing the named parameters captured by the URI pattern.
* * uri: The exact, matched URI used to launch this item.
*
* render() {
* return (
* <PaneItem workspace={this.props.workspace} uriPattern='atom-github://host/{id}'>
* {({itemHolder, params}) => (
* <ItemComponent ref={itemHolder.setter} id={params.id} />
* )}
* </PaneItem>
* );
* }
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

👍👍

export default class PaneItem extends React.Component {
static propTypes = {
workspace: PropTypes.object.isRequired,
children: PropTypes.func.isRequired,
uriPattern: PropTypes.string.isRequired,
}

constructor(props) {
super(props);

const uriPattern = new URIPattern(this.props.uriPattern);
const currentlyOpen = this.props.workspace.getPaneItems()
.map(item => {
const element = item.getElement ? item.getElement() : null;
const match = item.getURI ? uriPattern.matches(item.getURI()) : nonURIMatch;
const stub = item.setRealItem ? item : null;
return {element, match, stub};
})
.filter(each => each.element && each.match.ok())
.map(each => new OpenItem(each.match, each.element, each.stub));

this.subs = new CompositeDisposable();
this.state = {uriPattern, currentlyOpen};
}

static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.uriPattern.getOriginal() === nextProps.uriPattern) {
return null;
}

return {
uriPattern: new URIPattern(nextProps.uriPattern),
};
}

componentDidMount() {
this.subs.add(this.props.workspace.addOpener(this.opener));

for (const openItem of this.state.currentlyOpen) {
openItem.hydrateStub();
}
}

render() {
return this.state.currentlyOpen.map(item => {
return (
<Fragment key={item.getURI()}>
{item.renderPortal(this.props.children)}
</Fragment>
);
});
}

componentWillUnmount() {
this.subs.dispose();
}

opener = uri => {
const m = this.state.uriPattern.matches(uri);
if (!m.ok()) {
return undefined;
}

const openItem = new OpenItem(m);

return new Promise(resolve => {
this.setState(prevState => ({
currentlyOpen: [...prevState.currentlyOpen, openItem],
}), () => {
const paneItem = openItem.create();

this.subs.add(
this.props.workspace.onDidDestroyPaneItem(({item}) => {
if (item === paneItem) {
this.setState(prevState => ({
currentlyOpen: prevState.currentlyOpen.filter(each => each !== openItem),
}));
}
}),
);

resolve(paneItem);
});
});
}
}

/**
* A subtree rendered through a portal onto a detached DOM node for use as the root as a PaneItem.
*/
class OpenItem {
constructor(match, element = null, stub = null) {
this.domNode = element || document.createElement('div');
this.stubItem = stub;
this.match = match;
this.itemHolder = new RefHolder();
}

getURI() {
return this.match.getURI();
}

create() {
const h = this.itemHolder.isEmpty() ? null : this.itemHolder;
return createItem(this.domNode, h, this.match.getURI());
}

hydrateStub() {
if (this.stubItem) {
this.stubItem.setRealItem(this.create());
this.stubItem = null;
}
}

renderPortal(renderProp) {
return ReactDOM.createPortal(
renderProp({
itemHolder: this.itemHolder,
params: this.match.getParams(),
uri: this.match.getURI(),
}),
this.domNode,
);
}
}
Loading