summaryrefslogtreecommitdiffstats
path: root/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
diff options
context:
space:
mode:
Diffstat (limited to 'polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js')
-rw-r--r--polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js644
1 files changed, 504 insertions, 140 deletions
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
index ef39e1f673..4b64f7b4e4 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
@@ -1,33 +1,494 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
(function() {
'use strict';
const PARENT = 'PARENT';
+ const Defs = {};
+
+ /**
+ * @typedef {{
+ * basePatchNum: (string|number),
+ * patchNum: (number),
+ * }}
+ */
+ Defs.patchRange;
+
+ /**
+ * @typedef {{
+ * changeNum: number,
+ * path: string,
+ * patchRange: !Defs.patchRange,
+ * projectConfig: (Object|undefined),
+ * }}
+ */
+ Defs.commentMeta;
+
+ /**
+ * @typedef {{
+ * meta: !Defs.commentMeta,
+ * left: !Array,
+ * right: !Array,
+ * }}
+ */
+ Defs.commentsBySide;
+
+ /**
+ * Construct a change comments object, which can be data-bound to child
+ * elements of that which uses the gr-comment-api.
+ *
+ * @param {!Object} comments
+ * @param {!Object} robotComments
+ * @param {!Object} drafts
+ * @param {number} changeNum
+ * @constructor
+ */
+ function ChangeComments(comments, robotComments, drafts, changeNum) {
+ this._comments = comments;
+ this._robotComments = robotComments;
+ this._drafts = drafts;
+ this._changeNum = changeNum;
+ }
+
+ ChangeComments.prototype = {
+ get comments() {
+ return this._comments;
+ },
+ get drafts() {
+ return this._drafts;
+ },
+ get robotComments() {
+ return this._robotComments;
+ },
+ };
+
+ ChangeComments.prototype._patchNumEquals =
+ Gerrit.PatchSetBehavior.patchNumEquals;
+ ChangeComments.prototype._isMergeParent =
+ Gerrit.PatchSetBehavior.isMergeParent;
+ ChangeComments.prototype._getParentIndex =
+ Gerrit.PatchSetBehavior.getParentIndex;
+
+ /**
+ * Get an object mapping file paths to a boolean representing whether that
+ * path contains diff comments in the given patch set (including drafts and
+ * robot comments).
+ *
+ * Paths with comments are mapped to true, whereas paths without comments
+ * are not mapped.
+ *
+ * @param {Defs.patchRange=} opt_patchRange The patch-range object containing
+ * patchNum and basePatchNum properties to represent the range.
+ * @return {!Object}
+ */
+ ChangeComments.prototype.getPaths = function(opt_patchRange) {
+ const responses = [this.comments, this.drafts, this.robotComments];
+ const commentMap = {};
+ for (const response of responses) {
+ for (const path in response) {
+ if (response.hasOwnProperty(path) &&
+ response[path].some(c => {
+ // If don't care about patch range, we know that the path exists.
+ if (!opt_patchRange) { return true; }
+ return this._isInPatchRange(c, opt_patchRange);
+ })) {
+ commentMap[path] = true;
+ }
+ }
+ }
+ return commentMap;
+ };
+
+ /**
+ * Gets all the comments and robot comments for the given change.
+ *
+ * @param {number=} opt_patchNum
+ * @return {!Object}
+ */
+ ChangeComments.prototype.getAllPublishedComments = function(opt_patchNum) {
+ return this.getAllComments(false, opt_patchNum);
+ };
+
+ /**
+ * Gets all the comments for a particular thread group. Used for refreshing
+ * comments after the thread group has already been built.
+ *
+ * @param {string} rootId
+ * @return {!Array} an array of comments
+ */
+ ChangeComments.prototype.getCommentsForThread = function(rootId) {
+ const allThreads = this.getAllThreadsForChange();
+ const threadMatch = allThreads.find(t => t.rootId === rootId);
+
+ // In the event that a single draft comment was removed by the thread-list
+ // and the diff view is updating comments, there will no longer be a thread
+ // found. In this case, return null.
+ return threadMatch ? threadMatch.comments : null;
+ };
+
+ /**
+ * Filters an array of comments by line and side
+ *
+ * @param {!Array} comments
+ * @param {boolean} parentOnly whether the only comments returned should have
+ * the side attribute set to PARENT
+ * @param {string} commentSide whether the comment was left on the left or the
+ * right side regardless or unified or side-by-side
+ * @param {number=} opt_line line number, can be undefined if file comment
+ * @return {!Array} an array of comments
+ */
+ ChangeComments.prototype._filterCommentsBySideAndLine = function(comments,
+ parentOnly, commentSide, opt_line) {
+ return comments.filter(c => {
+ // if parentOnly, only match comments with PARENT for the side.
+ let sideMatch = parentOnly ? c.side === PARENT : c.side !== PARENT;
+ if (parentOnly) {
+ sideMatch = sideMatch && c.side === PARENT;
+ }
+ return sideMatch && c.line === opt_line;
+ }).map(c => {
+ c.__commentSide = commentSide;
+ return c;
+ });
+ };
+
+ /**
+ * Gets all the comments and robot comments for the given change.
+ *
+ * @param {boolean=} opt_includeDrafts
+ * @param {number=} opt_patchNum
+ * @return {!Object}
+ */
+ ChangeComments.prototype.getAllComments = function(opt_includeDrafts,
+ opt_patchNum) {
+ const paths = this.getPaths();
+ const publishedComments = {};
+ for (const path of Object.keys(paths)) {
+ let commentsToAdd = this.getAllCommentsForPath(path, opt_patchNum);
+ if (opt_includeDrafts) {
+ const drafts = this.getAllDraftsForPath(path, opt_patchNum)
+ .map(d => Object.assign({__draft: true}, d));
+ commentsToAdd = commentsToAdd.concat(drafts);
+ }
+ publishedComments[path] = commentsToAdd;
+ }
+ return publishedComments;
+ };
+
+ /**
+ * Gets all the comments and robot comments for the given change.
+ *
+ * @param {number=} opt_patchNum
+ * @return {!Object}
+ */
+ ChangeComments.prototype.getAllDrafts = function(opt_patchNum) {
+ const paths = this.getPaths();
+ const drafts = {};
+ for (const path of Object.keys(paths)) {
+ drafts[path] = this.getAllDraftsForPath(path, opt_patchNum);
+ }
+ return drafts;
+ };
+
+ /**
+ * Get the comments (robot comments) for a path and optional patch num.
+ *
+ * @param {!string} path
+ * @param {number=} opt_patchNum
+ * @param {boolean=} opt_includeDrafts
+ * @return {!Array}
+ */
+ ChangeComments.prototype.getAllCommentsForPath = function(path,
+ opt_patchNum, opt_includeDrafts) {
+ const comments = this._comments[path] || [];
+ const robotComments = this._robotComments[path] || [];
+ let allComments = comments.concat(robotComments);
+ if (opt_includeDrafts) {
+ const drafts = this.getAllDraftsForPath(path)
+ .map(d => Object.assign({__draft: true}, d));
+ allComments = allComments.concat(drafts);
+ }
+ if (!opt_patchNum) { return allComments; }
+ return (allComments || []).filter(c =>
+ this._patchNumEquals(c.patch_set, opt_patchNum)
+ );
+ };
+
+ /**
+ * Get the drafts for a path and optional patch num.
+ *
+ * @param {!string} path
+ * @param {number=} opt_patchNum
+ * @return {!Array}
+ */
+ ChangeComments.prototype.getAllDraftsForPath = function(path,
+ opt_patchNum) {
+ const comments = this._drafts[path] || [];
+ if (!opt_patchNum) { return comments; }
+ return (comments || []).filter(c =>
+ this._patchNumEquals(c.patch_set, opt_patchNum)
+ );
+ };
+
+ /**
+ * Get the comments (with drafts and robot comments) for a path and
+ * patch-range. Returns an object with left and right properties mapping to
+ * arrays of comments in on either side of the patch range for that path.
+ *
+ * @param {!string} path
+ * @param {!Defs.patchRange} patchRange The patch-range object containing patchNum
+ * and basePatchNum properties to represent the range.
+ * @param {Object=} opt_projectConfig Optional project config object to
+ * include in the meta sub-object.
+ * @return {!Defs.commentsBySide}
+ */
+ ChangeComments.prototype.getCommentsBySideForPath = function(path,
+ patchRange, opt_projectConfig) {
+ const comments = this.comments[path] || [];
+ const drafts = this.drafts[path] || [];
+ const robotComments = this.robotComments[path] || [];
+
+ drafts.forEach(d => { d.__draft = true; });
+
+ const all = comments.concat(drafts).concat(robotComments);
+
+ const baseComments = all.filter(c =>
+ this._isInBaseOfPatchRange(c, patchRange));
+ const revisionComments = all.filter(c =>
+ this._isInRevisionOfPatchRange(c, patchRange));
+
+ return {
+ meta: {
+ changeNum: this._changeNum,
+ path,
+ patchRange,
+ projectConfig: opt_projectConfig,
+ },
+ left: baseComments,
+ right: revisionComments,
+ };
+ };
+
+ /**
+ * @param {!Object} comments Object keyed by file, with a value of an array
+ * of comments left on that file.
+ * @return {!Array} A flattened list of all comments, where each comment
+ * also includes the file that it was left on, which was the key of the
+ * originall object.
+ */
+ ChangeComments.prototype._commentObjToArrayWithFile = function(comments) {
+ let commentArr = [];
+ for (const file of Object.keys(comments)) {
+ const commentsForFile = [];
+ for (const comment of comments[file]) {
+ commentsForFile.push(Object.assign({__path: file}, comment));
+ }
+ commentArr = commentArr.concat(commentsForFile);
+ }
+ return commentArr;
+ };
+
+ ChangeComments.prototype._commentObjToArray = function(comments) {
+ let commentArr = [];
+ for (const file of Object.keys(comments)) {
+ commentArr = commentArr.concat(comments[file]);
+ }
+ return commentArr;
+ };
+
+ /**
+ * Computes a string counting the number of commens in a given file and path.
+ *
+ * @param {number} patchNum
+ * @param {string=} opt_path
+ * @return {number}
+ */
+ ChangeComments.prototype.computeCommentCount = function(patchNum, opt_path) {
+ if (opt_path) {
+ return this.getAllCommentsForPath(opt_path, patchNum).length;
+ }
+ const allComments = this.getAllPublishedComments(patchNum);
+ return this._commentObjToArray(allComments).length;
+ };
+
+ /**
+ * Computes a string counting the number of draft comments in the entire
+ * change, optionally filtered by path and/or patchNum.
+ *
+ * @param {number=} opt_patchNum
+ * @param {string=} opt_path
+ * @return {number}
+ */
+ ChangeComments.prototype.computeDraftCount = function(opt_patchNum,
+ opt_path) {
+ if (opt_path) {
+ return this.getAllDraftsForPath(opt_path, opt_patchNum).length;
+ }
+ const allDrafts = this.getAllDrafts(opt_patchNum);
+ return this._commentObjToArray(allDrafts).length;
+ };
+
+ /**
+ * Computes a number of unresolved comment threads in a given file and path.
+ *
+ * @param {number} patchNum
+ * @param {string=} opt_path
+ * @return {number}
+ */
+ ChangeComments.prototype.computeUnresolvedNum = function(patchNum,
+ opt_path) {
+ let comments = [];
+ let drafts = [];
+
+ if (opt_path) {
+ comments = this.getAllCommentsForPath(opt_path, patchNum);
+ drafts = this.getAllDraftsForPath(opt_path, patchNum);
+ } else {
+ comments = this._commentObjToArray(
+ this.getAllPublishedComments(patchNum));
+ }
+
+ comments = comments.concat(drafts);
+
+ const threads = this.getCommentThreads(this._sortComments(comments));
+
+ const unresolvedThreads = threads
+ .filter(thread =>
+ thread.comments.length &&
+ thread.comments[thread.comments.length - 1].unresolved);
+
+ return unresolvedThreads.length;
+ };
+
+ ChangeComments.prototype.getAllThreadsForChange = function() {
+ const comments = this._commentObjToArrayWithFile(this.getAllComments(true));
+ const sortedComments = this._sortComments(comments);
+ return this.getCommentThreads(sortedComments);
+ };
+
+ ChangeComments.prototype._sortComments = function(comments) {
+ return comments.slice(0).sort((c1, c2) => {
+ return util.parseDate(c1.updated) - util.parseDate(c2.updated);
+ });
+ };
+
+ /**
+ * Computes all of the comments in thread format.
+ *
+ * @param {!Array} comments sorted by updated timestamp.
+ * @return {!Array}
+ */
+ ChangeComments.prototype.getCommentThreads = function(comments) {
+ const threads = [];
+ const idThreadMap = {};
+ for (const comment of comments) {
+ // If the comment is in reply to another comment, find that comment's
+ // thread and append to it.
+ if (comment.in_reply_to) {
+ const thread = idThreadMap[comment.in_reply_to];
+ if (thread) {
+ thread.comments.push(comment);
+ idThreadMap[comment.id] = thread;
+ continue;
+ }
+ }
+
+ // Otherwise, this comment starts its own thread.
+ const newThread = {
+ comments: [comment],
+ patchNum: comment.patch_set,
+ path: comment.__path,
+ line: comment.line,
+ rootId: comment.id,
+ };
+ if (comment.side) {
+ newThread.commentSide = comment.side;
+ }
+ threads.push(newThread);
+ idThreadMap[comment.id] = newThread;
+ }
+ return threads;
+ };
+
+ /**
+ * Whether the given comment should be included in the base side of the
+ * given patch range.
+ * @param {!Object} comment
+ * @param {!Defs.patchRange} range
+ * @return {boolean}
+ */
+ ChangeComments.prototype._isInBaseOfPatchRange = function(comment, range) {
+ // If the base of the patch range is a parent of a merge, and the comment
+ // appears on a specific parent then only show the comment if the parent
+ // index of the comment matches that of the range.
+ if (comment.parent && comment.side === PARENT) {
+ return this._isMergeParent(range.basePatchNum) &&
+ comment.parent === this._getParentIndex(range.basePatchNum);
+ }
+
+ // If the base of the range is the parent of the patch:
+ if (range.basePatchNum === PARENT &&
+ comment.side === PARENT &&
+ this._patchNumEquals(comment.patch_set, range.patchNum)) {
+ return true;
+ }
+ // If the base of the range is not the parent of the patch:
+ if (range.basePatchNum !== PARENT &&
+ comment.side !== PARENT &&
+ this._patchNumEquals(comment.patch_set, range.basePatchNum)) {
+ return true;
+ }
+ return false;
+ };
+
+ /**
+ * Whether the given comment should be included in the revision side of the
+ * given patch range.
+ * @param {!Object} comment
+ * @param {!Defs.patchRange} range
+ * @return {boolean}
+ */
+ ChangeComments.prototype._isInRevisionOfPatchRange = function(comment,
+ range) {
+ return comment.side !== PARENT &&
+ this._patchNumEquals(comment.patch_set, range.patchNum);
+ };
+
+ /**
+ * Whether the given comment should be included in the given patch range.
+ * @param {!Object} comment
+ * @param {!Defs.patchRange} range
+ * @return {boolean|undefined}
+ */
+ ChangeComments.prototype._isInPatchRange = function(comment, range) {
+ return this._isInBaseOfPatchRange(comment, range) ||
+ this._isInRevisionOfPatchRange(comment, range);
+ };
+
Polymer({
is: 'gr-comment-api',
properties: {
- /** @type {number} */
- _changeNum: Number,
- /** @type {!Object|undefined} */
- _comments: Object,
- /** @type {!Object|undefined} */
- _drafts: Object,
- /** @type {!Object|undefined} */
- _robotComments: Object,
+ _changeComments: Object,
+ },
+
+ listeners: {
+ 'reload-drafts': 'reloadDrafts',
},
behaviors: [
@@ -40,135 +501,38 @@
* does not yield the comment data.
*
* @param {number} changeNum
- * @return {!Promise}
+ * @return {!Promise<!Object>}
*/
loadAll(changeNum) {
- this._changeNum = changeNum;
-
- // Reset comment arrays.
- this._comments = undefined;
- this._drafts = undefined;
- this._robotComments = undefined;
-
const promises = [];
- promises.push(this.$.restAPI.getDiffComments(changeNum)
- .then(comments => { this._comments = comments; }));
- promises.push(this.$.restAPI.getDiffRobotComments(changeNum)
- .then(robotComments => { this._robotComments = robotComments; }));
- promises.push(this.$.restAPI.getDiffDrafts(changeNum)
- .then(drafts => { this._drafts = drafts; }));
-
- return Promise.all(promises);
- },
+ promises.push(this.$.restAPI.getDiffComments(changeNum));
+ promises.push(this.$.restAPI.getDiffRobotComments(changeNum));
+ promises.push(this.$.restAPI.getDiffDrafts(changeNum));
- /**
- * Get an object mapping file paths to a boolean representing whether that
- * path contains diff comments in the given patch set (including drafts and
- * robot comments).
- *
- * Paths with comments are mapped to true, whereas paths without comments
- * are not mapped.
- *
- * @param {!Object} patchRange The patch-range object containing patchNum
- * and basePatchNum properties to represent the range.
- * @return {Object}
- */
- getPaths(patchRange) {
- const responses = [this._comments, this._drafts, this._robotComments];
- const commentMap = {};
- for (const response of responses) {
- for (const path in response) {
- if (response.hasOwnProperty(path) &&
- response[path].some(c => this._isInPatchRange(c, patchRange))) {
- commentMap[path] = true;
- }
- }
- }
- return commentMap;
+ return Promise.all(promises).then(([comments, robotComments, drafts]) => {
+ this._changeComments = new ChangeComments(comments,
+ robotComments, drafts, changeNum);
+ return this._changeComments;
+ });
},
/**
- * Get the comments (with drafts and robot comments) for a path and
- * patch-range. Returns an object with left and right properties mapping to
- * arrays of comments in on either side of the patch range for that path.
+ * Re-initialize _changeComments with a new ChangeComments object, that
+ * uses the previous values for comments and robot comments, but fetches
+ * updated draft comments.
*
- * @param {!string} path
- * @param {!Object} patchRange The patch-range object containing patchNum
- * and basePatchNum properties to represent the range.
- * @param {Object=} opt_projectConfig Optional project config object to
- * include in the meta sub-object.
- * @return {Object}
- */
- getCommentsForPath(path, patchRange, opt_projectConfig) {
- const comments = this._comments[path] || [];
- const drafts = this._drafts[path] || [];
- const robotComments = this._robotComments[path] || [];
-
- drafts.forEach(d => { d.__draft = true; });
-
- const all = comments.concat(drafts).concat(robotComments);
-
- const baseComments = all.filter(c =>
- this._isInBaseOfPatchRange(c, patchRange));
- const revisionComments = all.filter(c =>
- this._isInRevisionOfPatchRange(c, patchRange));
-
- return {
- meta: {
- changeNum: this._changeNum,
- path,
- patchRange,
- projectConfig: opt_projectConfig,
- },
- left: baseComments,
- right: revisionComments,
- };
- },
-
- /**
- * Whether the given comment should be included in the base side of the
- * given patch range.
- * @param {!Object} comment
- * @param {!Object} range
- * @return {boolean}
+ * @param {number} changeNum
+ * @return {!Promise<!Object>}
*/
- _isInBaseOfPatchRange(comment, range) {
- // If the base of the range is the parent of the patch:
- if (range.basePatchNum === PARENT &&
- comment.side === PARENT &&
- this.patchNumEquals(comment.patch_set, range.patchNum)) {
- return true;
+ reloadDrafts(changeNum) {
+ if (!this._changeComments) {
+ return this.loadAll(changeNum);
}
- // If the base of the range is not the parent of the patch:
- if (range.basePatchNum !== PARENT &&
- comment.side !== PARENT &&
- this.patchNumEquals(comment.patch_set, range.basePatchNum)) {
- return true;
- }
- return false;
- },
-
- /**
- * Whether the given comment should be included in the revision side of the
- * given patch range.
- * @param {!Object} comment
- * @param {!Object} range
- * @return {boolean}
- */
- _isInRevisionOfPatchRange(comment, range) {
- return comment.side !== PARENT &&
- this.patchNumEquals(comment.patch_set, range.patchNum);
- },
-
- /**
- * Whether the given comment should be included in the given patch range.
- * @param {!Object} comment
- * @param {!Object} range
- * @return {boolean|undefined}
- */
- _isInPatchRange(comment, range) {
- return this._isInBaseOfPatchRange(comment, range) ||
- this._isInRevisionOfPatchRange(comment, range);
+ return this.$.restAPI.getDiffDrafts(changeNum).then(drafts => {
+ this._changeComments = new ChangeComments(this._changeComments.comments,
+ this._changeComments.robotComments, drafts, changeNum);
+ return this._changeComments;
+ });
},
});
})();