Merge "Load change notes eagerly to omit unparsable changes from results" into stable-3.6
diff --git a/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java b/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
index ad1703d..8ee8fc2 100644
--- a/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
+++ b/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
@@ -14,8 +14,9 @@
package com.google.gerrit.server.mail.send;
+import static com.google.common.base.Preconditions.checkArgument;
+
import com.google.common.io.CharStreams;
-import com.google.common.io.Resources;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.inject.Inject;
@@ -26,6 +27,7 @@
import com.google.template.soy.shared.SoyAstCache;
import java.io.IOException;
import java.io.Reader;
+import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -137,6 +139,8 @@
}
// Otherwise load the template as a resource.
- builder.add(Resources.getResource(logicalPath), logicalPath);
+ URL resource = this.getClass().getClassLoader().getResource(logicalPath);
+ checkArgument(resource != null, "resource %s not found.", logicalPath);
+ builder.add(resource, logicalPath);
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 86ace10..d3f29fc 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -1284,6 +1284,10 @@
if (value.basePatchNum === undefined)
value.basePatchNum = ParentPatchSetNum;
+ if (value.patchNum === undefined) {
+ value.patchNum = computeLatestPatchNum(this._allPatchSets);
+ }
+
const patchChanged = this.hasPatchRangeChanged(value);
let patchNumChanged = this.hasPatchNumChanged(value);
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
index 8664de3..0902fe6 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -21,12 +21,10 @@
import '../../shared/gr-button/gr-button';
import '../../shared/gr-date-formatter/gr-date-formatter';
import '../../shared/gr-formatted-text/gr-formatted-text';
-import '../../../styles/shared-styles';
import '../gr-message-scores/gr-message-scores';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-message_html';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
import {MessageTag, SpecialFilePath} from '../../../constants/constants';
-import {customElement, property, computed, observe} from '@polymer/decorators';
+import {customElement, property, state} from 'lit/decorators';
import {
ChangeInfo,
ServerInfo,
@@ -42,6 +40,7 @@
import {
ChangeMessage,
CommentThread,
+ isFormattedReviewerUpdate,
LabelExtreme,
PATCH_SET_PREFIX_PATTERN,
} from '../../../utils/comment-util';
@@ -54,6 +53,9 @@
computePredecessor,
} from '../../../utils/patch-set-util';
import {isServiceUser, replaceTemplates} from '../../../utils/account-util';
+import {assertIsDefined} from '../../../utils/common-util';
+import {when} from 'lit/directives/when';
+import {FormattedReviewerUpdateInfo} from '../../../types/types';
const UPLOADED_NEW_PATCHSET_PATTERN = /Uploaded patch set (\d+)./;
const MERGED_PATCHSET_PATTERN = /(\d+) is the latest approved patch-set/;
@@ -68,11 +70,7 @@
}
@customElement('gr-message')
-export class GrMessage extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrMessage extends LitElement {
/**
* Fired when this message's reply link is tapped.
*
@@ -98,12 +96,11 @@
changeNum?: NumericChangeId;
@property({type: Object})
- message: ChangeMessage | undefined;
+ message?: ChangeMessage | (ChangeMessage & FormattedReviewerUpdateInfo);
@property({type: Array})
commentThreads: CommentThread[] = [];
- @computed('message')
get author() {
return this.message?.author || this.message?.updated_by;
}
@@ -114,31 +111,8 @@
@property({type: Boolean})
hideAutomated = false;
- @property({
- type: Boolean,
- reflectToAttribute: true,
- computed: '_computeIsHidden(hideAutomated, isAutomated)',
- })
- override hidden = false;
-
- @computed('message')
- get isAutomated() {
- return !!this.message && this._computeIsAutomated(this.message);
- }
-
- @computed('message')
- get showOnBehalfOf() {
- return !!this.message && this._computeShowOnBehalfOf(this.message);
- }
-
- @property({
- type: Boolean,
- computed: '_computeShowReplyButton(message, _loggedIn)',
- })
- showReplyButton = false;
-
@property({type: String})
- projectName?: string;
+ projectName?: RepoName;
/**
* A mapping from label names to objects representing the minimum and
@@ -147,51 +121,23 @@
@property({type: Object})
labelExtremes?: LabelExtreme;
- @property({type: Object})
- _projectConfig?: ConfigInfo;
+ @state()
+ private projectConfig?: ConfigInfo;
@property({type: Boolean})
- _loggedIn = false;
+ loggedIn = false;
- @property({type: Boolean})
- _isAdmin = false;
+ @state()
+ private isAdmin = false;
- @property({type: Boolean})
- _isDeletingChangeMsg = false;
-
- @property({type: Boolean, computed: '_computeExpanded(message.expanded)'})
- _expanded = false;
-
- @property({
- type: String,
- computed:
- '_computeMessageContentExpanded(_expanded, message.message,' +
- ' message.accounts_in_message,' +
- ' message.tag)',
- })
- _messageContentExpanded = '';
-
- @property({
- type: String,
- computed:
- '_computeMessageContentCollapsed(message.message,' +
- ' message.accounts_in_message,' +
- ' message.tag,' +
- ' commentThreads)',
- })
- _messageContentCollapsed = '';
-
- @property({
- type: String,
- computed: '_computeCommentCountText(commentThreads)',
- })
- _commentCountText = '';
+ @state()
+ private isDeletingChangeMsg = false;
private readonly restApiService = getAppContext().restApiService;
constructor() {
super();
- this.addEventListener('click', e => this._handleClick(e));
+ this.addEventListener('click', e => this.handleClick(e));
}
override connectedCallback() {
@@ -200,44 +146,380 @@
this.config = config;
});
this.restApiService.getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
+ this.loggedIn = loggedIn;
});
this.restApiService.getIsAdmin().then(isAdmin => {
- this._isAdmin = !!isAdmin;
+ this.isAdmin = !!isAdmin;
});
}
- @observe('message.expanded')
- _updateExpandedClass(expanded: boolean) {
- if (expanded) {
+ static override get styles() {
+ return css`
+ :host {
+ display: block;
+ position: relative;
+ cursor: pointer;
+ overflow-y: hidden;
+ }
+ :host(.expanded) {
+ cursor: auto;
+ }
+ .collapsed .contentContainer {
+ align-items: center;
+ color: var(--deemphasized-text-color);
+ display: flex;
+ white-space: nowrap;
+ }
+ .contentContainer {
+ padding: var(--spacing-m) var(--spacing-l);
+ }
+ .expanded .contentContainer {
+ background-color: var(--background-color-secondary);
+ }
+ .collapsed .contentContainer {
+ background-color: var(--background-color-primary);
+ }
+ div.serviceUser.expanded div.contentContainer {
+ background-color: var(
+ --background-color-service-user,
+ var(--background-color-secondary)
+ );
+ }
+ div.serviceUser.collapsed div.contentContainer {
+ background-color: var(
+ --background-color-service-user,
+ var(--background-color-primary)
+ );
+ }
+ .name {
+ font-weight: var(--font-weight-bold);
+ }
+ .message {
+ --gr-formatted-text-prose-max-width: 120ch;
+ }
+ .collapsed .message {
+ max-width: none;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ .collapsed .author,
+ .collapsed .content,
+ .collapsed .message,
+ .collapsed .updateCategory,
+ gr-account-chip {
+ display: inline;
+ }
+ gr-button {
+ margin: 0 -4px;
+ }
+ .collapsed gr-thread-list,
+ .collapsed .replyBtn,
+ .collapsed .deleteBtn,
+ .collapsed .hideOnCollapsed,
+ .hideOnOpen {
+ display: none;
+ }
+ .replyBtn {
+ margin-right: var(--spacing-m);
+ }
+ .collapsed .hideOnOpen {
+ display: block;
+ }
+ .collapsed .content {
+ flex: 1;
+ margin-right: var(--spacing-m);
+ min-width: 0;
+ overflow: hidden;
+ }
+ .collapsed .content.messageContent {
+ text-overflow: ellipsis;
+ }
+ .collapsed .dateContainer {
+ position: static;
+ }
+ .collapsed .author {
+ overflow: hidden;
+ color: var(--primary-text-color);
+ margin-right: var(--spacing-s);
+ }
+ .authorLabel {
+ min-width: 130px;
+ --account-max-length: 120px;
+ margin-right: var(--spacing-s);
+ }
+ .expanded .author {
+ cursor: pointer;
+ margin-bottom: var(--spacing-m);
+ }
+ .expanded .content {
+ padding-left: 40px;
+ }
+ .dateContainer {
+ position: absolute;
+ /* right and top values should match .contentContainer padding */
+ right: var(--spacing-l);
+ top: var(--spacing-m);
+ }
+ .dateContainer gr-button {
+ margin-right: var(--spacing-m);
+ color: var(--deemphasized-text-color);
+ }
+ .dateContainer .patchset:before {
+ content: 'Patchset ';
+ }
+ .dateContainer .patchsetDiffButton {
+ margin-right: var(--spacing-m);
+ --gr-button-padding: 0 var(--spacing-m);
+ }
+ span.date {
+ color: var(--deemphasized-text-color);
+ }
+ span.date:hover {
+ text-decoration: underline;
+ }
+ .dateContainer iron-icon {
+ cursor: pointer;
+ vertical-align: top;
+ }
+ .commentsSummary {
+ margin-right: var(--spacing-s);
+ min-width: 115px;
+ }
+ .expanded .commentsSummary {
+ display: none;
+ }
+ .commentsIcon {
+ vertical-align: top;
+ }
+ gr-account-label::part(gr-account-label-text) {
+ font-weight: var(--font-weight-bold);
+ }
+ iron-icon {
+ --iron-icon-height: 20px;
+ --iron-icon-width: 20px;
+ }
+ @media screen and (max-width: 50em) {
+ .expanded .content {
+ padding-left: 0;
+ }
+ .commentsSummary {
+ min-width: 0px;
+ }
+ .authorLabel {
+ width: 100px;
+ }
+ .dateContainer .patchset:before {
+ content: 'PS ';
+ }
+ }
+ `;
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('projectName')) {
+ this.projectNameChanged();
+ }
+ }
+
+ override render() {
+ if (!this.message) return nothing;
+ if (this.hideAutomated && this.computeIsAutomated()) return nothing;
+ this.updateExpandedClass();
+ return html` <div class=${this.computeClass()}>
+ <div class="contentContainer">
+ ${this.renderAuthor()} ${this.renderCommentsSummary()}
+ ${this.renderMessageContent()} ${this.renderReviewerUpdate()}
+ ${this.renderDateContainer()}
+ </div>
+ </div>`;
+ }
+
+ private renderAuthor() {
+ assertIsDefined(this.message, 'message');
+ return html` <div class="author" @click=${this.handleAuthorClick}>
+ ${when(
+ this.computeShowOnBehalfOf(),
+ () => html`
+ <span>
+ <span class="name">${this.message?.real_author?.name}</span>
+ on behalf of
+ </span>
+ `
+ )}
+ <gr-account-label
+ .account=${this.author}
+ class="authorLabel"
+ ></gr-account-label>
+ <gr-message-scores
+ .labelExtremes=${this.labelExtremes}
+ .message=${this.message}
+ .change=${this.change}
+ ></gr-message-scores>
+ </div>`;
+ }
+
+ private renderCommentsSummary() {
+ if (!this.commentThreads?.length) return nothing;
+
+ const commentCountText = pluralize(this.commentThreads.length, 'comment');
+ return html`
+ <div class="commentsSummary">
+ <iron-icon icon="gr-icons:comment" class="commentsIcon"></iron-icon>
+ <span class="numberOfComments">${commentCountText}</span>
+ </div>
+ `;
+ }
+
+ private renderMessageContent() {
+ if (!this.message?.message) return nothing;
+ const messageContentCollapsed =
+ this.computeMessageContent(
+ false,
+ this.message.message.substring(0, 1000),
+ this.message.accounts_in_message,
+ this.message.tag
+ ) || this.patchsetCommentSummary();
+ return html` <div class="content messageContent">
+ <div class="message hideOnOpen">${messageContentCollapsed}</div>
+ ${this.renderExpandedMessageContent()}
+ </div>`;
+ }
+
+ private renderExpandedMessageContent() {
+ if (!this.message?.expanded) return nothing;
+ const messageContentExpanded = this.computeMessageContent(
+ true,
+ this.message.message,
+ this.message.accounts_in_message,
+ this.message.tag
+ );
+ return html`
+ <gr-formatted-text
+ noTrailingMargin
+ class="message hideOnCollapsed"
+ .content=${messageContentExpanded}
+ .config=${this.projectConfig?.commentlinks}
+ ></gr-formatted-text>
+ ${when(messageContentExpanded, () => this.renderActionContainer())}
+ <gr-thread-list
+ ?hidden=${!this.commentThreads.length}
+ .threads=${this.commentThreads}
+ hide-dropdown
+ show-comment-context
+ .messageId=${this.message.id}
+ >
+ </gr-thread-list>
+ `;
+ }
+
+ private renderActionContainer() {
+ if (!this.computeShowReplyButton()) return nothing;
+ return html` <div class="replyActionContainer">
+ <gr-button class="replyBtn" link="" @click=${this.handleReplyTap}>
+ Reply
+ </gr-button>
+ ${when(
+ this.isAdmin,
+ () => html`
+ <gr-button
+ ?disabled=${this.isDeletingChangeMsg}
+ class="deleteBtn"
+ link=""
+ @click=${this.handleDeleteMessage}
+ >
+ Delete
+ </gr-button>
+ `
+ )}
+ </div>`;
+ }
+
+ private renderReviewerUpdate() {
+ assertIsDefined(this.message, 'message');
+ if (!isFormattedReviewerUpdate(this.message)) return;
+ return html` <div class="content">
+ ${this.message.updates.map(update => this.renderMessageUpdate(update))}
+ </div>`;
+ }
+
+ private renderMessageUpdate(update: {
+ message: string;
+ reviewers: AccountInfo[];
+ }) {
+ return html`<div class="updateCategory">
+ ${update.message}
+ ${update.reviewers.map(
+ reviewer => html`
+ <gr-account-chip .account=${reviewer} .change=${this.change}>
+ </gr-account-chip>
+ `
+ )}
+ </div>`;
+ }
+
+ private renderDateContainer() {
+ return html`<span class="dateContainer">
+ ${this.renderDiffButton()}
+ ${when(
+ this.message?._revision_number,
+ () => html`
+ <span class="patchset">${this.message?._revision_number} |</span>
+ `
+ )}
+ ${when(
+ this.message?.id,
+ () => html`
+ <span class="date" @click=${this.handleAnchorClick}>
+ <gr-date-formatter
+ withTooltip
+ showDateAndTime
+ .dateStr=${this.message?.date}
+ ></gr-date-formatter>
+ </span>
+ `,
+ () => html`
+ <span class="date">
+ <gr-date-formatter
+ withTooltip
+ showDateAndTime
+ .dateStr=${this.message?.date}
+ ></gr-date-formatter>
+ </span>
+ `
+ )}
+ <iron-icon
+ id="expandToggle"
+ @click=${this.toggleExpanded}
+ title="Toggle expanded state"
+ icon=${this.computeExpandToggleIcon()}
+ ></iron-icon>
+ </span>`;
+ }
+
+ private renderDiffButton() {
+ if (!this.showViewDiffButton()) return nothing;
+ return html` <gr-button
+ class="patchsetDiffButton"
+ @click=${this.handleViewPatchsetDiff}
+ link
+ >
+ View Diff
+ </gr-button>`;
+ }
+
+ private updateExpandedClass() {
+ if (this.message?.expanded) {
this.classList.add('expanded');
} else {
this.classList.remove('expanded');
}
}
- _computeCommentCountText(commentThreads?: CommentThread[]) {
- if (!commentThreads?.length) {
- return undefined;
- }
-
- return pluralize(commentThreads.length, 'comment');
- }
-
- _computeMessageContentExpanded(
- expanded: boolean,
- content?: string,
- accountsInMessage?: AccountInfo[],
- tag?: ReviewInputTag
- ) {
- if (!expanded) return '';
- return this._computeMessageContent(true, content, accountsInMessage, tag);
- }
-
- _patchsetCommentSummary(commentThreads: CommentThread[] = []) {
+ // Private but used in tests.
+ patchsetCommentSummary() {
const id = this.message?.id;
if (!id) return '';
- const patchsetThreads = commentThreads.filter(
+ const patchsetThreads = (this.commentThreads ?? []).filter(
thread => thread.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS
);
for (const thread of patchsetThreads) {
@@ -258,45 +540,29 @@
return '';
}
- _computeMessageContentCollapsed(
- content?: string,
- accountsInMessage?: AccountInfo[],
- tag?: ReviewInputTag,
- commentThreads?: CommentThread[]
- ) {
- // Content is under text-overflow, so it's always shorten
- const shortenedContent = content?.substring(0, 1000);
- const summary = this._computeMessageContent(
- false,
- shortenedContent,
- accountsInMessage,
- tag
- );
- if (summary || !commentThreads) return summary;
- return this._patchsetCommentSummary(commentThreads);
- }
-
- _showViewDiffButton(message?: ChangeMessage) {
+ private showViewDiffButton() {
return (
- this._isNewPatchsetTag(message?.tag) || this._isMergePatchset(message)
+ this.isNewPatchsetTag(this.message?.tag) ||
+ this.isMergePatchset(this.message)
);
}
- _isMergePatchset(message?: ChangeMessage) {
+ private isMergePatchset(message?: ChangeMessage) {
return (
message?.tag === MessageTag.TAG_MERGED &&
message?.message.match(MERGED_PATCHSET_PATTERN)
);
}
- _isNewPatchsetTag(tag?: ReviewInputTag) {
+ private isNewPatchsetTag(tag?: ReviewInputTag) {
return (
tag === MessageTag.TAG_NEW_PATCHSET ||
tag === MessageTag.TAG_NEW_WIP_PATCHSET
);
}
- _handleViewPatchsetDiff(e: Event) {
+ // Private but used in tests
+ handleViewPatchsetDiff(e: Event) {
if (!this.message || !this.change) return;
let patchNum: PatchSetNum;
let basePatchNum: PatchSetNum;
@@ -323,14 +589,15 @@
e.stopPropagation();
}
- _computeMessageContent(
+ // private but used in tests
+ computeMessageContent(
isExpanded: boolean,
content?: string,
accountsInMessage?: AccountInfo[],
tag?: ReviewInputTag
) {
if (!content) return '';
- const isNewPatchSet = this._isNewPatchsetTag(tag);
+ const isNewPatchSet = this.isNewPatchsetTag(tag);
if (accountsInMessage) {
content = replaceTemplates(content, accountsInMessage, this.config);
@@ -371,74 +638,68 @@
return mappedLines.join('\n').trim();
}
- _computeAuthor(message: ChangeMessage) {
- return message.author || message.updated_by;
- }
-
- _computeShowOnBehalfOf(message: ChangeMessage) {
- const author = this._computeAuthor(message);
+ // private but used in tests
+ computeShowOnBehalfOf() {
+ if (!this.message) return false;
return !!(
- author &&
- message.real_author &&
- author._account_id !== message.real_author._account_id
+ this.author &&
+ this.message.real_author &&
+ this.author._account_id !== this.message.real_author._account_id
);
}
- _computeShowReplyButton(message?: ChangeMessage, loggedIn?: boolean) {
+ // private but used in tests.
+ computeShowReplyButton() {
return (
- message &&
- !!message.message &&
- loggedIn &&
- !this._computeIsAutomated(message)
+ !!this.message &&
+ !!this.message.message &&
+ this.loggedIn &&
+ !this.computeIsAutomated()
);
}
- _computeExpanded(expanded: boolean) {
- return expanded;
- }
-
- _handleClick(e: Event) {
- if (this.message?.expanded) {
+ private handleClick(e: Event) {
+ if (!this.message || this.message?.expanded) {
return;
}
e.stopPropagation();
- this.set('message.expanded', true);
+ this.message.expanded = true;
+ this.requestUpdate();
}
- _handleAuthorClick(e: Event) {
- if (!this.message?.expanded) {
+ private handleAuthorClick(e: Event) {
+ if (!this.message || !this.message?.expanded) {
return;
}
e.stopPropagation();
- this.set('message.expanded', false);
+ this.message.expanded = false;
+ this.requestUpdate();
}
- _computeIsAutomated(message: ChangeMessage) {
+ // private but used in tests.
+ computeIsAutomated() {
return !!(
- message.reviewer ||
- this._computeIsReviewerUpdate(message) ||
- (message.tag && message.tag.startsWith('autogenerated'))
+ this.message?.reviewer ||
+ this.computeIsReviewerUpdate() ||
+ (this.message?.tag && this.message.tag.startsWith('autogenerated'))
);
}
- _computeIsHidden(hideAutomated: boolean, isAutomated: boolean) {
- return hideAutomated && isAutomated;
+ private computeIsReviewerUpdate() {
+ return this.message?.type === 'REVIEWER_UPDATE';
}
- _computeIsReviewerUpdate(message: ChangeMessage) {
- return message.type === 'REVIEWER_UPDATE';
- }
-
- _computeClass(expanded?: boolean, author?: AccountInfo) {
+ private computeClass() {
+ const expanded = this.message?.expanded;
const classes = [];
classes.push(expanded ? 'expanded' : 'collapsed');
- if (isServiceUser(author)) classes.push('serviceUser');
+ if (isServiceUser(this.author)) classes.push('serviceUser');
return classes.join(' ');
}
- _handleAnchorClick(e: Event) {
+ private handleAnchorClick(e: Event) {
e.preventDefault();
- // The element which triggers _handleAnchorClick is rendered only if
+ // The element which triggers handleAnchorClick is rendered only if
// message.id defined: the element is wrapped in dom-if if="[[message.id]]"
const detail: MessageAnchorTapDetail = {
id: this.message!.id,
@@ -452,7 +713,7 @@
);
}
- _handleReplyTap(e: Event) {
+ private handleReplyTap(e: Event) {
e.preventDefault();
this.dispatchEvent(
new CustomEvent('reply', {
@@ -463,14 +724,14 @@
);
}
- _handleDeleteMessage(e: Event) {
+ private handleDeleteMessage(e: Event) {
e.preventDefault();
if (!this.message || !this.message.id || !this.changeNum) return;
- this._isDeletingChangeMsg = true;
+ this.isDeletingChangeMsg = true;
this.restApiService
.deleteChangeCommitMessage(this.changeNum, this.message.id)
.then(() => {
- this._isDeletingChangeMsg = false;
+ this.isDeletingChangeMsg = false;
this.dispatchEvent(
new CustomEvent('change-message-deleted', {
detail: {message: this.message},
@@ -481,23 +742,25 @@
});
}
- @observe('projectName')
- _projectNameChanged(name?: string) {
- if (!name) {
- this._projectConfig = undefined;
+ private projectNameChanged() {
+ if (!this.projectName) {
+ this.projectConfig = undefined;
return;
}
- this.restApiService.getProjectConfig(name as RepoName).then(config => {
- this._projectConfig = config;
+ this.restApiService.getProjectConfig(this.projectName).then(config => {
+ this.projectConfig = config;
});
}
- _computeExpandToggleIcon(expanded: boolean) {
- return expanded ? 'gr-icons:expand-less' : 'gr-icons:expand-more';
+ private computeExpandToggleIcon() {
+ return this.message?.expanded
+ ? 'gr-icons:expand-less'
+ : 'gr-icons:expand-more';
}
- _toggleExpanded(e: Event) {
+ private toggleExpanded(e: Event) {
e.stopPropagation();
- this.set('message.expanded', !this.message?.expanded);
+ if (!this.message) return;
+ this.message = {...this.message, expanded: !this.message.expanded};
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
deleted file mode 100644
index 70e6381..0000000
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
+++ /dev/null
@@ -1,307 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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 {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style>
- :host {
- display: block;
- position: relative;
- cursor: pointer;
- overflow-y: hidden;
- }
- :host(.expanded) {
- cursor: auto;
- }
- .collapsed .contentContainer {
- align-items: center;
- color: var(--deemphasized-text-color);
- display: flex;
- white-space: nowrap;
- }
- .contentContainer {
- padding: var(--spacing-m) var(--spacing-l);
- }
- .expanded .contentContainer {
- background-color: var(--background-color-secondary);
- }
- .collapsed .contentContainer {
- background-color: var(--background-color-primary);
- }
- div.serviceUser.expanded div.contentContainer {
- background-color: var(
- --background-color-service-user,
- var(--background-color-secondary)
- );
- }
- div.serviceUser.collapsed div.contentContainer {
- background-color: var(
- --background-color-service-user,
- var(--background-color-primary)
- );
- }
- .name {
- font-weight: var(--font-weight-bold);
- }
- .message {
- --gr-formatted-text-prose-max-width: 120ch;
- }
- .collapsed .message {
- max-width: none;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .collapsed .author,
- .collapsed .content,
- .collapsed .message,
- .collapsed .updateCategory,
- gr-account-chip {
- display: inline;
- }
- gr-button {
- margin: 0 -4px;
- }
- .collapsed gr-thread-list,
- .collapsed .replyBtn,
- .collapsed .deleteBtn,
- .collapsed .hideOnCollapsed,
- .hideOnOpen {
- display: none;
- }
- .replyBtn {
- margin-right: var(--spacing-m);
- }
- .collapsed .hideOnOpen {
- display: block;
- }
- .collapsed .content {
- flex: 1;
- margin-right: var(--spacing-m);
- min-width: 0;
- overflow: hidden;
- }
- .collapsed .content.messageContent {
- text-overflow: ellipsis;
- }
- .collapsed .dateContainer {
- position: static;
- }
- .collapsed .author {
- overflow: hidden;
- color: var(--primary-text-color);
- margin-right: var(--spacing-s);
- }
- .authorLabel {
- min-width: 130px;
- --account-max-length: 120px;
- margin-right: var(--spacing-s);
- }
- .expanded .author {
- cursor: pointer;
- margin-bottom: var(--spacing-m);
- }
- .expanded .content {
- padding-left: 40px;
- }
- .dateContainer {
- position: absolute;
- /* right and top values should match .contentContainer padding */
- right: var(--spacing-l);
- top: var(--spacing-m);
- }
- .dateContainer gr-button {
- margin-right: var(--spacing-m);
- color: var(--deemphasized-text-color);
- }
- .dateContainer .patchset:before {
- content: 'Patchset ';
- }
- .dateContainer .patchsetDiffButton {
- margin-right: var(--spacing-m);
- --gr-button-padding: 0 var(--spacing-m);
- }
- span.date {
- color: var(--deemphasized-text-color);
- }
- span.date:hover {
- text-decoration: underline;
- }
- .dateContainer iron-icon {
- cursor: pointer;
- vertical-align: top;
- }
- .commentsSummary {
- margin-right: var(--spacing-s);
- min-width: 115px;
- }
- .expanded .commentsSummary {
- display: none;
- }
- .commentsIcon {
- vertical-align: top;
- }
- gr-account-label::part(gr-account-label-text) {
- font-weight: var(--font-weight-bold);
- }
- iron-icon {
- --iron-icon-height: 20px;
- --iron-icon-width: 20px;
- }
- @media screen and (max-width: 50em) {
- .expanded .content {
- padding-left: 0;
- }
- .commentsSummary {
- min-width: 0px;
- }
- .authorLabel {
- width: 100px;
- }
- .dateContainer .patchset:before {
- content: 'PS ';
- }
- }
- </style>
- <div class$="[[_computeClass(_expanded, author)]]">
- <div class="contentContainer">
- <div class="author" on-click="_handleAuthorClick">
- <span hidden$="[[!showOnBehalfOf]]">
- <span class="name">[[message.real_author.name]]</span>
- on behalf of
- </span>
- <gr-account-label
- account="[[author]]"
- class="authorLabel"
- ></gr-account-label>
- <gr-message-scores
- label-extremes="[[labelExtremes]]"
- message="[[message]]"
- change="[[change]]"
- ></gr-message-scores>
- </div>
- <template is="dom-if" if="[[_commentCountText]]">
- <div class="commentsSummary">
- <iron-icon icon="gr-icons:comment" class="commentsIcon"></iron-icon>
- <span class="numberOfComments">[[_commentCountText]]</span>
- </div>
- </template>
- <template is="dom-if" if="[[message.message]]">
- <div class="content messageContent">
- <div class="message hideOnOpen">[[_messageContentCollapsed]]</div>
- <template is="dom-if" if="[[_expanded]]">
- <gr-formatted-text
- noTrailingMargin
- class="message hideOnCollapsed"
- content="[[_messageContentExpanded]]"
- config="[[_projectConfig.commentlinks]]"
- ></gr-formatted-text>
- <template is="dom-if" if="[[_messageContentExpanded]]">
- <div
- class="replyActionContainer"
- hidden$="[[!showReplyButton]]"
- hidden=""
- >
- <gr-button
- class="replyBtn"
- link=""
- small=""
- on-click="_handleReplyTap"
- >
- Reply
- </gr-button>
- <gr-button
- disabled$="[[_isDeletingChangeMsg]]"
- class="deleteBtn"
- hidden$="[[!_isAdmin]]"
- hidden=""
- link=""
- small=""
- on-click="_handleDeleteMessage"
- >
- Delete
- </gr-button>
- </div>
- </template>
- <gr-thread-list
- hidden$="[[!commentThreads.length]]"
- threads="[[commentThreads]]"
- hide-dropdown
- show-comment-context
- message-id="[[message.id]]"
- >
- </gr-thread-list>
- </template>
- </div>
- </template>
- <template is="dom-if" if="[[_computeIsReviewerUpdate(message)]]">
- <div class="content">
- <template is="dom-repeat" items="[[message.updates]]" as="update">
- <div class="updateCategory">
- [[update.message]]
- <template
- is="dom-repeat"
- items="[[update.reviewers]]"
- as="reviewer"
- >
- <gr-account-chip account="[[reviewer]]" change="[[change]]">
- </gr-account-chip>
- </template>
- </div>
- </template>
- </div>
- </template>
- <span class="dateContainer">
- <template is="dom-if" if="[[_showViewDiffButton(message)]]">
- <gr-button
- class="patchsetDiffButton"
- on-click="_handleViewPatchsetDiff"
- link
- >
- View Diff
- </gr-button>
- </template>
- <template is="dom-if" if="[[message._revision_number]]">
- <span class="patchset">[[message._revision_number]] |</span>
- </template>
- <template is="dom-if" if="[[!message.id]]">
- <span class="date">
- <gr-date-formatter
- withTooltip
- showDateAndTime
- date-str="[[message.date]]"
- ></gr-date-formatter>
- </span>
- </template>
- <template is="dom-if" if="[[message.id]]">
- <span class="date" on-click="_handleAnchorClick">
- <gr-date-formatter
- withTooltip
- showDateAndTime
- date-str="[[message.date]]"
- ></gr-date-formatter>
- </span>
- </template>
- <iron-icon
- id="expandToggle"
- on-click="_toggleExpanded"
- title="Toggle expanded state"
- icon="[[_computeExpandToggleIcon(_expanded)]]"
- ></iron-icon>
- </span>
- </div>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
index ffe59f0..bbe39ff 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
@@ -51,8 +51,8 @@
import {GrButton} from '../../shared/gr-button/gr-button';
import {CommentSide} from '../../../constants/constants';
import {SinonStubbedMember} from 'sinon';
-
-const basicFixture = fixtureFromElement('gr-message');
+import {html} from 'lit';
+import {fixture} from '@open-wc/testing-helpers';
suite('gr-message tests', () => {
let element: GrMessage;
@@ -60,8 +60,7 @@
suite('when admin and logged in', () => {
setup(async () => {
stubRestApi('getIsAdmin').returns(Promise.resolve(true));
- element = basicFixture.instantiate();
- await flush();
+ element = await fixture<GrMessage>(html`<gr-message></gr-message>`);
});
test('reply event', async () => {
@@ -85,9 +84,7 @@
promise.resolve();
});
await flush();
- assert.isFalse(
- queryAndAssert<HTMLElement>(element, '.replyActionContainer').hidden
- );
+ assert.isOk(query<HTMLElement>(element, '.replyActionContainer'));
tap(queryAndAssert(element, '.replyBtn'));
await promise;
});
@@ -106,9 +103,9 @@
_revision_number: 1 as PatchSetNum,
expanded: true,
};
+ await element.updateComplete;
- await flush();
- assert.isFalse(queryAndAssert<HTMLElement>(element, '.deleteBtn').hidden);
+ assert.isOk(query<HTMLElement>(element, '.deleteBtn'));
});
test('delete change message', async () => {
@@ -126,11 +123,13 @@
_revision_number: 1 as PatchSetNum,
expanded: true,
};
+ await element.updateComplete;
const promise = mockPromise();
element.addEventListener(
'change-message-deleted',
- (e: CustomEvent<ChangeMessageDeletedEventDetail>) => {
+ async (e: CustomEvent<ChangeMessageDeletedEventDetail>) => {
+ await element.updateComplete;
assert.deepEqual(e.detail.message, element.message);
assert.isFalse(
queryAndAssert<GrButton>(element, '.deleteBtn').disabled
@@ -138,82 +137,192 @@
promise.resolve();
}
);
- await flush();
tap(queryAndAssert(element, '.deleteBtn'));
+ await element.updateComplete;
assert.isTrue(queryAndAssert<GrButton>(element, '.deleteBtn').disabled);
await promise;
});
- test('autogenerated prefix hiding', () => {
+ test('autogenerated prefix hiding', async () => {
element.message = {
...createChangeMessage(),
tag: 'autogenerated:gerrit:test' as ReviewInputTag,
expanded: false,
};
+ await element.updateComplete;
- assert.isTrue(element.isAutomated);
- assert.isFalse(element.hidden);
+ assert.isTrue(element.computeIsAutomated());
+ expect(element).shadowDom.to.equal(/* HTML */ `<div class="collapsed">
+ <div class="contentContainer">
+ <div class="author">
+ <gr-account-label class="authorLabel"> </gr-account-label>
+ <gr-message-scores> </gr-message-scores>
+ </div>
+ <div class="content messageContent">
+ <div class="hideOnOpen message">
+ This is a message with id cm_id_1
+ </div>
+ </div>
+ <span class="dateContainer">
+ <span class="date">
+ <gr-date-formatter showdateandtime="" withtooltip="">
+ </gr-date-formatter>
+ </span>
+ <iron-icon
+ icon="gr-icons:expand-more"
+ id="expandToggle"
+ title="Toggle expanded state"
+ >
+ </iron-icon>
+ </span>
+ </div>
+ </div>`);
element.hideAutomated = true;
+ await element.updateComplete;
- assert.isTrue(element.hidden);
+ expect(element).shadowDom.to.equal(/* HTML */ '');
});
- test('reviewer message treated as autogenerated', () => {
+ test('reviewer message treated as autogenerated', async () => {
element.message = {
...createChangeMessage(),
tag: 'autogenerated:gerrit:test' as ReviewInputTag,
reviewer: {},
expanded: false,
};
+ await element.updateComplete;
- assert.isTrue(element.isAutomated);
- assert.isFalse(element.hidden);
+ assert.isTrue(element.computeIsAutomated());
+ expect(element).shadowDom.to.equal(/* HTML */ `<div class="collapsed">
+ <div class="contentContainer">
+ <div class="author">
+ <gr-account-label class="authorLabel"> </gr-account-label>
+ <gr-message-scores> </gr-message-scores>
+ </div>
+ <div class="content messageContent">
+ <div class="hideOnOpen message">
+ This is a message with id cm_id_1
+ </div>
+ </div>
+ <span class="dateContainer">
+ <span class="date">
+ <gr-date-formatter showdateandtime="" withtooltip="">
+ </gr-date-formatter>
+ </span>
+ <iron-icon
+ icon="gr-icons:expand-more"
+ id="expandToggle"
+ title="Toggle expanded state"
+ >
+ </iron-icon>
+ </span>
+ </div>
+ </div>`);
element.hideAutomated = true;
+ await element.updateComplete;
- assert.isTrue(element.hidden);
+ expect(element).shadowDom.to.equal(/* HTML */ '');
});
- test('batch reviewer message treated as autogenerated', () => {
+ test('batch reviewer message treated as autogenerated', async () => {
element.message = {
...createChangeMessage(),
type: 'REVIEWER_UPDATE',
reviewer: {},
expanded: false,
+ updates: [],
};
+ await element.updateComplete;
- assert.isTrue(element.isAutomated);
- assert.isFalse(element.hidden);
+ assert.isTrue(element.computeIsAutomated());
+ expect(element).shadowDom.to.equal(/* HTML */ `<div class="collapsed">
+ <div class="contentContainer">
+ <div class="author">
+ <gr-account-label class="authorLabel"> </gr-account-label>
+ <gr-message-scores> </gr-message-scores>
+ </div>
+ <div class="content messageContent">
+ <div class="hideOnOpen message">
+ This is a message with id cm_id_1
+ </div>
+ </div>
+ <div class="content"></div>
+ <span class="dateContainer">
+ <span class="date">
+ <gr-date-formatter showdateandtime="" withtooltip="">
+ </gr-date-formatter>
+ </span>
+ <iron-icon
+ icon="gr-icons:expand-more"
+ id="expandToggle"
+ title="Toggle expanded state"
+ >
+ </iron-icon>
+ </span>
+ </div>
+ </div>`);
element.hideAutomated = true;
+ await element.updateComplete;
- assert.isTrue(element.hidden);
+ expect(element).shadowDom.to.equal(/* HTML */ '');
});
- test('tag that is not autogenerated prefix does not hide', () => {
+ test('tag that is not autogenerated prefix does not hide', async () => {
element.message = {
...createChangeMessage(),
tag: 'something' as ReviewInputTag,
expanded: false,
};
+ await element.updateComplete;
- assert.isFalse(element.isAutomated);
- assert.isFalse(element.hidden);
+ assert.isFalse(element.computeIsAutomated());
+ const rendered = /* HTML */ `<div class="collapsed">
+ <div class="contentContainer">
+ <div class="author">
+ <gr-account-label class="authorLabel"> </gr-account-label>
+ <gr-message-scores> </gr-message-scores>
+ </div>
+ <div class="content messageContent">
+ <div class="hideOnOpen message">
+ This is a message with id cm_id_1
+ </div>
+ </div>
+ <span class="dateContainer">
+ <span class="date">
+ <gr-date-formatter showdateandtime="" withtooltip="">
+ </gr-date-formatter>
+ </span>
+ <iron-icon
+ icon="gr-icons:expand-more"
+ id="expandToggle"
+ title="Toggle expanded state"
+ >
+ </iron-icon>
+ </span>
+ </div>
+ </div>`;
+ expect(element).shadowDom.to.equal(rendered);
element.hideAutomated = true;
+ await element.updateComplete;
+ console.error(element.computeIsAutomated());
- assert.isFalse(element.hidden);
+ expect(element).shadowDom.to.equal(rendered);
});
test('reply button hidden unless logged in', () => {
- const message = {
+ element.message = {
...createChangeMessage(),
message: 'Uploaded patch set 1.',
expanded: false,
};
- assert.isFalse(element._computeShowReplyButton(message, false));
- assert.isTrue(element._computeShowReplyButton(message, true));
+ element.loggedIn = false;
+ assert.isFalse(element.computeShowReplyButton());
+ element.loggedIn = true;
+ assert.isTrue(element.computeShowReplyButton());
});
test('_computeShowOnBehalfOf', () => {
@@ -222,29 +331,32 @@
message: '...',
expanded: false,
};
- assert.isNotOk(element._computeShowOnBehalfOf(message));
+ element.message = message;
+ assert.isNotOk(element.computeShowOnBehalfOf());
message.author = {_account_id: 1115495 as AccountId};
- assert.isNotOk(element._computeShowOnBehalfOf(message));
+ assert.isNotOk(element.computeShowOnBehalfOf());
message.real_author = {_account_id: 1115495 as AccountId};
- assert.isNotOk(element._computeShowOnBehalfOf(message));
+ assert.isNotOk(element.computeShowOnBehalfOf());
message.real_author._account_id = 123456 as AccountId;
- assert.isOk(element._computeShowOnBehalfOf(message));
+ assert.isOk(element.computeShowOnBehalfOf());
message.updated_by = message.author;
delete message.author;
- assert.isOk(element._computeShowOnBehalfOf(message));
+ assert.isOk(element.computeShowOnBehalfOf());
delete message.updated_by;
- assert.isNotOk(element._computeShowOnBehalfOf(message));
+ assert.isNotOk(element.computeShowOnBehalfOf());
});
- test('clicking on date link fires event', () => {
+ test('clicking on date link fires event', async () => {
element.message = {
...createChangeMessage(),
type: 'REVIEWER_UPDATE',
reviewer: {},
id: '47c43261_55aa2c41' as ChangeMessageId,
expanded: false,
+ updates: [],
};
- flush();
+ await element.updateComplete;
+
const stub = sinon.stub();
element.addEventListener('message-anchor-tap', stub);
const dateEl = queryAndAssert(element, '.date');
@@ -252,7 +364,7 @@
tap(dateEl);
assert.isTrue(stub.called);
- assert.deepEqual(stub.lastCall.args[0].detail, {id: element.message.id});
+ assert.deepEqual(stub.lastCall.args[0].detail, {id: element.message?.id});
});
suite('uploaded patchset X message navigates to X - 1 vs X', () => {
@@ -267,7 +379,7 @@
...createChangeMessage(),
message: 'Uploaded patch set 1.',
};
- element._handleViewPatchsetDiff(new MouseEvent('click'));
+ element.handleViewPatchsetDiff(new MouseEvent('click'));
assert.isTrue(
navStub.calledWithExactly(element.change!, {
patchNum: 1 as PatchSetNum,
@@ -281,7 +393,7 @@
...createChangeMessage(),
message: 'Uploaded patch set 2.',
};
- element._handleViewPatchsetDiff(new MouseEvent('click'));
+ element.handleViewPatchsetDiff(new MouseEvent('click'));
assert.isTrue(
navStub.calledWithExactly(element.change!, {
patchNum: 2 as PatchSetNum,
@@ -293,7 +405,7 @@
...createChangeMessage(),
message: 'Uploaded patch set 200.',
};
- element._handleViewPatchsetDiff(new MouseEvent('click'));
+ element.handleViewPatchsetDiff(new MouseEvent('click'));
assert.isTrue(
navStub.calledWithExactly(element.change!, {
patchNum: 200 as PatchSetNum,
@@ -307,7 +419,7 @@
...createChangeMessage(),
message: 'Commit message updated.',
};
- element._handleViewPatchsetDiff(new MouseEvent('click'));
+ element.handleViewPatchsetDiff(new MouseEvent('click'));
assert.isTrue(
navStub.calledWithExactly(element.change!, {
patchNum: 4 as PatchSetNum,
@@ -321,7 +433,7 @@
...createChangeMessage(),
message: 'abcd↵3 is the latest approved patch-set.↵abc',
};
- element._handleViewPatchsetDiff(new MouseEvent('click'));
+ element.handleViewPatchsetDiff(new MouseEvent('click'));
assert.isTrue(
navStub.calledWithExactly(element.change!, {
patchNum: 4 as PatchSetNum,
@@ -334,7 +446,7 @@
suite('compute messages', () => {
test('empty', () => {
assert.equal(
- element._computeMessageContent(
+ element.computeMessageContent(
true,
'',
undefined,
@@ -343,7 +455,7 @@
''
);
assert.equal(
- element._computeMessageContent(
+ element.computeMessageContent(
false,
'',
undefined,
@@ -356,13 +468,9 @@
test('new patchset', () => {
const original = 'Uploaded patch set 1.';
const tag = 'autogenerated:gerrit:newPatchSet' as ReviewInputTag;
- let actual = element._computeMessageContent(true, original, [], tag);
- assert.equal(
- actual,
- element._computeMessageContentCollapsed(original, [], tag, [])
- );
+ let actual = element.computeMessageContent(true, original, [], tag);
assert.equal(actual, original);
- actual = element._computeMessageContent(false, original, [], tag);
+ actual = element.computeMessageContent(false, original, [], tag);
assert.equal(actual, original);
});
@@ -370,13 +478,9 @@
const original = 'Patch Set 27: Patch Set 26 was rebased';
const tag = 'autogenerated:gerrit:newPatchSet' as ReviewInputTag;
const expected = 'Patch Set 26 was rebased';
- let actual = element._computeMessageContent(true, original, [], tag);
+ let actual = element.computeMessageContent(true, original, [], tag);
assert.equal(actual, expected);
- assert.equal(
- actual,
- element._computeMessageContentCollapsed(original, [], tag, [])
- );
- actual = element._computeMessageContent(false, original, [], tag);
+ actual = element.computeMessageContent(false, original, [], tag);
assert.equal(actual, expected);
});
@@ -384,31 +488,27 @@
const original = 'Patch Set 1:\n\nThis change is ready for review.';
const tag = undefined;
const expected = 'This change is ready for review.';
- let actual = element._computeMessageContent(true, original, [], tag);
+ let actual = element.computeMessageContent(true, original, [], tag);
assert.equal(actual, expected);
- assert.equal(
- actual,
- element._computeMessageContentCollapsed(original, [], tag, [])
- );
- actual = element._computeMessageContent(false, original, [], tag);
+ actual = element.computeMessageContent(false, original, [], tag);
assert.equal(actual, expected);
});
test('new patchset with vote', () => {
const original = 'Uploaded patch set 2: Code-Review+1';
const tag = 'autogenerated:gerrit:newPatchSet' as ReviewInputTag;
const expected = 'Uploaded patch set 2: Code-Review+1';
- let actual = element._computeMessageContent(true, original, [], tag);
+ let actual = element.computeMessageContent(true, original, [], tag);
assert.equal(actual, expected);
- actual = element._computeMessageContent(false, original, [], tag);
+ actual = element.computeMessageContent(false, original, [], tag);
assert.equal(actual, expected);
});
test('vote', () => {
const original = 'Patch Set 1: Code-Style+1';
const tag = undefined;
const expected = '';
- let actual = element._computeMessageContent(true, original, [], tag);
+ let actual = element.computeMessageContent(true, original, [], tag);
assert.equal(actual, expected);
- actual = element._computeMessageContent(false, original, [], tag);
+ actual = element.computeMessageContent(false, original, [], tag);
assert.equal(actual, expected);
});
@@ -416,9 +516,9 @@
const original = 'Patch Set 1:\n\n(3 comments)';
const tag = undefined;
const expected = '';
- let actual = element._computeMessageContent(true, original, [], tag);
+ let actual = element.computeMessageContent(true, original, [], tag);
assert.equal(actual, expected);
- actual = element._computeMessageContent(false, original, [], tag);
+ actual = element.computeMessageContent(false, original, [], tag);
assert.equal(actual, expected);
});
@@ -432,14 +532,14 @@
createAccountWithIdNameAndEmail(1),
createAccountWithIdNameAndEmail(2),
];
- let actual = element._computeMessageContent(
+ let actual = element.computeMessageContent(
true,
original,
accountsInMessage,
tag
);
assert.equal(actual, expected);
- actual = element._computeMessageContent(
+ actual = element.computeMessageContent(
false,
original,
accountsInMessage,
@@ -454,9 +554,9 @@
const tag = undefined;
const expected =
'Removed vote: \n\n * Code-Style+1 by Gerrit Account 1\n * Code-Style-1 by Gerrit Account 2';
- let actual = element._computeMessageContent(true, original, [], tag);
+ let actual = element.computeMessageContent(true, original, [], tag);
assert.equal(actual, expected);
- actual = element._computeMessageContent(false, original, [], tag);
+ actual = element.computeMessageContent(false, original, [], tag);
assert.equal(actual, expected);
});
});
@@ -466,11 +566,10 @@
setup(async () => {
stubRestApi('getLoggedIn').returns(Promise.resolve(false));
stubRestApi('getIsAdmin').returns(Promise.resolve(false));
- element = basicFixture.instantiate();
- await flush();
+ element = await fixture<GrMessage>(html`<gr-message></gr-message>`);
});
- test('reply and delete button should be hidden', () => {
+ test('reply and delete button should be hidden', async () => {
element.message = {
...createChangeMessage(),
id: '47c43261_55aa2c41' as ChangeMessageId,
@@ -485,25 +584,24 @@
expanded: true,
};
- flush();
- assert.isTrue(
- queryAndAssert<HTMLElement>(element, '.replyActionContainer').hidden
- );
- assert.isTrue(queryAndAssert<HTMLElement>(element, '.deleteBtn').hidden);
+ await element.updateComplete;
+ assert.isNotOk(query<HTMLElement>(element, '.replyActionContainer'));
+ assert.isNotOk(query<HTMLElement>(element, '.deleteBtn'));
});
});
- suite('patchset comment summary', () => {
- setup(() => {
- element = basicFixture.instantiate();
+ suite('patchset comment summary', async () => {
+ setup(async () => {
+ element = await fixture<GrMessage>(html`<gr-message></gr-message>`);
element.message = {
...createChangeMessage(),
id: '6a07f64a82f96e7337ca5f7f84cfc73abf8ac2a3' as ChangeMessageId,
};
+ await element.updateComplete;
});
test('single patchset comment posted', () => {
- const threads = [
+ element.commentThreads = [
{
comments: [
{
@@ -516,7 +614,6 @@
message: 'testing the load',
unresolved: false,
path: '/PATCHSET_LEVEL',
- collapsed: false,
},
],
patchNum: 1 as PatchSetNum,
@@ -525,23 +622,15 @@
commentSide: CommentSide.REVISION,
},
];
+ assert.equal(element.patchsetCommentSummary(), 'testing the load');
assert.equal(
- element._computeMessageContentCollapsed(
- '',
- undefined,
- undefined,
- threads
- ),
- 'testing the load'
- );
- assert.equal(
- element._computeMessageContent(false, '', undefined, undefined),
+ element.computeMessageContent(false, '', undefined, undefined),
''
);
});
test('single patchset comment with reply', () => {
- const threads = [
+ element.commentThreads = [
{
comments: [
{
@@ -552,7 +641,6 @@
message: 'testing the load',
unresolved: false,
path: '/PATCHSET_LEVEL',
- collapsed: false,
},
{
change_message_id: '6a07f64a82f96e7337ca5f7f84cfc73abf8ac2a3',
@@ -564,7 +652,6 @@
unresolved: false,
path: '/PATCHSET_LEVEL',
__draft: true,
- collapsed: true,
},
],
patchNum: 1 as PatchSetNum,
@@ -573,17 +660,9 @@
commentSide: CommentSide.REVISION,
},
];
+ assert.equal(element.patchsetCommentSummary(), 'n');
assert.equal(
- element._computeMessageContentCollapsed(
- '',
- undefined,
- undefined,
- threads
- ),
- 'n'
- );
- assert.equal(
- element._computeMessageContent(false, '', undefined, undefined),
+ element.computeMessageContent(false, '', undefined, undefined),
''
);
});
@@ -592,11 +671,10 @@
suite('when logged in but not admin', () => {
setup(async () => {
stubRestApi('getIsAdmin').returns(Promise.resolve(false));
- element = basicFixture.instantiate();
- await flush();
+ element = await fixture<GrMessage>(html`<gr-message></gr-message>`);
});
- test('can see reply but not delete button', () => {
+ test('can see reply but not delete button', async () => {
element.message = {
...createChangeMessage(),
id: '47c43261_55aa2c41' as ChangeMessageId,
@@ -610,17 +688,16 @@
_revision_number: 1 as PatchSetNum,
expanded: true,
};
+ await element.updateComplete;
- flush();
- assert.isFalse(
- queryAndAssert<HTMLElement>(element, '.replyActionContainer').hidden
- );
- assert.isTrue(queryAndAssert<HTMLElement>(element, '.deleteBtn').hidden);
+ assert.isOk(query<HTMLElement>(element, '.replyActionContainer'));
+ assert.isNotOk(query<HTMLElement>(element, '.deleteBtn'));
});
- test('reply button shown when message is updated', () => {
+ test('reply button shown when message is updated', async () => {
element.message = undefined;
- flush();
+ await element.updateComplete;
+
let replyEl = query(element, '.replyActionContainer');
// We don't even expect the button to show up in the DOM when the message
// is undefined.
@@ -639,10 +716,10 @@
_revision_number: 1 as PatchSetNum,
expanded: true,
};
- flush();
+ await element.updateComplete;
+
replyEl = queryAndAssert(element, '.replyActionContainer');
assert.isOk(replyEl);
- assert.isFalse((replyEl as HTMLElement).hidden);
});
});
});
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
index 28acbea..f6b0ad4 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
@@ -57,6 +57,7 @@
import {commentsModelToken} from '../../../models/comments/comments-model';
import {changeModelToken} from '../../../models/change/change-model';
import {resolve, DIPolymerElement} from '../../../models/dependency';
+import {queryAll} from '../../../utils/common-util';
/**
* The content of the enum is also used in the UI for the button text.
@@ -305,7 +306,7 @@
super.disconnectedCallback();
}
- scrollToMessage(messageID: string) {
+ async scrollToMessage(messageID: string) {
const selector = `[data-message-id="${messageID}"]`;
const el = this.shadowRoot!.querySelector(selector) as
| GrMessage
@@ -317,13 +318,15 @@
);
return;
}
- if (!el) {
+ if (!el || !el.message) {
this._showAllActivity = true;
setTimeout(() => this.scrollToMessage(messageID));
return;
}
- el.set('message.expanded', true);
+ el.message.expanded = true;
+ el.requestUpdate();
+ await el.updateComplete;
let top = el.offsetTop;
for (
let offsetParent = el.offsetParent as HTMLElement | null;
@@ -409,11 +412,14 @@
}
_updateExpandedStateOfAllMessages(exp: boolean) {
- if (this._combinedMessages) {
- for (let i = 0; i < this._combinedMessages.length; i++) {
- this._combinedMessages[i].expanded = exp;
- this.notifyPath(`_combinedMessages.${i}.expanded`);
- }
+ if (!this._combinedMessages) return;
+
+ for (let i = 0; i < this._combinedMessages.length; i++) {
+ this._combinedMessages[i].expanded = exp;
+ this.notifyPath(`_combinedMessages.${i}.expanded`);
+ }
+ for (const message of queryAll<GrMessage>(this, 'gr-message')) {
+ message.requestUpdate('message');
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
index 283ea357..b9cb616 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
@@ -41,6 +41,7 @@
UrlEncodedCommentId,
} from '../../../types/common';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+import {assertIsDefined} from '../../../utils/common-util';
createCommentApiMockWithTemplateElement(
'gr-messages-list-comment-mock-api',
@@ -167,43 +168,53 @@
await flush();
});
- test('expand/collapse all', () => {
+ test('expand/collapse all', async () => {
let allMessageEls = getMessages();
for (const message of allMessageEls) {
- message._expanded = false;
+ assertIsDefined(message.message);
+ message.message = {...message.message, expanded: false};
+ await message.updateComplete;
}
MockInteractions.tap(allMessageEls[1]);
- assert.isTrue(allMessageEls[1]._expanded);
+ assert.isTrue(allMessageEls[1].message?.expanded);
MockInteractions.tap(queryAndAssert(element, '#collapse-messages'));
allMessageEls = getMessages();
for (const message of allMessageEls) {
- assert.isTrue(message._expanded);
+ assert.isTrue(message.message?.expanded);
}
MockInteractions.tap(queryAndAssert(element, '#collapse-messages'));
allMessageEls = getMessages();
for (const message of allMessageEls) {
- assert.isFalse(message._expanded);
+ assert.isFalse(message.message?.expanded);
}
});
test('expand/collapse from external keypress', () => {
// Start with one expanded message. -> not all collapsed
element.scrollToMessage(messages[1].id);
- assert.isFalse([...getMessages()].filter(m => m._expanded).length === 0);
+ assert.isFalse(
+ [...getMessages()].filter(m => m.message?.expanded).length === 0
+ );
// Press 'z' -> all collapsed
element.handleExpandCollapse(false);
- assert.isTrue([...getMessages()].filter(m => m._expanded).length === 0);
+ assert.isTrue(
+ [...getMessages()].filter(m => m.message?.expanded).length === 0
+ );
// Press 'x' -> all expanded
element.handleExpandCollapse(true);
- assert.isTrue([...getMessages()].filter(m => !m._expanded).length === 0);
+ assert.isTrue(
+ [...getMessages()].filter(m => !m.message?.expanded).length === 0
+ );
// Press 'z' -> all collapsed
element.handleExpandCollapse(false);
- assert.isTrue([...getMessages()].filter(m => m._expanded).length === 0);
+ assert.isTrue(
+ [...getMessages()].filter(m => m.message?.expanded).length === 0
+ );
});
test('showAllActivity does not appear when all msgs are important', () => {
@@ -211,50 +222,52 @@
assert.isNotOk(query(element, '.showAllActivityToggle'));
});
- test('scroll to message', () => {
+ test('scroll to message', async () => {
const allMessageEls = getMessages();
for (const message of allMessageEls) {
- message.set('message.expanded', false);
+ assertIsDefined(message.message);
+ message.message = {...message.message, expanded: false};
}
const scrollToStub = sinon.stub(window, 'scrollTo');
const highlightStub = sinon.stub(element, '_highlightEl');
- element.scrollToMessage('invalid');
+ await element.scrollToMessage('invalid');
for (const message of allMessageEls) {
+ assertIsDefined(message.message);
assert.isFalse(
- message._expanded,
+ message.message.expanded,
'expected gr-message to not be expanded'
);
}
const messageID = messages[1].id;
- element.scrollToMessage(messageID);
+ await element.scrollToMessage(messageID);
assert.isTrue(
queryAndAssert<GrMessage>(element, `[data-message-id="${messageID}"]`)
- ._expanded
+ .message?.expanded
);
assert.isTrue(scrollToStub.calledOnce);
assert.isTrue(highlightStub.calledOnce);
});
- test('scroll to message offscreen', () => {
+ test('scroll to message offscreen', async () => {
const scrollToStub = sinon.stub(window, 'scrollTo');
const highlightStub = sinon.stub(element, '_highlightEl');
element.messages = generateRandomMessages(25);
- flush();
+ await element.updateComplete;
assert.isFalse(scrollToStub.called);
assert.isFalse(highlightStub.called);
const messageID = element.messages[1].id;
- element.scrollToMessage(messageID);
+ await element.scrollToMessage(messageID);
assert.isTrue(scrollToStub.calledOnce);
assert.isTrue(highlightStub.calledOnce);
assert.isTrue(
queryAndAssert<GrMessage>(element, `[data-message-id="${messageID}"]`)
- ._expanded
+ .message?.expanded
);
});
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
index c651d08..5d0f52a 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
@@ -423,8 +423,9 @@
}
}
- handleEditCommitMessage() {
+ async handleEditCommitMessage() {
this.editing = true;
+ await this.updateComplete;
this.focusTextarea();
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
index 4456381..dc2cbc7 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
@@ -25,7 +25,7 @@
<g id="expand-less"><path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
<g id="expand-more"><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"></path></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#unfold_more -->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=unfold_more -->
<g id="unfold-more"><path d="M0 0h24v24H0z" fill="none"></path><path d="M12 5.83L15.17 9l1.41-1.41L12 3 7.41 7.59 8.83 9 12 5.83zm0 12.34L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15 12 18.17z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
<g id="search"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"></path></g>
@@ -61,11 +61,11 @@
<g id="info"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
<g id="info-outline"><path d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z"></path></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#ic_hourglass_full-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=ic_hourglass_full-->
<g id="hourglass"><path d="M6 2v6h.01L6 8.01 10 12l-4 4 .01.01H6V22h12v-5.99h-.01L18 16l-4-4 4-3.99-.01-.01H18V2H6z"></path><path d="M0 0h24v24H0V0z" fill="none"></path></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#mode_comment-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=mode_comment-->
<g id="comment"><path d="M21.99 4c0-1.1-.89-2-1.99-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4-.01-18z"></path><path d="M0 0h24v24H0z" fill="none"></path></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#calendar_today-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=calendar_today-->
<g id="calendar"><path d="M20 3h-1V1h-2v2H7V1H5v2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 18H4V8h16v13z"></path><path d="M0 0h24v24H0z" fill="none"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
<g id="error"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"></path></g>
@@ -77,11 +77,13 @@
<g id="unified"><path d="M4,2 L17,2 C18.1045695,2 19,2.8954305 19,4 L19,16 C19,17.1045695 18.1045695,18 17,18 L4,18 C2.8954305,18 2,17.1045695 2,16 L2,4 L2,4 C2,2.8954305 2.8954305,2 4,2 L4,2 Z M4,7 L4,9 L17,9 L17,7 L4,7 Z M4,11 L4,13 L17,13 L17,11 L4,11 Z" id="Combined-Shape" transform="scale(1.12, 1.2)"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
<g id="content-copy"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"></path></g>
+ <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
+ <g id="build"><path d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z"></path></g>
<!-- This is a custom PolyGerrit SVG -->
<g id="check"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"></path></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#check_circle-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=check_circle-->
<g id="check-circle"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#check_circle_outline-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=check_circle_outline-->
<g id="check-circle-outline"><path d="M0 0h24v24H0V0zm0 0h24v24H0V0z" fill="none"/><path d="M16.59 7.58L10 14.17l-3.59-3.58L5 12l5 5 8-8zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></g>
<!-- This SVG is a copy from https://fonts.google.com/icons?selected=Material+Icons:event_busy&icon.query=check+circle-->
<g id="check-circle-filled"><path d="M12,2C6.48,2,2,6.48,2,12c0,5.52,4.48,10,10,10s10-4.48,10-10C22,6.48,17.52,2,12,2z M10,17l-4-4l1.4-1.4l2.6,2.6l6.6-6.6 L18,9L10,17z"/><path d="M0,0h24v24H0V0z" fill="none"/></g>
@@ -110,45 +112,45 @@
<g id="review"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"></path></g>
<!-- This is a custom PolyGerrit SVG -->
<g id="zeroState"><path d="M22 9V7h-2V5c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-2h2v-2h-2v-2h2v-2h-2V9h2zm-4 10H4V5h14v14zM6 13h5v4H6zm6-6h4v3h-4zM6 7h5v5H6zm6 4h4v6h-4z"></path></g>
- <!-- This SVG is an adaptation of material.io https://material.io/icons/#label_important-->
+ <!-- This SVG is an adaptation of material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=label_important-->
<g id="attention"><path d="M1 23 l13 0 c.67 0 1.27 -.33 1.63 -.84 l7.37 -10.16 l-7.37 -10.16 c-.36 -.51 -.96 -.84 -1.63 -.84 L1 1 L7 12 z"></path></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#pets-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=pets-->
<g id="pets"><circle cx="4.5" cy="9.5" r="2.5"/><circle cx="9" cy="5.5" r="2.5"/><circle cx="15" cy="5.5" r="2.5"/><circle cx="19.5" cy="9.5" r="2.5"/><path d="M17.34 14.86c-.87-1.02-1.6-1.89-2.48-2.91-.46-.54-1.05-1.08-1.75-1.32-.11-.04-.22-.07-.33-.09-.25-.04-.52-.04-.78-.04s-.53 0-.79.05c-.11.02-.22.05-.33.09-.7.24-1.28.78-1.75 1.32-.87 1.02-1.6 1.89-2.48 2.91-1.31 1.31-2.92 2.76-2.62 4.79.29 1.02 1.02 2.03 2.33 2.32.73.15 3.06-.44 5.54-.44h.18c2.48 0 4.81.58 5.54.44 1.31-.29 2.04-1.31 2.33-2.32.31-2.04-1.3-3.49-2.61-4.8z"/><path d="M0 0h24v24H0z" fill="none"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#visibility-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=visibility-->
<g id="ready"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons -->
<g id="schedule"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"></path></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#bug_report-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=bug_report-->
<g id="bug"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5c-.49 0-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z"/></g>
<!-- This SVG is a copy from material.io https://fonts.gstatic.com/s/i/googlematerialicons/move_item/v1/24px.svg -->
<g id="move-item"><path d="M15,19H5V5h10v4h2V5c0-1.1-0.89-2-2-2H5C3.9,3,3,3.9,3,5v14c0,1.1,0.9,2,2,2h10c1.11,0,2-0.9,2-2v-4h-2V19z"/><polygon points="20.01,8.01 18.59,9.41 20.17,11 8,11 8,13 20.17,13 18.59,14.59 20.01,15.99 24,12"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#warning-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=warning-->
<g id="warning"><path d="M0 0h24v24H0z" fill="none"/><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#timelapse-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=timelapse-->
<g id="timelapse"><path d="M0 0h24v24H0z" fill="none"/><path d="M16.24 7.76C15.07 6.59 13.54 6 12 6v6l-4.24 4.24c2.34 2.34 6.14 2.34 8.49 0 2.34-2.34 2.34-6.14-.01-8.48zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#mark_chat_read-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=mark_chat_read-->
<g id="markChatRead"><path d="M12,18l-6,0l-4,4V4c0-1.1,0.9-2,2-2h16c1.1,0,2,0.9,2,2v7l-2,0V4H4v12l8,0V18z M23,14.34l-1.41-1.41l-4.24,4.24l-2.12-2.12 l-1.41,1.41L17.34,20L23,14.34z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#message-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=message-->
<g id="message"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#launch-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=launch-->
<g id="launch"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#filter-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=filter-->
<g id="filter"><path d="M0,0h24 M24,24H0" fill="none"/><path d="M4.25,5.61C6.27,8.2,10,13,10,13v6c0,0.55,0.45,1,1,1h2c0.55,0,1-0.45,1-1v-6c0,0,3.72-4.8,5.74-7.39 C20.25,4.95,19.78,4,18.95,4H5.04C4.21,4,3.74,4.95,4.25,5.61z"/><path d="M0,0h24v24H0V0z" fill="none"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#arrow_drop_down-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=arrow_drop_down-->
<g id="arrowDropDown"><path d="M0 0h24v24H0z" fill="none"/><path d="M7 10l5 5 5-5z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#arrow_drop_up-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=arrow_drop_up-->
<g id="arrowDropUp"><path d="M0 0h24v24H0z" fill="none"/><path d="M7 14l5-5 5 5z"/></g>
<!-- This is just a placeholder, i.e. an empty icon that has the same size as a normal icon. -->
<g id="placeholder"><path d="M0 0h24v24H0z" fill="none"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#insert_photo-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=insert_photo-->
<g id="insert-photo"><path d="M0 0h24v24H0z" fill="none"/><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#download-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=download-->
<g id="download"><path d="M0 0h24v24H0z" fill="none"/><path d="M5,20h14v-2H5V20z M19,9h-4V3H9v6H5l7,7L19,9z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#system_update-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=system_update-->
<g id="system-update"><path d="M0 0h24v24H0z" fill="none"/><path d="M17 1.01L7 1c-1.1 0-2 .9-2 2v18c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM17 19H7V5h10v14zm-1-6h-3V8h-2v5H8l4 4 4-4z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#swap_horiz-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=swap_horiz-->
<g id="swapHoriz"><path d="M0 0h24v24H0z" fill="none"/><path d="M6.99 11L3 15l3.99 4v-3H14v-2H6.99v-3zM21 9l-3.99-4v3H10v2h7.01v3L21 9z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#link-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=link-->
<g id="link"><path d="M0 0h24v24H0z" fill="none"/><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></g>
<!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material%20Icons%3Aplay_arrow-->
<g id="playArrow"><path d="M0 0h24v24H0z" fill="none"/><path d="M8 5v14l11-7z"/></g>
diff --git a/polygerrit-ui/app/utils/comment-util.ts b/polygerrit-ui/app/utils/comment-util.ts
index 2aefa99..669c491 100644
--- a/polygerrit-ui/app/utils/comment-util.ts
+++ b/polygerrit-ui/app/utils/comment-util.ts
@@ -38,6 +38,7 @@
import {isMergeParent, getParentIndex} from './patch-set-util';
import {DiffInfo} from '../types/diff';
import {LineNumber} from '../api/diff';
+import {FormattedReviewerUpdateInfo} from '../types/types';
export interface DraftCommentProps {
// This must be true for all drafts. Drafts received from the backend will be
@@ -98,6 +99,12 @@
commentThreads: CommentThread[];
}
+export function isFormattedReviewerUpdate(
+ message: ChangeMessage
+): message is ChangeMessage & FormattedReviewerUpdateInfo {
+ return message.type === 'REVIEWER_UPDATE';
+}
+
export type LabelExtreme = {[labelName: string]: VotingRangeInfo};
export const PATCH_SET_PREFIX_PATTERN =