diff options
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.js | 644 |
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; + }); }, }); })(); |