diff --git a/graphql/schema.graphql b/graphql/schema.graphql
index 982da21ad8..4ae3e130ee 100644
--- a/graphql/schema.graphql
+++ b/graphql/schema.graphql
@@ -308,6 +308,12 @@ interface Comment {
# Check if this comment was created via an email reply.
createdViaEmail: Boolean!
+ # The user who edited the comment.
+ editor: User
+
+ # The moment the editor made the last edit
+ lastEditedAt: DateTime
+
# Identifies the date and time when the object was last updated.
updatedAt: DateTime!
@@ -464,8 +470,14 @@ type CommitComment implements Node, Comment, Reactable, RepositoryNode {
# Identifies the primary key from the database.
databaseId: Int @deprecated(reason: "Exposed database IDs will eventually be removed in favor of global Relay IDs.")
+
+ # The user who edited the comment.
+ editor: User
id: ID!
+ # The moment the editor made the last edit
+ lastEditedAt: DateTime
+
# Are reaction live updates enabled for this subject.
liveReactionUpdatesEnabled: Boolean!
@@ -916,8 +928,14 @@ type GistComment implements Node, Comment {
# Check if this comment was created via an email reply.
createdViaEmail: Boolean!
+
+ # The user who edited the comment.
+ editor: User
id: ID!
+ # The moment the editor made the last edit
+ lastEditedAt: DateTime
+
# Identifies the date and time when the object was last updated.
updatedAt: DateTime!
@@ -1218,6 +1236,9 @@ type Issue implements Node, Comment, Issueish, Reactable, RepositoryNode, Timeli
# Identifies the primary key from the database.
databaseId: Int @deprecated(reason: "Exposed database IDs will eventually be removed in favor of global Relay IDs.")
+
+ # The user who edited the comment.
+ editor: User
id: ID!
# A list of labels associated with the Issue or Pull Request.
@@ -1235,6 +1256,9 @@ type Issue implements Node, Comment, Issueish, Reactable, RepositoryNode, Timeli
before: String
): LabelConnection
+ # The moment the editor made the last edit
+ lastEditedAt: DateTime
+
# Are reaction live updates enabled for this subject.
liveReactionUpdatesEnabled: Boolean!
@@ -1363,11 +1387,17 @@ type IssueComment implements Node, Comment, Reactable, RepositoryNode {
# Identifies the primary key from the database.
databaseId: Int @deprecated(reason: "Exposed database IDs will eventually be removed in favor of global Relay IDs.")
+
+ # The user who edited the comment.
+ editor: User
id: ID!
# Identifies the issue associated with the comment.
issue: Issue!
+ # The moment the editor made the last edit
+ lastEditedAt: DateTime
+
# Are reaction live updates enabled for this subject.
liveReactionUpdatesEnabled: Boolean!
@@ -2644,6 +2674,9 @@ type PullRequest implements Node, Comment, Issueish, Reactable, RepositoryNode,
# Identifies the primary key from the database.
databaseId: Int @deprecated(reason: "Exposed database IDs will eventually be removed in favor of global Relay IDs.")
+ # The user who edited this pull request's body.
+ editor: User
+
# Identifies the head Ref associated with the pull request.
headRef: Ref
@@ -2666,6 +2699,9 @@ type PullRequest implements Node, Comment, Issueish, Reactable, RepositoryNode,
before: String
): LabelConnection
+ # The moment the editor made the last edit
+ lastEditedAt: DateTime
+
# Are reaction live updates enabled for this subject.
liveReactionUpdatesEnabled: Boolean!
@@ -2866,8 +2902,14 @@ type PullRequestReview implements Node, Comment, RepositoryNode {
# Identifies the primary key from the database.
databaseId: Int @deprecated(reason: "Exposed database IDs will eventually be removed in favor of global Relay IDs.")
+
+ # The user who edited the comment.
+ editor: User
id: ID!
+ # The moment the editor made the last edit
+ lastEditedAt: DateTime
+
# The HTTP URL permalink for this PullRequestReview.
path: URI!
@@ -2930,8 +2972,14 @@ type PullRequestReviewComment implements Node, Comment, Reactable, RepositoryNod
# The diff hunk to which the comment applies.
diffHunk: String!
+
+ # The user who edited the comment.
+ editor: User
id: ID!
+ # The moment the editor made the last edit
+ lastEditedAt: DateTime
+
# Are reaction live updates enabled for this subject.
liveReactionUpdatesEnabled: Boolean!
@@ -3461,7 +3509,7 @@ type ReferencedEvent implements Node, IssueEvent {
}
# A release contains the content for a release.
-type Release implements Node {
+type Release implements Node, UniformResourceLocatable {
# Identifies the description of the release.
description: String
id: ID!
@@ -3469,6 +3517,9 @@ type Release implements Node {
# Identifies the title of the release.
name: String!
+ # The HTTP path for this issue
+ path: URI!
+
# List of releases assets which are dependent on this release.
releaseAsset(
# Returns the first _n_ elements from the list.
@@ -3504,6 +3555,9 @@ type Release implements Node {
# The Git tag the release points to
tag: Ref
+
+ # The HTTP url for this issue
+ url: URI!
}
# A release asset contains the content for a release asset.
diff --git a/graphql/schema.json b/graphql/schema.json
index 66f458803e..048e7ca97b 100644
--- a/graphql/schema.json
+++ b/graphql/schema.json
@@ -641,6 +641,18 @@
"isDeprecated": true,
"deprecationReason": "Exposed database IDs will eventually be removed in favor of global Relay IDs."
},
+ {
+ "name": "editor",
+ "description": "The user who edited the comment.",
+ "args": [],
+ "type": {
+ "kind": "OBJECT",
+ "name": "User",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "id",
"description": null,
@@ -710,6 +722,18 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "lastEditedAt",
+ "description": "The moment the editor made the last edit",
+ "args": [],
+ "type": {
+ "kind": "SCALAR",
+ "name": "DateTime",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "liveReactionUpdatesEnabled",
"description": "Are reaction live updates enabled for this subject.",
@@ -2850,6 +2874,22 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "path",
+ "description": "The HTTP path for this issue",
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "URI",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "releaseAsset",
"description": "List of releases assets which are dependent on this release.",
@@ -2985,6 +3025,22 @@
},
"isDeprecated": false,
"deprecationReason": null
+ },
+ {
+ "name": "url",
+ "description": "The HTTP url for this issue",
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "URI",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
}
],
"inputFields": null,
@@ -2993,6 +3049,11 @@
"kind": "INTERFACE",
"name": "Node",
"ofType": null
+ },
+ {
+ "kind": "INTERFACE",
+ "name": "UniformResourceLocatable",
+ "ofType": null
}
],
"enumValues": null,
@@ -6254,6 +6315,18 @@
"isDeprecated": true,
"deprecationReason": "Exposed database IDs will eventually be removed in favor of global Relay IDs."
},
+ {
+ "name": "editor",
+ "description": "The user who edited this pull request's body.",
+ "args": [],
+ "type": {
+ "kind": "OBJECT",
+ "name": "User",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "headRef",
"description": "Identifies the head Ref associated with the pull request.",
@@ -6351,6 +6424,18 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "lastEditedAt",
+ "description": "The moment the editor made the last edit",
+ "args": [],
+ "type": {
+ "kind": "SCALAR",
+ "name": "DateTime",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "liveReactionUpdatesEnabled",
"description": "Are reaction live updates enabled for this subject.",
@@ -9799,6 +9884,18 @@
"isDeprecated": true,
"deprecationReason": "Exposed database IDs will eventually be removed in favor of global Relay IDs."
},
+ {
+ "name": "editor",
+ "description": "The user who edited the comment.",
+ "args": [],
+ "type": {
+ "kind": "OBJECT",
+ "name": "User",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "id",
"description": null,
@@ -9815,6 +9912,18 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "lastEditedAt",
+ "description": "The moment the editor made the last edit",
+ "args": [],
+ "type": {
+ "kind": "SCALAR",
+ "name": "DateTime",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "liveReactionUpdatesEnabled",
"description": "Are reaction live updates enabled for this subject.",
@@ -10212,6 +10321,30 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "editor",
+ "description": "The user who edited the comment.",
+ "args": [],
+ "type": {
+ "kind": "OBJECT",
+ "name": "User",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "lastEditedAt",
+ "description": "The moment the editor made the last edit",
+ "args": [],
+ "type": {
+ "kind": "SCALAR",
+ "name": "DateTime",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "updatedAt",
"description": "Identifies the date and time when the object was last updated.",
@@ -11186,6 +11319,18 @@
"isDeprecated": true,
"deprecationReason": "Exposed database IDs will eventually be removed in favor of global Relay IDs."
},
+ {
+ "name": "editor",
+ "description": "The user who edited the comment.",
+ "args": [],
+ "type": {
+ "kind": "OBJECT",
+ "name": "User",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "id",
"description": null,
@@ -11202,6 +11347,18 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "lastEditedAt",
+ "description": "The moment the editor made the last edit",
+ "args": [],
+ "type": {
+ "kind": "SCALAR",
+ "name": "DateTime",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "path",
"description": "The HTTP URL permalink for this PullRequestReview.",
@@ -11700,6 +11857,18 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "editor",
+ "description": "The user who edited the comment.",
+ "args": [],
+ "type": {
+ "kind": "OBJECT",
+ "name": "User",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "id",
"description": null,
@@ -11716,6 +11885,18 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "lastEditedAt",
+ "description": "The moment the editor made the last edit",
+ "args": [],
+ "type": {
+ "kind": "SCALAR",
+ "name": "DateTime",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "liveReactionUpdatesEnabled",
"description": "Are reaction live updates enabled for this subject.",
@@ -12353,6 +12534,18 @@
"isDeprecated": true,
"deprecationReason": "Exposed database IDs will eventually be removed in favor of global Relay IDs."
},
+ {
+ "name": "editor",
+ "description": "The user who edited the comment.",
+ "args": [],
+ "type": {
+ "kind": "OBJECT",
+ "name": "User",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "id",
"description": null,
@@ -12385,6 +12578,18 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "lastEditedAt",
+ "description": "The moment the editor made the last edit",
+ "args": [],
+ "type": {
+ "kind": "SCALAR",
+ "name": "DateTime",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "liveReactionUpdatesEnabled",
"description": "Are reaction live updates enabled for this subject.",
@@ -17196,6 +17401,11 @@
"name": "PullRequest",
"ofType": null
},
+ {
+ "kind": "OBJECT",
+ "name": "Release",
+ "ofType": null
+ },
{
"kind": "OBJECT",
"name": "Repository",
@@ -21426,6 +21636,18 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "editor",
+ "description": "The user who edited the comment.",
+ "args": [],
+ "type": {
+ "kind": "OBJECT",
+ "name": "User",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "id",
"description": null,
@@ -21442,6 +21664,18 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "lastEditedAt",
+ "description": "The moment the editor made the last edit",
+ "args": [],
+ "type": {
+ "kind": "SCALAR",
+ "name": "DateTime",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "updatedAt",
"description": "Identifies the date and time when the object was last updated.",
diff --git a/lib/views/decoration.js b/lib/views/decoration.js
new file mode 100644
index 0000000000..18605c6347
--- /dev/null
+++ b/lib/views/decoration.js
@@ -0,0 +1,108 @@
+import React from 'react';
+import {CompositeDisposable} from 'atom';
+
+import Portal from './portal';
+
+export default class Decoration extends React.Component {
+ static propTypes = {
+ editor: React.PropTypes.object.isRequired,
+ marker: React.PropTypes.object.isRequired,
+ type: React.PropTypes.oneOf(['line', 'line-number', 'highlight', 'overlay', 'gutter', 'block']).isRequired,
+ position: React.PropTypes.oneOf(['head', 'tail']),
+ class: React.PropTypes.string,
+ children: React.PropTypes.element,
+ getItem: React.PropTypes.func,
+ options: React.PropTypes.object,
+ }
+
+ static defaultProps = {
+ options: {},
+ position: 'head',
+ getItem: ({portal, subtree}) => portal,
+ }
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.decoration = null;
+ this.subscriptions = new CompositeDisposable();
+ }
+
+ usesItem() {
+ return this.props.type === 'gutter' || this.props.type === 'overlay' || this.props.type === 'block';
+ }
+
+ componentWillReceiveProps(nextProps) {
+ let recreationRequired = this.props.editor !== nextProps.editor ||
+ this.props.marker !== nextProps.marker ||
+ this.props.type !== nextProps.type ||
+ this.props.position !== nextProps.position ||
+ this.props.class !== nextProps.class ||
+ this.props.getItem !== nextProps.getItem ||
+ this.props.children !== nextProps.children;
+
+ if (!recreationRequired) {
+ // Compare additional options.
+ const optionKeys = Object.keys(this.props.options);
+ const nextOptionKeys = Object.keys(nextProps.options);
+
+ if (optionKeys.length !== nextOptionKeys.length) {
+ recreationRequired = true;
+ } else {
+ for (let i = 0; i < optionKeys.length; i++) {
+ const key = optionKeys[i];
+ if (this.props.options[key] !== nextProps.options[key]) {
+ recreationRequired = true;
+ break;
+ }
+ }
+ }
+ }
+
+ if (recreationRequired) {
+ this.decoration && this.decoration.destroy();
+ this.setupDecoration();
+ }
+ }
+
+ componentDidMount() {
+ this.setupDecoration();
+ }
+
+ render() {
+ if (this.usesItem()) {
+ return { this.portal = c; }}>{this.props.children};
+ } else {
+ return null;
+ }
+ }
+
+ setupDecoration() {
+ if (this.decoration) {
+ return;
+ }
+
+ let item = null;
+ if (this.usesItem()) {
+ item = this.props.getItem({portal: this.portal, subtree: this.portal.getRenderedSubtree()});
+ }
+
+ const options = {
+ ...this.props.options,
+ type: this.props.type,
+ position: this.props.position,
+ class: this.props.class,
+ item,
+ };
+
+ this.decoration = this.props.editor.decorateMarker(this.props.marker, options);
+ this.subscriptions.add(this.decoration.onDidDestroy(() => {
+ this.decoration = null;
+ }));
+ }
+
+ componentWillUnmount() {
+ this.decoration && this.decoration.destroy();
+ this.subscriptions.dispose();
+ }
+}
diff --git a/test/views/decoration.test.js b/test/views/decoration.test.js
new file mode 100644
index 0000000000..6747c15db3
--- /dev/null
+++ b/test/views/decoration.test.js
@@ -0,0 +1,112 @@
+import React from 'react';
+import sinon from 'sinon';
+import {mount} from 'enzyme';
+
+import Decoration from '../../lib/views/decoration';
+
+describe('Decoration', () => {
+ let atomEnv, editor, marker;
+
+ beforeEach(async () => {
+ atomEnv = global.buildAtomEnvironment();
+ const workspace = atomEnv.workspace;
+
+ editor = await workspace.open(__filename);
+ marker = editor.markBufferRange([[2, 0], [6, 0]]);
+ });
+
+ afterEach(() => atomEnv.destroy());
+
+ it('decorates its marker on render', () => {
+ const app = (
+
+ );
+ mount(app);
+
+ assert.lengthOf(editor.getLineDecorations({position: 'head', class: 'something'}), 1);
+ });
+
+ describe('with a subtree', () => {
+ beforeEach(() => {
+ sinon.spy(editor, 'decorateMarker');
+ });
+
+ it('creates a block decoration', () => {
+ const app = (
+
+
+ This is a subtree
+
+
+ );
+ mount(app);
+
+ const args = editor.decorateMarker.firstCall.args;
+ assert.equal(args[0], marker);
+ assert.equal(args[1].type, 'block');
+ const child = args[1].item.node.firstElementChild;
+ assert.equal(child.className, 'decoration-subtree');
+ assert.equal(child.textContent, 'This is a subtree');
+ });
+
+ it('creates an overlay decoration', () => {
+ const app = (
+
+
+ This is a subtree
+
+
+ );
+ mount(app);
+
+ const args = editor.decorateMarker.firstCall.args;
+ assert.equal(args[0], marker);
+ assert.equal(args[1].type, 'overlay');
+ const child = args[1].item.node.firstElementChild;
+ assert.equal(child.className, 'decoration-subtree');
+ assert.equal(child.textContent, 'This is a subtree');
+ });
+
+ it('creates a gutter decoration', () => {
+ const app = (
+
+
+ This is a subtree
+
+
+ );
+ mount(app);
+
+ const args = editor.decorateMarker.firstCall.args;
+ assert.equal(args[0], marker);
+ assert.equal(args[1].type, 'gutter');
+ const child = args[1].item.node.firstElementChild;
+ assert.equal(child.className, 'decoration-subtree');
+ assert.equal(child.textContent, 'This is a subtree');
+ });
+ });
+
+ it('destroys its decoration on unmount', () => {
+ const app = (
+
+ );
+ const wrapper = mount(app);
+
+ assert.lengthOf(editor.getLineDecorations({class: 'whatever'}), 1);
+
+ wrapper.unmount();
+
+ assert.lengthOf(editor.getLineDecorations({class: 'whatever'}), 0);
+ });
+});