Merge changes from topics "ts-comment-thread", "ts-thread-list"
* changes: Convert gr-thread-list to typescript Rename files to preserve history Convert gr-comment-thread to typescript Rename files to preserve history
This commit is contained in:
@@ -1141,6 +1141,7 @@ class GrChangeView extends KeyboardShortcutMixin(
|
||||
// selected file index.
|
||||
const patchRangeState = this.viewState.patchRange;
|
||||
if (this.viewState.changeNum !== this._changeNum ||
|
||||
!patchRangeState ||
|
||||
patchRangeState.basePatchNum !== this._patchRange.basePatchNum ||
|
||||
patchRangeState.patchNum !== this._patchRange.patchNum) {
|
||||
this._resetFileListViewState();
|
||||
|
||||
@@ -821,6 +821,8 @@ suite('gr-change-view tests', () => {
|
||||
getAllThreadsForChange: () => THREADS,
|
||||
computeDraftCount: () => 0,
|
||||
}));
|
||||
element._change = generateChange();
|
||||
element._changeNum = element._change._number;
|
||||
});
|
||||
|
||||
test('draft threads should be a new copy with correct states', done => {
|
||||
@@ -941,8 +943,10 @@ suite('gr-change-view tests', () => {
|
||||
|
||||
suite('Findings comment tab', () => {
|
||||
setup(done => {
|
||||
element._changeNum = '42';
|
||||
element._change = {
|
||||
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
|
||||
project: 'testRepo',
|
||||
revisions: {
|
||||
rev2: {_number: 2, commit: {parents: []}},
|
||||
rev1: {_number: 1, commit: {parents: []}},
|
||||
|
||||
@@ -1549,6 +1549,7 @@ suite('gr-file-list tests', () => {
|
||||
element = commentApiWrapper.$.fileList;
|
||||
loadCommentSpy = sinon.spy(commentApiWrapper.$.commentAPI, 'loadAll');
|
||||
element.diffPrefs = {};
|
||||
element.change = {_number: 42, project: 'testRepo'};
|
||||
sinon.stub(element, '_reviewFile');
|
||||
|
||||
// Stub methods on the changeComments object after changeComments has
|
||||
|
||||
@@ -1,308 +0,0 @@
|
||||
/**
|
||||
* @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);
|
||||
@@ -0,0 +1,371 @@
|
||||
/**
|
||||
* @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';
|
||||
import '../../../styles/shared-styles';
|
||||
import '../../shared/gr-comment-thread/gr-comment-thread';
|
||||
import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
|
||||
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
|
||||
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
|
||||
import {PolymerElement} from '@polymer/polymer/polymer-element';
|
||||
import {htmlTemplate} from './gr-thread-list_html';
|
||||
import {parseDate} from '../../../utils/date-util';
|
||||
|
||||
import {NO_THREADS_MSG} from '../../../constants/messages';
|
||||
import {CommentSide, SpecialFilePath} from '../../../constants/constants';
|
||||
import {customElement, property, observe} from '@polymer/decorators';
|
||||
import {CommentThread} from '../../diff/gr-comment-api/gr-comment-api';
|
||||
import {Comment, RobotComment} from '../../shared/gr-comment/gr-comment';
|
||||
import {PolymerSpliceChange} from '@polymer/polymer/interfaces';
|
||||
import {ChangeInfo} from '../../../types/common';
|
||||
|
||||
interface CommentThreadWithInfo {
|
||||
thread: CommentThread;
|
||||
hasRobotComment: boolean;
|
||||
hasHumanReplyToRobotComment: boolean;
|
||||
unresolved: boolean;
|
||||
isEditing: boolean;
|
||||
hasDraft: boolean;
|
||||
updated?: Date;
|
||||
}
|
||||
|
||||
@customElement('gr-thread-list')
|
||||
export class GrThreadList extends GestureEventListeners(
|
||||
LegacyElementMixin(PolymerElement)
|
||||
) {
|
||||
static get template() {
|
||||
return htmlTemplate;
|
||||
}
|
||||
|
||||
@property({type: Object})
|
||||
change?: ChangeInfo;
|
||||
|
||||
@property({type: Array})
|
||||
threads: CommentThread[] = [];
|
||||
|
||||
@property({type: String})
|
||||
changeNum?: string;
|
||||
|
||||
@property({type: Boolean})
|
||||
loggedIn?: boolean;
|
||||
|
||||
@property({type: Array})
|
||||
_sortedThreads: CommentThread[] = [];
|
||||
|
||||
@property({type: Boolean})
|
||||
_unresolvedOnly = false;
|
||||
|
||||
@property({type: Boolean})
|
||||
_draftsOnly = false;
|
||||
|
||||
@property({type: Boolean})
|
||||
onlyShowRobotCommentsWithHumanReply = false;
|
||||
|
||||
@property({type: Boolean})
|
||||
hideToggleButtons = false;
|
||||
|
||||
@property({type: String})
|
||||
emptyThreadMsg = NO_THREADS_MSG;
|
||||
|
||||
_computeShowDraftToggle(loggedIn?: boolean) {
|
||||
return loggedIn ? 'show' : '';
|
||||
}
|
||||
|
||||
_compareThreads(c1: CommentThreadWithInfo, c2: CommentThreadWithInfo) {
|
||||
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) {
|
||||
if (!c1.thread.patchNum) return 1;
|
||||
if (!c2.thread.patchNum) return -1;
|
||||
// TODO(TS): Explicit comparison for 'edit' and 'PARENT' missing?
|
||||
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;
|
||||
}
|
||||
|
||||
if (c2.updated !== c1.updated) {
|
||||
if (!c1.updated) return 1;
|
||||
if (!c2.updated) return -1;
|
||||
return c2.updated.getTime() - c1.updated.getTime();
|
||||
}
|
||||
|
||||
if (c2.thread.rootId !== c1.thread.rootId) {
|
||||
if (!c1.thread.rootId) return 1;
|
||||
if (!c2.thread.rootId) return -1;
|
||||
return c1.thread.rootId.localeCompare(c2.thread.rootId);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 threads
|
||||
* @param spliceRecord
|
||||
*/
|
||||
@observe('threads', 'threads.splices')
|
||||
_updateSortedThreads(
|
||||
threads: CommentThread[],
|
||||
_: PolymerSpliceChange<CommentThread[]>
|
||||
) {
|
||||
if (!threads || threads.length === 0) {
|
||||
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
|
||||
// TODO(TS): We have removed a buggy check of the splices here. A splice
|
||||
// with addedCount > 0 or removed.length > 0 should also cause re-sorting
|
||||
// and re-rendering, but apparently spliceRecord is always undefined for
|
||||
// whatever reason.
|
||||
if (this._sortedThreads.length === threads.length) {
|
||||
// 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: CommentThread[],
|
||||
thread: CommentThread,
|
||||
unresolvedOnly?: boolean,
|
||||
draftsOnly?: boolean,
|
||||
onlyShowRobotCommentsWithHumanReply?: boolean
|
||||
) {
|
||||
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: CommentThread[],
|
||||
thread: CommentThread,
|
||||
unresolvedOnly?: boolean,
|
||||
draftsOnly?: boolean,
|
||||
onlyShowRobotCommentsWithHumanReply?: boolean
|
||||
) {
|
||||
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: CommentThread,
|
||||
unresolvedOnly?: boolean,
|
||||
draftsOnly?: boolean,
|
||||
onlyShowRobotCommentsWithHumanReply?: boolean
|
||||
) {
|
||||
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: CommentThread): CommentThreadWithInfo {
|
||||
const comments = thread.comments;
|
||||
const lastComment = (comments[comments.length - 1] || {}) as Comment;
|
||||
let hasRobotComment = false;
|
||||
let hasHumanReplyToRobotComment = false;
|
||||
comments.forEach(comment => {
|
||||
if ((comment as RobotComment).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
|
||||
? parseDate(lastComment.updated)
|
||||
: lastComment.__date,
|
||||
};
|
||||
}
|
||||
|
||||
removeThread(rootId: string) {
|
||||
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: CustomEvent) {
|
||||
this.removeThread(e.detail.rootId);
|
||||
}
|
||||
|
||||
_handleCommentsChanged(e: CustomEvent) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('thread-list-modified', {
|
||||
detail: {rootId: e.detail.rootId, path: e.detail.path},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
_isOnParent(side?: CommentSide) {
|
||||
// TODO(TS): That looks like a bug? CommentSide.REVISION will also be
|
||||
// classified as parent??
|
||||
return !!side;
|
||||
}
|
||||
|
||||
/**
|
||||
* Work around a issue on iOS when clicking turns into double tap
|
||||
*/
|
||||
_onTapUnresolvedToggle(e: Event) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'gr-thread-list': GrThreadList;
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,10 @@ suite('gr-thread-list tests', () => {
|
||||
|
||||
setup(done => {
|
||||
element = basicFixture.instantiate();
|
||||
element.changeNum = 123;
|
||||
element.change = {
|
||||
project: 'testRepo',
|
||||
};
|
||||
element.threads = [
|
||||
{
|
||||
comments: [
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
PathToCommentsInfoMap,
|
||||
PathToRobotCommentsInfoMap,
|
||||
RobotCommentInfo,
|
||||
Timestamp,
|
||||
UrlEncodedCommentId,
|
||||
ChangeNum,
|
||||
} from '../../../types/common';
|
||||
@@ -81,24 +82,27 @@ export function isPatchSetFile(
|
||||
return !!(x as PatchSetFile).path;
|
||||
}
|
||||
|
||||
export function sortComments<
|
||||
T extends CommentInfoWithPath | CommentInfoWithTwoPaths
|
||||
>(comments: T[]): T[] {
|
||||
interface SortableComment {
|
||||
__draft?: boolean;
|
||||
__date?: Date;
|
||||
updated?: Timestamp;
|
||||
id?: UrlEncodedCommentId;
|
||||
}
|
||||
|
||||
export function sortComments<T extends SortableComment>(comments: T[]): T[] {
|
||||
return comments.slice(0).sort((c1, c2) => {
|
||||
const d1 = !!(c1 as HumanCommentInfoWithPath).__draft;
|
||||
const d2 = !!(c2 as HumanCommentInfoWithPath).__draft;
|
||||
const d1 = !!c1.__draft;
|
||||
const d2 = !!c2.__draft;
|
||||
if (d1 !== d2) return d1 ? 1 : -1;
|
||||
const date1 =
|
||||
(c1.updated && parseDate(c1.updated)) ||
|
||||
(c1 as HumanCommentInfoWithPath).__date;
|
||||
const date2 =
|
||||
(c2.updated && parseDate(c2.updated)) ||
|
||||
(c2 as HumanCommentInfoWithPath).__date;
|
||||
const dateDiff = date1.valueOf() - date2.valueOf();
|
||||
if (dateDiff) {
|
||||
return dateDiff;
|
||||
}
|
||||
return c1.id < c2.id ? -1 : c1.id > c2.id ? 1 : 0;
|
||||
|
||||
const date1 = (c1.updated && parseDate(c1.updated)) || c1.__date;
|
||||
const date2 = (c2.updated && parseDate(c2.updated)) || c2.__date;
|
||||
const dateDiff = date1!.valueOf() - date2!.valueOf();
|
||||
if (dateDiff !== 0) return dateDiff;
|
||||
|
||||
const id1 = c1.id ?? '';
|
||||
const id2 = c2.id ?? '';
|
||||
return id1.localeCompare(id2);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -568,6 +572,7 @@ export class ChangeComments {
|
||||
const threads: CommentThread[] = [];
|
||||
const idThreadMap: CommentIdToCommentThreadMap = {};
|
||||
for (const comment of comments) {
|
||||
if (!comment.id) continue;
|
||||
// If the comment is in reply to another comment, find that comment's
|
||||
// thread and append to it.
|
||||
if (comment.in_reply_to) {
|
||||
|
||||
@@ -219,15 +219,15 @@ suite('gr-comment-api tests', () => {
|
||||
element._changeComments._drafts = {
|
||||
'file/one': [
|
||||
{
|
||||
id: 11,
|
||||
id: '11',
|
||||
patch_set: 2,
|
||||
side: PARENT,
|
||||
line: 1,
|
||||
updated: makeTime(3),
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
in_reply_to: 4,
|
||||
id: '12',
|
||||
in_reply_to: '04',
|
||||
patch_set: 2,
|
||||
line: 1,
|
||||
// Draft gets lower timestamp than published comment, because we
|
||||
@@ -237,7 +237,7 @@ suite('gr-comment-api tests', () => {
|
||||
],
|
||||
'file/two': [
|
||||
{
|
||||
id: 5,
|
||||
id: '05',
|
||||
patch_set: 3,
|
||||
line: 1,
|
||||
updated: makeTime(3),
|
||||
@@ -247,7 +247,7 @@ suite('gr-comment-api tests', () => {
|
||||
element._changeComments._robotComments = {
|
||||
'file/one': [
|
||||
{
|
||||
id: 1,
|
||||
id: '01',
|
||||
patch_set: 2,
|
||||
side: PARENT,
|
||||
line: 1,
|
||||
@@ -259,8 +259,8 @@ suite('gr-comment-api tests', () => {
|
||||
end_character: 2,
|
||||
},
|
||||
}, {
|
||||
id: 2,
|
||||
in_reply_to: 4,
|
||||
id: '02',
|
||||
in_reply_to: '04',
|
||||
patch_set: 2,
|
||||
unresolved: true,
|
||||
line: 1,
|
||||
@@ -270,27 +270,39 @@ suite('gr-comment-api tests', () => {
|
||||
};
|
||||
element._changeComments._comments = {
|
||||
'file/one': [
|
||||
{id: 3, patch_set: 2, side: PARENT, line: 2, updated: makeTime(1)},
|
||||
{id: 4, patch_set: 2, line: 1, updated: makeTime(1)},
|
||||
{
|
||||
id: '03',
|
||||
patch_set: 2,
|
||||
side: PARENT,
|
||||
line: 2,
|
||||
updated: makeTime(1),
|
||||
},
|
||||
{id: '04', patch_set: 2, line: 1, updated: makeTime(1)},
|
||||
],
|
||||
'file/two': [
|
||||
{id: 5, patch_set: 2, line: 2, updated: makeTime(1)},
|
||||
{id: 6, patch_set: 3, line: 2, updated: makeTime(1)},
|
||||
{id: '05', patch_set: 2, line: 2, updated: makeTime(1)},
|
||||
{id: '06', patch_set: 3, line: 2, updated: makeTime(1)},
|
||||
],
|
||||
'file/three': [
|
||||
{
|
||||
id: 7,
|
||||
id: '07',
|
||||
patch_set: 2,
|
||||
side: PARENT,
|
||||
unresolved: true,
|
||||
line: 1,
|
||||
updated: makeTime(1),
|
||||
},
|
||||
{id: 8, patch_set: 3, line: 1, updated: makeTime(1)},
|
||||
{id: '08', patch_set: 3, line: 1, updated: makeTime(1)},
|
||||
],
|
||||
'file/four': [
|
||||
{id: 9, patch_set: 5, side: PARENT, line: 1, updated: makeTime(1)},
|
||||
{id: 10, patch_set: 5, line: 1, updated: makeTime(1)},
|
||||
{
|
||||
id: '09',
|
||||
patch_set: 5,
|
||||
side: PARENT,
|
||||
line: 1,
|
||||
updated: makeTime(1),
|
||||
},
|
||||
{id: '10', patch_set: 5, line: 1, updated: makeTime(1)},
|
||||
],
|
||||
};
|
||||
});
|
||||
@@ -500,7 +512,7 @@ suite('gr-comment-api tests', () => {
|
||||
{
|
||||
comments: [
|
||||
{
|
||||
id: 1,
|
||||
id: '01',
|
||||
patch_set: 2,
|
||||
side: 'PARENT',
|
||||
line: 1,
|
||||
@@ -518,11 +530,11 @@ suite('gr-comment-api tests', () => {
|
||||
patchNum: 2,
|
||||
path: 'file/one',
|
||||
line: 1,
|
||||
rootId: 1,
|
||||
rootId: '01',
|
||||
}, {
|
||||
comments: [
|
||||
{
|
||||
id: 3,
|
||||
id: '03',
|
||||
patch_set: 2,
|
||||
side: 'PARENT',
|
||||
line: 2,
|
||||
@@ -534,19 +546,19 @@ suite('gr-comment-api tests', () => {
|
||||
patchNum: 2,
|
||||
path: 'file/one',
|
||||
line: 2,
|
||||
rootId: 3,
|
||||
rootId: '03',
|
||||
}, {
|
||||
comments: [
|
||||
{
|
||||
id: 4,
|
||||
id: '04',
|
||||
patch_set: 2,
|
||||
line: 1,
|
||||
__path: 'file/one',
|
||||
updated: '2013-02-26 15:01:43.986000000',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
in_reply_to: 4,
|
||||
id: '02',
|
||||
in_reply_to: '04',
|
||||
patch_set: 2,
|
||||
unresolved: true,
|
||||
line: 1,
|
||||
@@ -554,8 +566,8 @@ suite('gr-comment-api tests', () => {
|
||||
updated: '2013-02-26 15:03:43.986000000',
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
in_reply_to: 4,
|
||||
id: '12',
|
||||
in_reply_to: '04',
|
||||
patch_set: 2,
|
||||
line: 1,
|
||||
__path: 'file/one',
|
||||
@@ -566,11 +578,11 @@ suite('gr-comment-api tests', () => {
|
||||
patchNum: 2,
|
||||
path: 'file/one',
|
||||
line: 1,
|
||||
rootId: 4,
|
||||
rootId: '04',
|
||||
}, {
|
||||
comments: [
|
||||
{
|
||||
id: 5,
|
||||
id: '05',
|
||||
patch_set: 2,
|
||||
line: 2,
|
||||
__path: 'file/two',
|
||||
@@ -580,11 +592,11 @@ suite('gr-comment-api tests', () => {
|
||||
patchNum: 2,
|
||||
path: 'file/two',
|
||||
line: 2,
|
||||
rootId: 5,
|
||||
rootId: '05',
|
||||
}, {
|
||||
comments: [
|
||||
{
|
||||
id: 6,
|
||||
id: '06',
|
||||
patch_set: 3,
|
||||
line: 2,
|
||||
__path: 'file/two',
|
||||
@@ -594,11 +606,11 @@ suite('gr-comment-api tests', () => {
|
||||
patchNum: 3,
|
||||
path: 'file/two',
|
||||
line: 2,
|
||||
rootId: 6,
|
||||
rootId: '06',
|
||||
}, {
|
||||
comments: [
|
||||
{
|
||||
id: 7,
|
||||
id: '07',
|
||||
patch_set: 2,
|
||||
side: 'PARENT',
|
||||
unresolved: true,
|
||||
@@ -611,11 +623,11 @@ suite('gr-comment-api tests', () => {
|
||||
patchNum: 2,
|
||||
path: 'file/three',
|
||||
line: 1,
|
||||
rootId: 7,
|
||||
rootId: '07',
|
||||
}, {
|
||||
comments: [
|
||||
{
|
||||
id: 8,
|
||||
id: '08',
|
||||
patch_set: 3,
|
||||
line: 1,
|
||||
__path: 'file/three',
|
||||
@@ -625,11 +637,11 @@ suite('gr-comment-api tests', () => {
|
||||
patchNum: 3,
|
||||
path: 'file/three',
|
||||
line: 1,
|
||||
rootId: 8,
|
||||
rootId: '08',
|
||||
}, {
|
||||
comments: [
|
||||
{
|
||||
id: 9,
|
||||
id: '09',
|
||||
patch_set: 5,
|
||||
side: 'PARENT',
|
||||
line: 1,
|
||||
@@ -641,25 +653,25 @@ suite('gr-comment-api tests', () => {
|
||||
patchNum: 5,
|
||||
path: 'file/four',
|
||||
line: 1,
|
||||
rootId: 9,
|
||||
rootId: '09',
|
||||
}, {
|
||||
comments: [
|
||||
{
|
||||
id: 10,
|
||||
id: '10',
|
||||
patch_set: 5,
|
||||
line: 1,
|
||||
__path: 'file/four',
|
||||
updated: '2013-02-26 15:01:43.986000000',
|
||||
},
|
||||
],
|
||||
rootId: 10,
|
||||
rootId: '10',
|
||||
patchNum: 5,
|
||||
path: 'file/four',
|
||||
line: 1,
|
||||
}, {
|
||||
comments: [
|
||||
{
|
||||
id: 5,
|
||||
id: '05',
|
||||
patch_set: 3,
|
||||
line: 1,
|
||||
__path: 'file/two',
|
||||
@@ -667,14 +679,14 @@ suite('gr-comment-api tests', () => {
|
||||
updated: '2013-02-26 15:03:43.986000000',
|
||||
},
|
||||
],
|
||||
rootId: 5,
|
||||
rootId: '05',
|
||||
patchNum: 3,
|
||||
path: 'file/two',
|
||||
line: 1,
|
||||
}, {
|
||||
comments: [
|
||||
{
|
||||
id: 11,
|
||||
id: '11',
|
||||
patch_set: 2,
|
||||
side: 'PARENT',
|
||||
line: 1,
|
||||
@@ -683,7 +695,7 @@ suite('gr-comment-api tests', () => {
|
||||
updated: '2013-02-26 15:03:43.986000000',
|
||||
},
|
||||
],
|
||||
rootId: 11,
|
||||
rootId: '11',
|
||||
commentSide: 'PARENT',
|
||||
patchNum: 2,
|
||||
path: 'file/one',
|
||||
@@ -698,15 +710,15 @@ suite('gr-comment-api tests', () => {
|
||||
let expectedComments = [
|
||||
{
|
||||
__path: 'file/one',
|
||||
id: 4,
|
||||
id: '04',
|
||||
patch_set: 2,
|
||||
line: 1,
|
||||
updated: '2013-02-26 15:01:43.986000000',
|
||||
},
|
||||
{
|
||||
__path: 'file/one',
|
||||
id: 2,
|
||||
in_reply_to: 4,
|
||||
id: '02',
|
||||
in_reply_to: '04',
|
||||
patch_set: 2,
|
||||
unresolved: true,
|
||||
line: 1,
|
||||
@@ -715,18 +727,18 @@ suite('gr-comment-api tests', () => {
|
||||
{
|
||||
__path: 'file/one',
|
||||
__draft: true,
|
||||
id: 12,
|
||||
in_reply_to: 4,
|
||||
id: '12',
|
||||
in_reply_to: '04',
|
||||
patch_set: 2,
|
||||
line: 1,
|
||||
updated: '2013-02-26 15:02:43.986000000',
|
||||
},
|
||||
];
|
||||
assert.deepEqual(element._changeComments.getCommentsForThread(4),
|
||||
assert.deepEqual(element._changeComments.getCommentsForThread('04'),
|
||||
expectedComments);
|
||||
|
||||
expectedComments = [{
|
||||
id: 11,
|
||||
id: '11',
|
||||
patch_set: 2,
|
||||
side: 'PARENT',
|
||||
line: 1,
|
||||
@@ -735,10 +747,10 @@ suite('gr-comment-api tests', () => {
|
||||
updated: '2013-02-26 15:03:43.986000000',
|
||||
}];
|
||||
|
||||
assert.deepEqual(element._changeComments.getCommentsForThread(11),
|
||||
assert.deepEqual(element._changeComments.getCommentsForThread('11'),
|
||||
expectedComments);
|
||||
|
||||
assert.deepEqual(element._changeComments.getCommentsForThread(1000),
|
||||
assert.deepEqual(element._changeComments.getCommentsForThread('1000'),
|
||||
null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,583 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright (C) 2015 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 '../../../styles/shared-styles.js';
|
||||
import '../gr-rest-api-interface/gr-rest-api-interface.js';
|
||||
import '../gr-storage/gr-storage.js';
|
||||
import '../gr-comment/gr-comment.js';
|
||||
import {dom} 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-comment-thread_html.js';
|
||||
import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
|
||||
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
|
||||
import {appContext} from '../../../services/app-context.js';
|
||||
import {SpecialFilePath} from '../../../constants/constants.js';
|
||||
import {computeDisplayPath} from '../../../utils/path-list-util.js';
|
||||
import {sortComments} from '../../diff/gr-comment-api/gr-comment-api.js';
|
||||
|
||||
const UNRESOLVED_EXPAND_COUNT = 5;
|
||||
const NEWLINE_PATTERN = /\n/g;
|
||||
|
||||
/**
|
||||
* @extends PolymerElement
|
||||
*/
|
||||
class GrCommentThread extends KeyboardShortcutMixin(GestureEventListeners(
|
||||
LegacyElementMixin(PolymerElement))) {
|
||||
// KeyboardShortcutMixin Not used in this element rather other elements tests
|
||||
|
||||
static get template() { return htmlTemplate; }
|
||||
|
||||
static get is() { return 'gr-comment-thread'; }
|
||||
/**
|
||||
* Fired when the thread should be discarded.
|
||||
*
|
||||
* @event thread-discard
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when a comment in the thread is permanently modified.
|
||||
*
|
||||
* @event thread-changed
|
||||
*/
|
||||
|
||||
/**
|
||||
* gr-comment-thread exposes the following attributes that allow a
|
||||
* diff widget like gr-diff to show the thread in the right location:
|
||||
*
|
||||
* line-num:
|
||||
* 1-based line number or undefined if it refers to the entire file.
|
||||
*
|
||||
* comment-side:
|
||||
* "left" or "right". These indicate which of the two diffed versions
|
||||
* the comment relates to. In the case of unified diff, the left
|
||||
* version is the one whose line number column is further to the left.
|
||||
*
|
||||
* range:
|
||||
* The range of text that the comment refers to (start_line,
|
||||
* start_character, end_line, end_character), serialized as JSON. If
|
||||
* set, range's end_line will have the same value as line-num. Line
|
||||
* numbers are 1-based, char numbers are 0-based. The start position
|
||||
* (start_line, start_character) is inclusive, and the end position
|
||||
* (end_line, end_character) is exclusive.
|
||||
*/
|
||||
static get properties() {
|
||||
return {
|
||||
changeNum: String,
|
||||
comments: {
|
||||
type: Array,
|
||||
value() { return []; },
|
||||
},
|
||||
/**
|
||||
* @type {?{start_line: number, start_character: number, end_line: number,
|
||||
* end_character: number}}
|
||||
*/
|
||||
range: {
|
||||
type: Object,
|
||||
reflectToAttribute: true,
|
||||
},
|
||||
keyEventTarget: {
|
||||
type: Object,
|
||||
value() { return document.body; },
|
||||
},
|
||||
commentSide: {
|
||||
type: String,
|
||||
reflectToAttribute: true,
|
||||
},
|
||||
patchNum: String,
|
||||
path: String,
|
||||
projectName: {
|
||||
type: String,
|
||||
observer: '_projectNameChanged',
|
||||
},
|
||||
hasDraft: {
|
||||
type: Boolean,
|
||||
notify: true,
|
||||
reflectToAttribute: true,
|
||||
},
|
||||
isOnParent: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
parentIndex: {
|
||||
type: Number,
|
||||
value: null,
|
||||
},
|
||||
rootId: {
|
||||
type: String,
|
||||
notify: true,
|
||||
computed: '_computeRootId(comments.*)',
|
||||
},
|
||||
/**
|
||||
* If this is true, the comment thread also needs to have the change and
|
||||
* line properties property set
|
||||
*/
|
||||
showFilePath: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
/** Necessary only if showFilePath is true or when used with gr-diff */
|
||||
lineNum: {
|
||||
type: Number,
|
||||
reflectToAttribute: true,
|
||||
},
|
||||
unresolved: {
|
||||
type: Boolean,
|
||||
notify: true,
|
||||
reflectToAttribute: true,
|
||||
},
|
||||
_showActions: Boolean,
|
||||
_lastComment: Object,
|
||||
_orderedComments: Array,
|
||||
_projectConfig: Object,
|
||||
isRobotComment: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true,
|
||||
},
|
||||
showFileName: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
showPatchset: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static get observers() {
|
||||
return [
|
||||
'_commentsChanged(comments.*)',
|
||||
];
|
||||
}
|
||||
|
||||
get keyBindings() {
|
||||
return {
|
||||
'e shift+e': '_handleEKey',
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.reporting = appContext.reportingService;
|
||||
this.flagsService = appContext.flagsService;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
created() {
|
||||
super.created();
|
||||
this.addEventListener('comment-update',
|
||||
e => this._handleCommentUpdate(e));
|
||||
}
|
||||
|
||||
/** @override */
|
||||
attached() {
|
||||
super.attached();
|
||||
this._getLoggedIn().then(loggedIn => {
|
||||
this._showActions = loggedIn;
|
||||
});
|
||||
this._setInitialExpandedState();
|
||||
}
|
||||
|
||||
addOrEditDraft(opt_lineNum, opt_range) {
|
||||
const lastComment = this.comments[this.comments.length - 1] || {};
|
||||
if (lastComment.__draft) {
|
||||
const commentEl = this._commentElWithDraftID(
|
||||
lastComment.id || lastComment.__draftID);
|
||||
commentEl.editing = true;
|
||||
|
||||
// If the comment was collapsed, re-open it to make it clear which
|
||||
// actions are available.
|
||||
commentEl.collapsed = false;
|
||||
} else {
|
||||
const range = opt_range ? opt_range :
|
||||
lastComment ? lastComment.range : undefined;
|
||||
const unresolved = lastComment ? lastComment.unresolved : undefined;
|
||||
this.addDraft(opt_lineNum, range, unresolved);
|
||||
}
|
||||
}
|
||||
|
||||
addDraft(opt_lineNum, opt_range, opt_unresolved) {
|
||||
const draft = this._newDraft(opt_lineNum, opt_range);
|
||||
draft.__editing = true;
|
||||
draft.unresolved = opt_unresolved === false ? opt_unresolved : true;
|
||||
this.push('comments', draft);
|
||||
}
|
||||
|
||||
fireRemoveSelf() {
|
||||
this.dispatchEvent(new CustomEvent('thread-discard',
|
||||
{detail: {rootId: this.rootId}, bubbles: false}));
|
||||
}
|
||||
|
||||
_getDiffUrlForPath(path) {
|
||||
if (this.comments[0].__draft) {
|
||||
return GerritNav.getUrlForDiffById(this.changeNum,
|
||||
this.projectName, path, this.patchNum);
|
||||
}
|
||||
return GerritNav.getUrlForComment(this.changeNum, this.projectName,
|
||||
this.comments[0].id);
|
||||
}
|
||||
|
||||
_getDiffUrlForComment(projectName, changeNum, path, patchNum) {
|
||||
if ((this.comments.length && this.comments[0].side === 'PARENT') ||
|
||||
this.comments[0].__draft) {
|
||||
return GerritNav.getUrlForDiffById(changeNum,
|
||||
projectName, path, patchNum, null, this.lineNum);
|
||||
}
|
||||
return GerritNav.getUrlForComment(this.changeNum, this.projectName,
|
||||
this.comments[0].id);
|
||||
}
|
||||
|
||||
_isPatchsetLevelComment(path) {
|
||||
return path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
|
||||
}
|
||||
|
||||
_computeDisplayPath(path) {
|
||||
const displayPath = computeDisplayPath(path);
|
||||
if (displayPath === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
|
||||
return `Patchset`;
|
||||
}
|
||||
return displayPath;
|
||||
}
|
||||
|
||||
_computeDisplayLine() {
|
||||
if (this.lineNum) return `#${this.lineNum}`;
|
||||
// If range is set, then lineNum equals the end line of the range.
|
||||
if (!this.lineNum && !this.range) {
|
||||
if (this.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
|
||||
return '';
|
||||
}
|
||||
return 'FILE';
|
||||
}
|
||||
if (this.range) return `#${this.range.end_line}`;
|
||||
return '';
|
||||
}
|
||||
|
||||
_getLoggedIn() {
|
||||
return this.$.restAPI.getLoggedIn();
|
||||
}
|
||||
|
||||
_commentsChanged() {
|
||||
this._orderedComments = sortComments(this.comments);
|
||||
this.updateThreadProperties();
|
||||
}
|
||||
|
||||
updateThreadProperties() {
|
||||
if (this._orderedComments.length) {
|
||||
this._lastComment = this._getLastComment();
|
||||
this.unresolved = this._lastComment.unresolved;
|
||||
this.hasDraft = this._lastComment.__draft;
|
||||
this.isRobotComment = !!(this._lastComment.robot_id);
|
||||
}
|
||||
}
|
||||
|
||||
_shouldDisableAction(_showActions, _lastComment) {
|
||||
return !_showActions || !_lastComment || !!_lastComment.__draft;
|
||||
}
|
||||
|
||||
_hideActions(_showActions, _lastComment) {
|
||||
return this._shouldDisableAction(_showActions, _lastComment) ||
|
||||
!!_lastComment.robot_id;
|
||||
}
|
||||
|
||||
_getLastComment() {
|
||||
return this._orderedComments[this._orderedComments.length - 1] || {};
|
||||
}
|
||||
|
||||
_handleEKey(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
|
||||
|
||||
// Don’t preventDefault in this case because it will render the event
|
||||
// useless for other handlers (other gr-comment-thread elements).
|
||||
if (e.detail.keyboardEvent.shiftKey) {
|
||||
this._expandCollapseComments(true);
|
||||
} else {
|
||||
if (this.modifierPressed(e)) { return; }
|
||||
this._expandCollapseComments(false);
|
||||
}
|
||||
}
|
||||
|
||||
_expandCollapseComments(actionIsCollapse) {
|
||||
const comments =
|
||||
this.root.querySelectorAll('gr-comment');
|
||||
for (const comment of comments) {
|
||||
comment.collapsed = actionIsCollapse;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the initial state of the comment thread.
|
||||
* Expands the thread if one of the following is true:
|
||||
* - last {UNRESOLVED_EXPAND_COUNT} comments expanded by default if the
|
||||
* thread is unresolved,
|
||||
* - it's a robot comment.
|
||||
*/
|
||||
_setInitialExpandedState() {
|
||||
if (this._orderedComments) {
|
||||
for (let i = 0; i < this._orderedComments.length; i++) {
|
||||
const comment = this._orderedComments[i];
|
||||
const isRobotComment = !!comment.robot_id;
|
||||
// False if it's an unresolved comment under UNRESOLVED_EXPAND_COUNT.
|
||||
const resolvedThread = !this.unresolved ||
|
||||
this._orderedComments.length - i - 1 >= UNRESOLVED_EXPAND_COUNT;
|
||||
if (comment.collapsed === undefined) {
|
||||
comment.collapsed = !isRobotComment && resolvedThread;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_createReplyComment(content, opt_isEditing,
|
||||
opt_unresolved) {
|
||||
this.reporting.recordDraftInteraction();
|
||||
const reply = this._newReply(
|
||||
this._orderedComments[this._orderedComments.length - 1].id,
|
||||
content,
|
||||
opt_unresolved);
|
||||
|
||||
// If there is currently a comment in an editing state, add an attribute
|
||||
// so that the gr-comment knows not to populate the draft text.
|
||||
for (let i = 0; i < this.comments.length; i++) {
|
||||
if (this.comments[i].__editing) {
|
||||
reply.__otherEditing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (opt_isEditing) {
|
||||
reply.__editing = true;
|
||||
}
|
||||
|
||||
this.push('comments', reply);
|
||||
|
||||
if (!opt_isEditing) {
|
||||
// Allow the reply to render in the dom-repeat.
|
||||
this.async(() => {
|
||||
const commentEl = this._commentElWithDraftID(reply.__draftID);
|
||||
commentEl.save();
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
|
||||
_isDraft(comment) {
|
||||
return !!comment.__draft;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean=} opt_quote
|
||||
*/
|
||||
_processCommentReply(opt_quote) {
|
||||
const comment = this._lastComment;
|
||||
let quoteStr;
|
||||
if (opt_quote) {
|
||||
const msg = comment.message;
|
||||
quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
|
||||
}
|
||||
this._createReplyComment(quoteStr, true, comment.unresolved);
|
||||
}
|
||||
|
||||
_handleCommentReply() {
|
||||
this._processCommentReply();
|
||||
}
|
||||
|
||||
_handleCommentQuote() {
|
||||
this._processCommentReply(true);
|
||||
}
|
||||
|
||||
_handleCommentAck() {
|
||||
this._createReplyComment('Ack', false, false);
|
||||
}
|
||||
|
||||
_handleCommentDone() {
|
||||
this._createReplyComment('Done', false, false);
|
||||
}
|
||||
|
||||
_handleCommentFix(e) {
|
||||
const comment = e.detail.comment;
|
||||
const msg = comment.message;
|
||||
const quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
|
||||
const response = quoteStr + 'Please fix.';
|
||||
this._createReplyComment(response, false, true);
|
||||
}
|
||||
|
||||
_commentElWithDraftID(id) {
|
||||
const els = this.root.querySelectorAll('gr-comment');
|
||||
for (const el of els) {
|
||||
if (el.comment.id === id || el.comment.__draftID === id) {
|
||||
return el;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_newReply(inReplyTo, opt_message, opt_unresolved) {
|
||||
const d = this._newDraft();
|
||||
d.in_reply_to = inReplyTo;
|
||||
if (opt_message != null) {
|
||||
d.message = opt_message;
|
||||
}
|
||||
if (opt_unresolved !== undefined) {
|
||||
d.unresolved = opt_unresolved;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number=} opt_lineNum
|
||||
* @param {!Object=} opt_range
|
||||
*/
|
||||
_newDraft(opt_lineNum, opt_range) {
|
||||
const d = {
|
||||
__draft: true,
|
||||
__draftID: Math.random().toString(36),
|
||||
__date: new Date(),
|
||||
};
|
||||
|
||||
// For replies, always use same meta info as root.
|
||||
if (this.comments && this.comments.length >= 1) {
|
||||
const rootComment = this.comments[0];
|
||||
[
|
||||
'path',
|
||||
'patchNum',
|
||||
'side',
|
||||
'__commentSide',
|
||||
'line',
|
||||
'range',
|
||||
'parent',
|
||||
].forEach(key => {
|
||||
if (rootComment.hasOwnProperty(key)) {
|
||||
d[key] = rootComment[key];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Set meta info for root comment.
|
||||
d.path = this.path;
|
||||
d.patchNum = this.patchNum;
|
||||
d.side = this._getSide(this.isOnParent);
|
||||
d.__commentSide = this.commentSide;
|
||||
|
||||
if (opt_lineNum) {
|
||||
d.line = opt_lineNum;
|
||||
}
|
||||
if (opt_range) {
|
||||
d.range = opt_range;
|
||||
}
|
||||
if (this.parentIndex) {
|
||||
d.parent = this.parentIndex;
|
||||
}
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
_getSide(isOnParent) {
|
||||
if (isOnParent) { return 'PARENT'; }
|
||||
return 'REVISION';
|
||||
}
|
||||
|
||||
_computeRootId(comments) {
|
||||
// Keep the root ID even if the comment was removed, so that notification
|
||||
// to sync will know which thread to remove.
|
||||
if (!comments.base.length) { return this.rootId; }
|
||||
const rootComment = comments.base[0];
|
||||
return rootComment.id || rootComment.__draftID;
|
||||
}
|
||||
|
||||
_handleCommentDiscard(e) {
|
||||
const diffCommentEl = dom(e).rootTarget;
|
||||
const comment = diffCommentEl.comment;
|
||||
const idx = this._indexOf(comment, this.comments);
|
||||
if (idx == -1) {
|
||||
throw Error('Cannot find comment ' +
|
||||
JSON.stringify(diffCommentEl.comment));
|
||||
}
|
||||
this.splice('comments', idx, 1);
|
||||
if (this.comments.length === 0) {
|
||||
this.fireRemoveSelf();
|
||||
}
|
||||
this._handleCommentSavedOrDiscarded(e);
|
||||
|
||||
// Check to see if there are any other open comments getting edited and
|
||||
// set the local storage value to its message value.
|
||||
for (const changeComment of this.comments) {
|
||||
if (changeComment.__editing) {
|
||||
const commentLocation = {
|
||||
changeNum: this.changeNum,
|
||||
patchNum: this.patchNum,
|
||||
path: changeComment.path,
|
||||
line: changeComment.line,
|
||||
};
|
||||
return this.$.storage.setDraftComment(commentLocation,
|
||||
changeComment.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_handleCommentSavedOrDiscarded(e) {
|
||||
this.dispatchEvent(new CustomEvent('thread-changed',
|
||||
{detail: {rootId: this.rootId, path: this.path},
|
||||
bubbles: false}));
|
||||
}
|
||||
|
||||
_handleCommentUpdate(e) {
|
||||
const comment = e.detail.comment;
|
||||
const index = this._indexOf(comment, this.comments);
|
||||
if (index === -1) {
|
||||
// This should never happen: comment belongs to another thread.
|
||||
console.warn('Comment update for another comment thread.');
|
||||
return;
|
||||
}
|
||||
this.set(['comments', index], comment);
|
||||
// Because of the way we pass these comment objects around by-ref, in
|
||||
// combination with the fact that Polymer does dirty checking in
|
||||
// observers, the this.set() call above will not cause a thread update in
|
||||
// some situations.
|
||||
this.updateThreadProperties();
|
||||
}
|
||||
|
||||
_indexOf(comment, arr) {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const c = arr[i];
|
||||
if ((c.__draftID != null && c.__draftID == comment.__draftID) ||
|
||||
(c.id != null && c.id == comment.id)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
_computeHostClass(unresolved) {
|
||||
if (this.isRobotComment) {
|
||||
return 'robotComment';
|
||||
}
|
||||
return unresolved ? 'unresolved' : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the project config when a project name has been provided.
|
||||
*
|
||||
* @param {string} name The project name.
|
||||
*/
|
||||
_projectNameChanged(name) {
|
||||
if (!name) { return; }
|
||||
this.$.restAPI.getProjectConfig(name).then(config => {
|
||||
this._projectConfig = config;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define(GrCommentThread.is, GrCommentThread);
|
||||
@@ -0,0 +1,642 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright (C) 2015 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 '../../../styles/shared-styles';
|
||||
import '../gr-rest-api-interface/gr-rest-api-interface';
|
||||
import '../gr-storage/gr-storage';
|
||||
import '../gr-comment/gr-comment';
|
||||
import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
|
||||
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
|
||||
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
|
||||
import {PolymerElement} from '@polymer/polymer/polymer-element';
|
||||
import {htmlTemplate} from './gr-comment-thread_html';
|
||||
import {
|
||||
CustomKeyboardEvent,
|
||||
KeyboardShortcutMixin,
|
||||
} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
|
||||
import {sortComments} from '../../diff/gr-comment-api/gr-comment-api';
|
||||
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
|
||||
import {appContext} from '../../../services/app-context';
|
||||
import {CommentSide, SpecialFilePath} from '../../../constants/constants';
|
||||
import {computeDisplayPath} from '../../../utils/path-list-util';
|
||||
import {customElement, observe, property} from '@polymer/decorators';
|
||||
import {RestApiService} from '../../../services/services/gr-rest-api/gr-rest-api';
|
||||
import {
|
||||
CommentRange,
|
||||
ConfigInfo,
|
||||
NumericChangeId,
|
||||
PatchSetNum,
|
||||
RepoName,
|
||||
UrlEncodedCommentId,
|
||||
} from '../../../types/common';
|
||||
import {Comment, GrComment, RobotComment} from '../gr-comment/gr-comment';
|
||||
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
|
||||
import {GrStorage, StorageLocation} from '../gr-storage/gr-storage';
|
||||
|
||||
const UNRESOLVED_EXPAND_COUNT = 5;
|
||||
const NEWLINE_PATTERN = /\n/g;
|
||||
|
||||
export interface GrCommentThread {
|
||||
$: {
|
||||
restAPI: RestApiService & Element;
|
||||
storage: GrStorage;
|
||||
};
|
||||
}
|
||||
|
||||
@customElement('gr-comment-thread')
|
||||
export class GrCommentThread extends KeyboardShortcutMixin(
|
||||
GestureEventListeners(LegacyElementMixin(PolymerElement))
|
||||
) {
|
||||
// KeyboardShortcutMixin Not used in this element rather other elements tests
|
||||
|
||||
static get template() {
|
||||
return htmlTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when the thread should be discarded.
|
||||
*
|
||||
* @event thread-discard
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when a comment in the thread is permanently modified.
|
||||
*
|
||||
* @event thread-changed
|
||||
*/
|
||||
|
||||
/**
|
||||
* gr-comment-thread exposes the following attributes that allow a
|
||||
* diff widget like gr-diff to show the thread in the right location:
|
||||
*
|
||||
* line-num:
|
||||
* 1-based line number or undefined if it refers to the entire file.
|
||||
*
|
||||
* comment-side:
|
||||
* "left" or "right". These indicate which of the two diffed versions
|
||||
* the comment relates to. In the case of unified diff, the left
|
||||
* version is the one whose line number column is further to the left.
|
||||
*
|
||||
* range:
|
||||
* The range of text that the comment refers to (start_line,
|
||||
* start_character, end_line, end_character), serialized as JSON. If
|
||||
* set, range's end_line will have the same value as line-num. Line
|
||||
* numbers are 1-based, char numbers are 0-based. The start position
|
||||
* (start_line, start_character) is inclusive, and the end position
|
||||
* (end_line, end_character) is exclusive.
|
||||
*/
|
||||
@property({type: Number})
|
||||
changeNum?: NumericChangeId;
|
||||
|
||||
@property({type: Array})
|
||||
comments: Comment[] = [];
|
||||
|
||||
@property({type: Object, reflectToAttribute: true})
|
||||
range?: CommentRange;
|
||||
|
||||
@property({type: Object})
|
||||
keyEventTarget: HTMLElement = document.body;
|
||||
|
||||
@property({type: String, reflectToAttribute: true})
|
||||
commentSide?: string;
|
||||
|
||||
@property({type: String})
|
||||
patchNum?: PatchSetNum;
|
||||
|
||||
@property({type: String})
|
||||
path?: string;
|
||||
|
||||
@property({type: String, observer: '_projectNameChanged'})
|
||||
projectName?: RepoName;
|
||||
|
||||
@property({type: Boolean, notify: true, reflectToAttribute: true})
|
||||
hasDraft?: boolean;
|
||||
|
||||
@property({type: Boolean})
|
||||
isOnParent = false;
|
||||
|
||||
@property({type: Number})
|
||||
parentIndex: number | null = null;
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
notify: true,
|
||||
computed: '_computeRootId(comments.*)',
|
||||
})
|
||||
rootId?: string;
|
||||
|
||||
@property({type: Boolean})
|
||||
showFilePath = false;
|
||||
|
||||
@property({type: Number, reflectToAttribute: true})
|
||||
lineNum?: number;
|
||||
|
||||
@property({type: Boolean, notify: true, reflectToAttribute: true})
|
||||
unresolved?: boolean;
|
||||
|
||||
@property({type: Boolean})
|
||||
_showActions?: boolean;
|
||||
|
||||
@property({type: Object})
|
||||
_lastComment?: Comment;
|
||||
|
||||
@property({type: Array})
|
||||
_orderedComments: Comment[] = [];
|
||||
|
||||
@property({type: Object})
|
||||
_projectConfig?: ConfigInfo;
|
||||
|
||||
@property({type: Boolean, reflectToAttribute: true})
|
||||
isRobotComment = false;
|
||||
|
||||
@property({type: Boolean})
|
||||
showFileName = true;
|
||||
|
||||
@property({type: Boolean})
|
||||
showPatchset = true;
|
||||
|
||||
get keyBindings() {
|
||||
return {
|
||||
'e shift+e': '_handleEKey',
|
||||
};
|
||||
}
|
||||
|
||||
reporting = appContext.reportingService;
|
||||
|
||||
flagsService = appContext.flagsService;
|
||||
|
||||
/** @override */
|
||||
created() {
|
||||
super.created();
|
||||
this.addEventListener('comment-update', e =>
|
||||
this._handleCommentUpdate(e as CustomEvent)
|
||||
);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
attached() {
|
||||
super.attached();
|
||||
this._getLoggedIn().then(loggedIn => {
|
||||
this._showActions = loggedIn;
|
||||
});
|
||||
this._setInitialExpandedState();
|
||||
}
|
||||
|
||||
addOrEditDraft(lineNum?: number, rangeParam?: CommentRange) {
|
||||
const lastComment = this.comments[this.comments.length - 1] || {};
|
||||
if (lastComment.__draft) {
|
||||
const commentEl = this._commentElWithDraftID(
|
||||
lastComment.id || lastComment.__draftID
|
||||
);
|
||||
if (!commentEl) throw new Error('Failed to find draft.');
|
||||
commentEl.editing = true;
|
||||
|
||||
// If the comment was collapsed, re-open it to make it clear which
|
||||
// actions are available.
|
||||
commentEl.collapsed = false;
|
||||
} else {
|
||||
const range = rangeParam
|
||||
? rangeParam
|
||||
: lastComment
|
||||
? lastComment.range
|
||||
: undefined;
|
||||
const unresolved = lastComment ? lastComment.unresolved : undefined;
|
||||
this.addDraft(lineNum, range, unresolved);
|
||||
}
|
||||
}
|
||||
|
||||
addDraft(lineNum?: number, range?: CommentRange, unresolved?: boolean) {
|
||||
const draft = this._newDraft(lineNum, range);
|
||||
draft.__editing = true;
|
||||
draft.unresolved = unresolved === false ? unresolved : true;
|
||||
this.push('comments', draft);
|
||||
}
|
||||
|
||||
fireRemoveSelf() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('thread-discard', {
|
||||
detail: {rootId: this.rootId},
|
||||
bubbles: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
_getDiffUrlForPath(path: string) {
|
||||
if (!this.changeNum) throw new Error('changeNum is missing');
|
||||
if (!this.projectName) throw new Error('projectName is missing');
|
||||
if (this.comments[0].__draft) {
|
||||
return GerritNav.getUrlForDiffById(
|
||||
this.changeNum,
|
||||
this.projectName,
|
||||
path,
|
||||
this.patchNum
|
||||
);
|
||||
}
|
||||
const id = this.comments[0].id;
|
||||
if (!id) throw new Error('A published comment is missing the id.');
|
||||
return GerritNav.getUrlForComment(this.changeNum, this.projectName, id);
|
||||
}
|
||||
|
||||
_getDiffUrlForComment(
|
||||
projectName: RepoName,
|
||||
changeNum: NumericChangeId,
|
||||
path: string,
|
||||
patchNum: PatchSetNum
|
||||
) {
|
||||
if (
|
||||
(this.comments.length && this.comments[0].side === 'PARENT') ||
|
||||
this.comments[0].__draft
|
||||
) {
|
||||
return GerritNav.getUrlForDiffById(
|
||||
changeNum,
|
||||
projectName,
|
||||
path,
|
||||
patchNum,
|
||||
undefined,
|
||||
this.lineNum
|
||||
);
|
||||
}
|
||||
const id = this.comments[0].id;
|
||||
if (!id) throw new Error('A published comment is missing the id.');
|
||||
if (!this.changeNum) throw new Error('changeNum is missing');
|
||||
if (!this.projectName) throw new Error('projectName is missing');
|
||||
return GerritNav.getUrlForComment(this.changeNum, this.projectName, id);
|
||||
}
|
||||
|
||||
_isPatchsetLevelComment(path: string) {
|
||||
return path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
|
||||
}
|
||||
|
||||
_computeDisplayPath(path: string) {
|
||||
const displayPath = computeDisplayPath(path);
|
||||
if (displayPath === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
|
||||
return 'Patchset';
|
||||
}
|
||||
return displayPath;
|
||||
}
|
||||
|
||||
_computeDisplayLine() {
|
||||
if (this.lineNum) return `#${this.lineNum}`;
|
||||
// If range is set, then lineNum equals the end line of the range.
|
||||
if (!this.lineNum && !this.range) {
|
||||
if (this.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
|
||||
return '';
|
||||
}
|
||||
return 'FILE';
|
||||
}
|
||||
if (this.range) return `#${this.range.end_line}`;
|
||||
return '';
|
||||
}
|
||||
|
||||
_getLoggedIn() {
|
||||
return this.$.restAPI.getLoggedIn();
|
||||
}
|
||||
|
||||
@observe('comments.*')
|
||||
_commentsChanged() {
|
||||
this._orderedComments = sortComments(this.comments);
|
||||
this.updateThreadProperties();
|
||||
}
|
||||
|
||||
updateThreadProperties() {
|
||||
if (this._orderedComments.length) {
|
||||
this._lastComment = this._getLastComment();
|
||||
this.unresolved = this._lastComment.unresolved;
|
||||
this.hasDraft = this._lastComment.__draft;
|
||||
this.isRobotComment = !!(this._lastComment as RobotComment).robot_id;
|
||||
}
|
||||
}
|
||||
|
||||
_shouldDisableAction(_showActions?: boolean, _lastComment?: Comment) {
|
||||
return !_showActions || !_lastComment || !!_lastComment.__draft;
|
||||
}
|
||||
|
||||
_hideActions(_showActions?: boolean, _lastComment?: Comment) {
|
||||
return (
|
||||
this._shouldDisableAction(_showActions, _lastComment) ||
|
||||
!!(_lastComment as RobotComment).robot_id
|
||||
);
|
||||
}
|
||||
|
||||
_getLastComment() {
|
||||
return this._orderedComments[this._orderedComments.length - 1] || {};
|
||||
}
|
||||
|
||||
_handleEKey(e: CustomKeyboardEvent) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don’t preventDefault in this case because it will render the event
|
||||
// useless for other handlers (other gr-comment-thread elements).
|
||||
if (e.detail.keyboardEvent?.shiftKey) {
|
||||
this._expandCollapseComments(true);
|
||||
} else {
|
||||
if (this.modifierPressed(e)) {
|
||||
return;
|
||||
}
|
||||
this._expandCollapseComments(false);
|
||||
}
|
||||
}
|
||||
|
||||
_expandCollapseComments(actionIsCollapse: boolean) {
|
||||
const comments = this.root?.querySelectorAll('gr-comment');
|
||||
if (!comments) return;
|
||||
for (const comment of comments) {
|
||||
comment.collapsed = actionIsCollapse;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the initial state of the comment thread.
|
||||
* Expands the thread if one of the following is true:
|
||||
* - last {UNRESOLVED_EXPAND_COUNT} comments expanded by default if the
|
||||
* thread is unresolved,
|
||||
* - it's a robot comment.
|
||||
*/
|
||||
_setInitialExpandedState() {
|
||||
if (this._orderedComments) {
|
||||
for (let i = 0; i < this._orderedComments.length; i++) {
|
||||
const comment = this._orderedComments[i];
|
||||
const isRobotComment = !!(comment as RobotComment).robot_id;
|
||||
// False if it's an unresolved comment under UNRESOLVED_EXPAND_COUNT.
|
||||
const resolvedThread =
|
||||
!this.unresolved ||
|
||||
this._orderedComments.length - i - 1 >= UNRESOLVED_EXPAND_COUNT;
|
||||
if (comment.collapsed === undefined) {
|
||||
comment.collapsed = !isRobotComment && resolvedThread;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_createReplyComment(
|
||||
content?: string,
|
||||
isEditing?: boolean,
|
||||
unresolved?: boolean
|
||||
) {
|
||||
this.reporting.recordDraftInteraction();
|
||||
const id = this._orderedComments[this._orderedComments.length - 1].id;
|
||||
if (!id) throw new Error('Cannot reply to comment without id.');
|
||||
const reply = this._newReply(id, content, unresolved);
|
||||
|
||||
// If there is currently a comment in an editing state, add an attribute
|
||||
// so that the gr-comment knows not to populate the draft text.
|
||||
for (let i = 0; i < this.comments.length; i++) {
|
||||
if (this.comments[i].__editing) {
|
||||
reply.__otherEditing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
reply.__editing = true;
|
||||
}
|
||||
|
||||
this.push('comments', reply);
|
||||
|
||||
if (!isEditing) {
|
||||
// Allow the reply to render in the dom-repeat.
|
||||
this.async(() => {
|
||||
const commentEl = this._commentElWithDraftID(reply.__draftID);
|
||||
if (commentEl) commentEl.save();
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
|
||||
_isDraft(comment: Comment) {
|
||||
return !!comment.__draft;
|
||||
}
|
||||
|
||||
_processCommentReply(quote?: boolean) {
|
||||
const comment = this._lastComment;
|
||||
if (!comment) throw new Error('Failed to find last comment.');
|
||||
let content = undefined;
|
||||
if (quote) {
|
||||
const msg = comment.message;
|
||||
if (!msg) throw new Error('Quoting empty comment.');
|
||||
content = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
|
||||
}
|
||||
this._createReplyComment(content, true, comment.unresolved);
|
||||
}
|
||||
|
||||
_handleCommentReply() {
|
||||
this._processCommentReply();
|
||||
}
|
||||
|
||||
_handleCommentQuote() {
|
||||
this._processCommentReply(true);
|
||||
}
|
||||
|
||||
_handleCommentAck() {
|
||||
this._createReplyComment('Ack', false, false);
|
||||
}
|
||||
|
||||
_handleCommentDone() {
|
||||
this._createReplyComment('Done', false, false);
|
||||
}
|
||||
|
||||
_handleCommentFix(e: CustomEvent) {
|
||||
const comment = e.detail.comment;
|
||||
const msg = comment.message;
|
||||
const quoted = msg.replace(NEWLINE_PATTERN, '\n> ') as string;
|
||||
const quoteStr = '> ' + quoted + '\n\n';
|
||||
const response = quoteStr + 'Please fix.';
|
||||
this._createReplyComment(response, false, true);
|
||||
}
|
||||
|
||||
_commentElWithDraftID(id?: string): GrComment | null {
|
||||
if (!id) return null;
|
||||
const els = this.root?.querySelectorAll('gr-comment');
|
||||
if (!els) return null;
|
||||
for (const el of els) {
|
||||
if (el.comment?.id === id || el.comment?.__draftID === id) {
|
||||
return el;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_newReply(
|
||||
inReplyTo: UrlEncodedCommentId,
|
||||
message?: string,
|
||||
unresolved?: boolean
|
||||
) {
|
||||
const d = this._newDraft();
|
||||
d.in_reply_to = inReplyTo;
|
||||
if (message !== undefined) {
|
||||
d.message = message;
|
||||
}
|
||||
if (unresolved !== undefined) {
|
||||
d.unresolved = unresolved;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
_newDraft(lineNum?: number, range?: CommentRange) {
|
||||
const d: Comment = {
|
||||
__draft: true,
|
||||
__draftID: Math.random().toString(36),
|
||||
__date: new Date(),
|
||||
};
|
||||
|
||||
// For replies, always use same meta info as root.
|
||||
if (this.comments && this.comments.length >= 1) {
|
||||
const rootComment = this.comments[0];
|
||||
if (rootComment.path !== undefined) d.path = rootComment.path;
|
||||
if (rootComment.patch_set !== undefined)
|
||||
d.patch_set = rootComment.patch_set;
|
||||
if (rootComment.side !== undefined) d.side = rootComment.side;
|
||||
if (rootComment.__commentSide !== undefined)
|
||||
d.__commentSide = rootComment.__commentSide;
|
||||
if (rootComment.line !== undefined) d.line = rootComment.line;
|
||||
if (rootComment.range !== undefined) d.range = rootComment.range;
|
||||
if (rootComment.parent !== undefined) d.parent = rootComment.parent;
|
||||
} else {
|
||||
// Set meta info for root comment.
|
||||
d.path = this.path;
|
||||
d.patch_set = this.patchNum;
|
||||
d.side = this._getSide(this.isOnParent);
|
||||
d.__commentSide = this.commentSide;
|
||||
|
||||
if (lineNum) {
|
||||
d.line = lineNum;
|
||||
}
|
||||
if (range) {
|
||||
d.range = range;
|
||||
}
|
||||
if (this.parentIndex) {
|
||||
d.parent = this.parentIndex;
|
||||
}
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
_getSide(isOnParent: boolean): CommentSide {
|
||||
return isOnParent ? CommentSide.PARENT : CommentSide.REVISION;
|
||||
}
|
||||
|
||||
_computeRootId(comments: PolymerDeepPropertyChange<Comment[], unknown>) {
|
||||
// Keep the root ID even if the comment was removed, so that notification
|
||||
// to sync will know which thread to remove.
|
||||
if (!comments.base.length) {
|
||||
return this.rootId;
|
||||
}
|
||||
const rootComment = comments.base[0];
|
||||
return rootComment.id || rootComment.__draftID;
|
||||
}
|
||||
|
||||
_handleCommentDiscard(e: Event) {
|
||||
if (!this.changeNum) throw new Error('changeNum is missing');
|
||||
if (!this.patchNum) throw new Error('patchNum is missing');
|
||||
const diffCommentEl = (dom(e) as EventApi).rootTarget as GrComment;
|
||||
const comment = diffCommentEl.comment;
|
||||
const idx = this._indexOf(comment, this.comments);
|
||||
if (idx === -1) {
|
||||
throw Error(
|
||||
'Cannot find comment ' + JSON.stringify(diffCommentEl.comment)
|
||||
);
|
||||
}
|
||||
this.splice('comments', idx, 1);
|
||||
if (this.comments.length === 0) {
|
||||
this.fireRemoveSelf();
|
||||
}
|
||||
this._handleCommentSavedOrDiscarded();
|
||||
|
||||
// Check to see if there are any other open comments getting edited and
|
||||
// set the local storage value to its message value.
|
||||
for (const changeComment of this.comments) {
|
||||
if (changeComment.__editing) {
|
||||
const commentLocation: StorageLocation = {
|
||||
changeNum: this.changeNum,
|
||||
patchNum: this.patchNum,
|
||||
path: changeComment.path,
|
||||
line: changeComment.line,
|
||||
};
|
||||
return this.$.storage.setDraftComment(
|
||||
commentLocation,
|
||||
changeComment.message ?? ''
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_handleCommentSavedOrDiscarded() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('thread-changed', {
|
||||
detail: {rootId: this.rootId, path: this.path},
|
||||
bubbles: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
_handleCommentUpdate(e: CustomEvent) {
|
||||
const comment = e.detail.comment;
|
||||
const index = this._indexOf(comment, this.comments);
|
||||
if (index === -1) {
|
||||
// This should never happen: comment belongs to another thread.
|
||||
console.warn('Comment update for another comment thread.');
|
||||
return;
|
||||
}
|
||||
this.set(['comments', index], comment);
|
||||
// Because of the way we pass these comment objects around by-ref, in
|
||||
// combination with the fact that Polymer does dirty checking in
|
||||
// observers, the this.set() call above will not cause a thread update in
|
||||
// some situations.
|
||||
this.updateThreadProperties();
|
||||
}
|
||||
|
||||
_indexOf(comment: Comment | undefined, arr: Comment[]) {
|
||||
if (!comment) return -1;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const c = arr[i];
|
||||
if (
|
||||
(c.__draftID && c.__draftID === comment.__draftID) ||
|
||||
(c.id && c.id === comment.id)
|
||||
) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
_computeHostClass(unresolved?: boolean) {
|
||||
if (this.isRobotComment) {
|
||||
return 'robotComment';
|
||||
}
|
||||
return unresolved ? 'unresolved' : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the project config when a project name has been provided.
|
||||
*
|
||||
* @param name The project name.
|
||||
*/
|
||||
_projectNameChanged(name?: RepoName) {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
this.$.restAPI.getProjectConfig(name).then(config => {
|
||||
this._projectConfig = config;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'gr-comment-thread': GrCommentThread;
|
||||
}
|
||||
}
|
||||
@@ -265,7 +265,7 @@ suite('comment action tests with unresolved thread', () => {
|
||||
updated: '2015-12-08 19:48:33.843000000',
|
||||
path: '/path/to/file.txt',
|
||||
unresolved: true,
|
||||
patchNum: 3,
|
||||
patch_set: 3,
|
||||
__commentSide: 'left',
|
||||
}];
|
||||
flush();
|
||||
@@ -707,7 +707,7 @@ suite('comment action tests with unresolved thread', () => {
|
||||
test('_newDraft with root', () => {
|
||||
const draft = element._newDraft();
|
||||
assert.equal(draft.__commentSide, 'left');
|
||||
assert.equal(draft.patchNum, 3);
|
||||
assert.equal(draft.patch_set, 3);
|
||||
});
|
||||
|
||||
test('_newDraft with no root', () => {
|
||||
@@ -716,7 +716,7 @@ suite('comment action tests with unresolved thread', () => {
|
||||
element.patchNum = 2;
|
||||
const draft = element._newDraft();
|
||||
assert.equal(draft.__commentSide, 'right');
|
||||
assert.equal(draft.patchNum, 2);
|
||||
assert.equal(draft.patch_set, 2);
|
||||
});
|
||||
|
||||
test('new comment gets created', () => {
|
||||
|
||||
@@ -83,13 +83,14 @@ const RESPECTFUL_REVIEW_TIPS = [
|
||||
'When disagreeing, explain the advantage of your approach.',
|
||||
];
|
||||
|
||||
interface Draft {
|
||||
export interface Draft {
|
||||
collapsed?: boolean;
|
||||
__editing?: boolean;
|
||||
__otherEditing?: boolean;
|
||||
__draft?: boolean;
|
||||
__draftID?: number;
|
||||
__draftID?: string;
|
||||
__commentSide?: string;
|
||||
__date?: Date;
|
||||
}
|
||||
|
||||
export type Comment = Draft & CommentInfo;
|
||||
@@ -900,8 +901,9 @@ export class GrComment extends KeyboardShortcutMixin(
|
||||
throw new Error('undefined changeNum or patchNum');
|
||||
}
|
||||
this._showStartRequest();
|
||||
if (!draft.id) throw new Error('Missing id in comment draft.');
|
||||
return this.$.restAPI
|
||||
.deleteDiffDraft(this.changeNum, this.patchNum, draft)
|
||||
.deleteDiffDraft(this.changeNum, this.patchNum, {id: draft.id})
|
||||
.then(result => {
|
||||
if (result.ok) {
|
||||
this._showEndRequest();
|
||||
@@ -1020,10 +1022,11 @@ export class GrComment extends KeyboardShortcutMixin(
|
||||
}
|
||||
if (
|
||||
!this.comment ||
|
||||
!this.comment.id ||
|
||||
this.changeNum === undefined ||
|
||||
this.patchNum === undefined
|
||||
) {
|
||||
throw new Error('undefined comment or changeNum or patchNum');
|
||||
throw new Error('undefined comment or id or changeNum or patchNum');
|
||||
}
|
||||
this.$.restAPI
|
||||
.deleteComment(
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
import {TimeFormat, DateFormat} from '../../../constants/constants';
|
||||
import {assertNever} from '../../../utils/common-util';
|
||||
import {RestApiService} from '../../../services/services/gr-rest-api/gr-rest-api';
|
||||
import {Timestamp} from '../../../types/common';
|
||||
|
||||
const TimeFormats = {
|
||||
TIME_12: 'h:mm A', // 2:14 PM
|
||||
@@ -210,7 +211,7 @@ export class GrDateFormatter extends TooltipMixin(
|
||||
}
|
||||
|
||||
_computeDateStr(
|
||||
dateStr?: string,
|
||||
dateStr?: Timestamp,
|
||||
timeFormat?: string,
|
||||
dateFormat?: DateFormatPair,
|
||||
relative?: boolean,
|
||||
@@ -248,7 +249,7 @@ export class GrDateFormatter extends TooltipMixin(
|
||||
}
|
||||
|
||||
_computeFullDateStr(
|
||||
dateStr?: string,
|
||||
dateStr?: Timestamp,
|
||||
timeFormat?: string,
|
||||
dateFormat?: DateFormatPair
|
||||
) {
|
||||
|
||||
@@ -2616,9 +2616,11 @@ export class GrRestApiInterface
|
||||
|
||||
_setRanges(comments?: CommentInfo[]) {
|
||||
comments = comments || [];
|
||||
comments.sort(
|
||||
(a, b) => parseDate(a.updated).valueOf() - parseDate(b.updated).valueOf()
|
||||
);
|
||||
comments.sort((a, b) => {
|
||||
if (!a.updated) return 1;
|
||||
if (!b.updated) return -1;
|
||||
return parseDate(a.updated).valueOf() - parseDate(b.updated).valueOf();
|
||||
});
|
||||
for (const comment of comments) {
|
||||
this._setRange(comments, comment);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import {CommentRange, PatchSetNum} from '../../../types/common';
|
||||
export interface StorageLocation {
|
||||
changeNum: number;
|
||||
patchNum: PatchSetNum;
|
||||
path: string;
|
||||
path?: string;
|
||||
line?: number;
|
||||
range?: CommentRange;
|
||||
}
|
||||
|
||||
@@ -534,7 +534,7 @@ _describe(
|
||||
export interface CustomKeyboardEvent extends CustomEvent, EventApi {
|
||||
event: CustomKeyboardEvent;
|
||||
detail: {
|
||||
keyboardEvent?: EventApi;
|
||||
keyboardEvent?: CustomKeyboardEvent;
|
||||
// TODO(TS): maybe should mark as optional and check before accessing
|
||||
key: string;
|
||||
};
|
||||
|
||||
@@ -90,13 +90,14 @@ export function stubBaseUrl(newUrl) {
|
||||
export function generateChange(options) {
|
||||
const change = {
|
||||
_number: 42,
|
||||
project: 'testRepo',
|
||||
};
|
||||
const revisionIdStart = 1;
|
||||
const messageIdStart = 1000;
|
||||
// We want to distinguish between empty arrays/objects and undefined
|
||||
// If an option is not set - the appropriate property is not set
|
||||
// If an options is set - the property always set
|
||||
if (typeof options.revisionsCount !== 'undefined') {
|
||||
if (options && typeof options.revisionsCount !== 'undefined') {
|
||||
const revisions = {};
|
||||
for (let i = 0; i < options.revisionsCount; i++) {
|
||||
const revisionId = (i + revisionIdStart).toString(16);
|
||||
@@ -107,7 +108,7 @@ export function generateChange(options) {
|
||||
}
|
||||
change.revisions = revisions;
|
||||
}
|
||||
if (typeof options.messagesCount !== 'undefined') {
|
||||
if (options && typeof options.messagesCount !== 'undefined') {
|
||||
const messages = [];
|
||||
for (let i = 0; i < options.messagesCount; i++) {
|
||||
messages.push({
|
||||
@@ -118,7 +119,7 @@ export function generateChange(options) {
|
||||
}
|
||||
change.messages = messages;
|
||||
}
|
||||
if (options.status) {
|
||||
if (options && options.status) {
|
||||
change.status = options.status;
|
||||
}
|
||||
return change;
|
||||
|
||||
@@ -1049,7 +1049,7 @@ export interface UserConfigInfo {
|
||||
*/
|
||||
export interface CommentInfo extends CommentInput {
|
||||
patch_set?: PatchSetNum;
|
||||
id: UrlEncodedCommentId;
|
||||
id?: UrlEncodedCommentId;
|
||||
path?: string;
|
||||
side?: CommentSide;
|
||||
parent?: number;
|
||||
@@ -1057,7 +1057,7 @@ export interface CommentInfo extends CommentInput {
|
||||
range?: CommentRange;
|
||||
in_reply_to?: UrlEncodedCommentId;
|
||||
message?: string;
|
||||
updated: Timestamp;
|
||||
updated?: Timestamp;
|
||||
author?: AccountInfo;
|
||||
tag?: string;
|
||||
unresolved?: boolean;
|
||||
@@ -1614,7 +1614,7 @@ export interface CommentInput {
|
||||
line?: number;
|
||||
range?: CommentRange;
|
||||
in_reply_to?: UrlEncodedCommentId;
|
||||
updated: Timestamp;
|
||||
updated?: Timestamp;
|
||||
message?: string;
|
||||
tag?: string;
|
||||
unresolved?: boolean;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import {Timestamp} from '../types/common';
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
@@ -20,7 +22,7 @@ const Duration = {
|
||||
DAY: 1000 * 60 * 60 * 24,
|
||||
};
|
||||
|
||||
export function parseDate(dateStr: string) {
|
||||
export function parseDate(dateStr: Timestamp) {
|
||||
// Timestamps are given in UTC and have the format
|
||||
// "'yyyy-mm-dd hh:mm:ss.fffffffff'" where "'ffffffffff'" represents
|
||||
// nanoseconds.
|
||||
|
||||
Reference in New Issue
Block a user