| /** |
| * @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 '../../../scripts/bundled-polymer.js'; |
| |
| 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 {util} from '../../../scripts/util.js'; |
| |
| import {NO_THREADS_MSG} from '../../../constants/messages.js'; |
| |
| /** |
| * Fired when a comment is saved or deleted |
| * |
| * @event thread-list-modified |
| * @extends Polymer.Element |
| */ |
| 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, |
| }, |
| _filteredThreads: { |
| type: Array, |
| computed: '_computeFilteredThreads(_sortedThreads, ' + |
| '_unresolvedOnly, _draftsOnly,' + |
| 'onlyShowRobotCommentsWithHumanReply)', |
| }, |
| _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 ['_computeSortedThreads(threads.*)']; } |
| |
| _computeShowDraftToggle(loggedIn) { |
| return loggedIn ? 'show' : ''; |
| } |
| |
| /** |
| * Order as follows: |
| * - Unresolved threads with drafts (reverse chronological) |
| * - Unresolved threads without drafts (reverse chronological) |
| * - Resolved threads with drafts (reverse chronological) |
| * - Resolved threads without drafts (reverse chronological) |
| * |
| * @param {!Object} changeRecord |
| */ |
| _computeSortedThreads(changeRecord) { |
| const threads = changeRecord.base; |
| if (!threads) { return []; } |
| this._updateSortedThreads(threads); |
| } |
| |
| // TODO(taoalpha): should allow only sort once during initialization |
| // to avoid flickering |
| _updateSortedThreads(threads) { |
| this._sortedThreads = |
| threads.map(this._getThreadWithSortInfo).sort((c1, c2) => { |
| // threads will be sorted by: |
| // - unresolved first |
| // - with drafts |
| // - file path |
| // - line |
| // - updated time |
| 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; } |
| } |
| |
| // TODO: Update here once we introduce patchset level comments |
| // they may not have or have a special line or path attribute |
| |
| if (c1.thread.path !== c2.thread.path) { |
| return c1.thread.path.localeCompare(c2.thread.path); |
| } |
| |
| // File level comments (no `line` property) |
| // should always show before any lines |
| if ([c1, c2].some(c => c.thread.line === undefined)) { |
| if (!c1.thread.line) { return -1; } |
| if (!c2.thread.line) { return 1; } |
| } else if (c1.thread.line !== c2.thread.line) { |
| return c1.thread.line - c2.thread.line; |
| } |
| |
| const c1Date = c1.__date || util.parseDate(c1.updated); |
| const c2Date = c2.__date || util.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); |
| }); |
| } |
| |
| _computeFilteredThreads(sortedThreads, unresolvedOnly, draftsOnly, |
| onlyShowRobotCommentsWithHumanReply) { |
| // Polymer 2: check for undefined |
| if ([ |
| sortedThreads, |
| unresolvedOnly, |
| draftsOnly, |
| onlyShowRobotCommentsWithHumanReply, |
| ].some(arg => arg === undefined)) { |
| return undefined; |
| } |
| |
| return sortedThreads.filter(c => { |
| if (draftsOnly) { |
| return c.hasDraft; |
| } else if (unresolvedOnly) { |
| return c.unresolved; |
| } else { |
| const comments = c && c.thread && c.thread.comments; |
| let robotComment = false; |
| let humanReplyToRobotComment = false; |
| comments.forEach(comment => { |
| if (comment.robot_id) { |
| robotComment = true; |
| } else if (robotComment) { |
| // Robot comment exists and human comment exists after it |
| humanReplyToRobotComment = true; |
| } |
| }); |
| if (robotComment && onlyShowRobotCommentsWithHumanReply) { |
| return humanReplyToRobotComment; |
| } |
| return c; |
| } |
| }).map(threadInfo => threadInfo.thread); |
| } |
| |
| _getThreadWithSortInfo(thread) { |
| const lastComment = thread.comments[thread.comments.length - 1] || {}; |
| |
| const lastNonDraftComment = |
| (lastComment.__draft && thread.comments.length > 1) ? |
| thread.comments[thread.comments.length - 2] : |
| lastComment; |
| |
| return { |
| thread, |
| // Use the unresolved bit for the last non draft comment. This is what |
| // anybody other than the current user would see. |
| unresolved: !!lastNonDraftComment.unresolved, |
| 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); |