| /** |
| * @license |
| * Copyright (C) 2018 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. |
| */ |
| import '@polymer/paper-toggle-button/paper-toggle-button.js'; |
| import '../../../styles/shared-styles.js'; |
| import '../../shared/gr-comment-thread/gr-comment-thread.js'; |
| import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js'; |
| import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; |
| import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; |
| import {PolymerElement} from '@polymer/polymer/polymer-element.js'; |
| import {htmlTemplate} from './gr-thread-list_html.js'; |
| import {parseDate} from '../../../utils/date-util.js'; |
| |
| import {NO_THREADS_MSG} from '../../../constants/messages.js'; |
| import {SpecialFilePath} from '../../../constants/constants.js'; |
| |
| /** |
| * Fired when a comment is saved or deleted |
| * |
| * @event thread-list-modified |
| * @extends PolymerElement |
| */ |
| class GrThreadList extends GestureEventListeners( |
| LegacyElementMixin( |
| PolymerElement)) { |
| static get template() { return htmlTemplate; } |
| |
| static get is() { return 'gr-thread-list'; } |
| |
| static get properties() { |
| return { |
| /** @type {?} */ |
| change: Object, |
| threads: Array, |
| changeNum: String, |
| loggedIn: Boolean, |
| _sortedThreads: { |
| type: Array, |
| }, |
| _unresolvedOnly: { |
| type: Boolean, |
| value: false, |
| }, |
| _draftsOnly: { |
| type: Boolean, |
| value: false, |
| }, |
| /* Boolean properties used must default to false if passed as attribute |
| by the parent */ |
| onlyShowRobotCommentsWithHumanReply: { |
| type: Boolean, |
| value: false, |
| }, |
| hideToggleButtons: { |
| type: Boolean, |
| value: false, |
| }, |
| emptyThreadMsg: { |
| type: String, |
| value: NO_THREADS_MSG, |
| }, |
| }; |
| } |
| |
| static get observers() { |
| return ['_updateSortedThreads(threads, threads.splices)']; |
| } |
| |
| _computeShowDraftToggle(loggedIn) { |
| return loggedIn ? 'show' : ''; |
| } |
| |
| _compareThreads(c1, c2) { |
| if (c1.thread.path !== c2.thread.path) { |
| // '/PATCHSET' will not come before '/COMMIT' when sorting |
| // alphabetically so move it to the front explicitly |
| if (c1.thread.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) { |
| return -1; |
| } |
| if (c2.thread.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) { |
| return 1; |
| } |
| return c1.thread.path.localeCompare(c2.thread.path); |
| } |
| |
| // Patchset comments have no line/range associated with them |
| if (c1.thread.line !== c2.thread.line) { |
| if (!c1.thread.line || !c2.thread.line) { |
| // one of them is a file level comment, show first |
| return c1.thread.line ? 1 : -1; |
| } |
| return c1.thread.line < c2.thread.line ? -1 : 1; |
| } |
| |
| if (c1.thread.patchNum !== c2.thread.patchNum) { |
| return c1.thread.patchNum > c2.thread.patchNum ? -1 : 1; |
| } |
| |
| if (c2.unresolved !== c1.unresolved) { |
| if (!c1.unresolved) { return 1; } |
| if (!c2.unresolved) { return -1; } |
| } |
| |
| if (c2.hasDraft !== c1.hasDraft) { |
| if (!c1.hasDraft) { return 1; } |
| if (!c2.hasDraft) { return -1; } |
| } |
| |
| const c1Date = c1.__date || parseDate(c1.updated); |
| const c2Date = c2.__date || parseDate(c2.updated); |
| const dateCompare = c2Date - c1Date; |
| if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) { |
| return 0; |
| } |
| return dateCompare ? dateCompare : c1.id.localeCompare(c2.id); |
| } |
| |
| /** |
| * Observer on threads and update _sortedThreads when needed. |
| * Order as follows: |
| * - Patchset level threads (descending based on patchset number) |
| * - unresolved |
| - comments with drafts |
| - comments without drafts |
| * - resolved |
| - comments with drafts |
| - comments without drafts |
| * - File name |
| * - Line number |
| * - Unresolved (descending based on patchset number) |
| * - comments with drafts |
| * - comments without drafts |
| * - Resolved (descending based on patchset number) |
| * - comments with drafts |
| * - comments without drafts |
| * |
| * @param {Array<Object>} threads |
| * @param {!Object} spliceRecord |
| */ |
| _updateSortedThreads(threads, spliceRecord) { |
| if (!threads) { |
| this._sortedThreads = []; |
| return; |
| } |
| // We only want to sort on thread additions / removals to avoid |
| // re-rendering on modifications (add new reply / edit draft etc) |
| // https://polymer-library.polymer-project.org/3.0/docs/devguide/observers#array-observation |
| const isArrayMutation = spliceRecord && |
| (spliceRecord.indexSplices.addedCount !== 0 |
| || spliceRecord.indexSplices.removed.length); |
| |
| if (this._sortedThreads |
| && this._sortedThreads.length === threads.length |
| && !isArrayMutation) { |
| // Instead of replacing the _sortedThreads which will trigger a re-render, |
| // we override all threads inside of it |
| |
| for (const thread of threads) { |
| const idxInSortedThreads = this._sortedThreads |
| .findIndex(t => t.rootId === thread.rootId); |
| this.set(`_sortedThreads.${idxInSortedThreads}`, {...thread}); |
| } |
| return; |
| } |
| |
| const threadsWithInfo = threads |
| .map(thread => this._getThreadWithStatusInfo(thread)); |
| this._sortedThreads = threadsWithInfo.sort((t1, t2) => |
| this._compareThreads(t1, t2)).map(threadInfo => threadInfo.thread); |
| } |
| |
| _isFirstThreadWithFileName(sortedThreads, thread, unresolvedOnly, draftsOnly, |
| onlyShowRobotCommentsWithHumanReply) { |
| const threads = sortedThreads.filter(t => this._shouldShowThread( |
| t, unresolvedOnly, draftsOnly, |
| onlyShowRobotCommentsWithHumanReply)); |
| const index = threads.findIndex(t => t.rootId === thread.rootId); |
| if (index === -1) { |
| return false; |
| } |
| return index === 0 || (threads[index - 1].path !== threads[index].path); |
| } |
| |
| _shouldRenderSeparator(sortedThreads, thread, unresolvedOnly, draftsOnly, |
| onlyShowRobotCommentsWithHumanReply) { |
| const threads = sortedThreads.filter(t => this._shouldShowThread( |
| t, unresolvedOnly, draftsOnly, |
| onlyShowRobotCommentsWithHumanReply)); |
| const index = threads.findIndex(t => t.rootId === thread.rootId); |
| if (index === -1) { |
| return false; |
| } |
| return index > 0 && this._isFirstThreadWithFileName(sortedThreads, |
| thread, unresolvedOnly, draftsOnly, |
| onlyShowRobotCommentsWithHumanReply); |
| } |
| |
| _shouldShowThread(thread, unresolvedOnly, draftsOnly, |
| onlyShowRobotCommentsWithHumanReply) { |
| if ([ |
| thread, |
| unresolvedOnly, |
| draftsOnly, |
| onlyShowRobotCommentsWithHumanReply, |
| ].includes(undefined)) { |
| return false; |
| } |
| |
| if (!draftsOnly |
| && !unresolvedOnly |
| && !onlyShowRobotCommentsWithHumanReply) { |
| return true; |
| } |
| |
| const threadInfo = this._getThreadWithStatusInfo(thread); |
| |
| if (threadInfo.isEditing) { |
| return true; |
| } |
| |
| if (threadInfo.hasRobotComment |
| && onlyShowRobotCommentsWithHumanReply |
| && !threadInfo.hasHumanReplyToRobotComment) { |
| return false; |
| } |
| |
| let filtersCheck = true; |
| if (draftsOnly && unresolvedOnly) { |
| filtersCheck = threadInfo.hasDraft && threadInfo.unresolved; |
| } else if (draftsOnly) { |
| filtersCheck = threadInfo.hasDraft; |
| } else if (unresolvedOnly) { |
| filtersCheck = threadInfo.unresolved; |
| } |
| |
| return filtersCheck; |
| } |
| |
| _getThreadWithStatusInfo(thread) { |
| const comments = thread.comments; |
| const lastComment = comments[comments.length - 1] || {}; |
| let hasRobotComment = false; |
| let hasHumanReplyToRobotComment = false; |
| comments.forEach(comment => { |
| if (comment.robot_id) { |
| hasRobotComment = true; |
| } else if (hasRobotComment) { |
| hasHumanReplyToRobotComment = true; |
| } |
| }); |
| |
| return { |
| thread, |
| hasRobotComment, |
| hasHumanReplyToRobotComment, |
| unresolved: !!lastComment.unresolved, |
| isEditing: !!lastComment.__editing, |
| hasDraft: !!lastComment.__draft, |
| updated: lastComment.updated || lastComment.__date, |
| }; |
| } |
| |
| removeThread(rootId) { |
| for (let i = 0; i < this.threads.length; i++) { |
| if (this.threads[i].rootId === rootId) { |
| this.splice('threads', i, 1); |
| // Needed to ensure threads get re-rendered in the correct order. |
| flush(); |
| return; |
| } |
| } |
| } |
| |
| _handleThreadDiscard(e) { |
| this.removeThread(e.detail.rootId); |
| } |
| |
| _handleCommentsChanged(e) { |
| this.dispatchEvent(new CustomEvent('thread-list-modified', |
| {detail: {rootId: e.detail.rootId, path: e.detail.path}})); |
| } |
| |
| _isOnParent(side) { |
| return !!side; |
| } |
| |
| /** |
| * Work around a issue on iOS when clicking turns into double tap |
| */ |
| _onTapUnresolvedToggle(e) { |
| e.preventDefault(); |
| } |
| } |
| |
| customElements.define(GrThreadList.is, GrThreadList); |