diff options
author | Paladox none <thomasmulhall410@yahoo.com> | 2022-05-12 12:17:32 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2022-05-12 12:17:32 +0000 |
commit | 94d0f99fa2d0373af5083068d49c6f4287135065 (patch) | |
tree | f50658737639afcefc436fbfddc8e62b0e10d1fa | |
parent | a792772864797a2b6db782cc711b02cac2e6fb98 (diff) | |
parent | 814ac33d352a22c56e4126598fc0b61ae5e369df (diff) |
Merge changes Id116e2b4,Ibdc1c20d,I3de8ec70 into stable-3.6
* changes:
Fix issue with closing comment
Finish rendering a message before scrolling
Migrate gr-message to Lit
6 files changed, 691 insertions, 632 deletions
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 8664de32e7..0902fe6e5f 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-account-chip/gr-account-chip'; 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 { import { ChangeMessage, CommentThread, + isFormattedReviewerUpdate, LabelExtreme, PATCH_SET_PREFIX_PATTERN, } from '../../../utils/comment-util'; @@ -54,6 +53,9 @@ import { 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 @@ export interface MessageAnchorTapDetail { } @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 @@ export class GrMessage extends PolymerElement { 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 @@ export class GrMessage extends PolymerElement { @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 @@ export class GrMessage extends PolymerElement { @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 @@ export class GrMessage extends PolymerElement { 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) { - this.classList.add('expanded'); - } else { - this.classList.remove('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 '; + } + } + `; } - _computeCommentCountText(commentThreads?: CommentThread[]) { - if (!commentThreads?.length) { - return undefined; + override willUpdate(changedProperties: PropertyValues) { + if (changedProperties.has('projectName')) { + this.projectNameChanged(); } - - return pluralize(commentThreads.length, 'comment'); } - _computeMessageContentExpanded( - expanded: boolean, - content?: string, - accountsInMessage?: AccountInfo[], - tag?: ReviewInputTag - ) { - if (!expanded) return ''; - return this._computeMessageContent(true, content, accountsInMessage, tag); + 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'); + } } - _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 @@ export class GrMessage extends PolymerElement { 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 @@ export class GrMessage extends PolymerElement { 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 @@ export class GrMessage extends PolymerElement { 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; - } - - _computeIsReviewerUpdate(message: ChangeMessage) { - return message.type === 'REVIEWER_UPDATE'; + private computeIsReviewerUpdate() { + return this.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 @@ export class GrMessage extends PolymerElement { ); } - _handleReplyTap(e: Event) { + private handleReplyTap(e: Event) { e.preventDefault(); this.dispatchEvent( new CustomEvent('reply', { @@ -463,14 +724,14 @@ export class GrMessage extends PolymerElement { ); } - _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 @@ export class GrMessage extends PolymerElement { }); } - @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 70e6381829..0000000000 --- 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 ffe59f0fc5..bbe39ff07f 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 { 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('gr-message tests', () => { 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 @@ suite('gr-message tests', () => { 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 @@ suite('gr-message tests', () => { _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 @@ suite('gr-message tests', () => { _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 @@ suite('gr-message tests', () => { 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, }; - - assert.isTrue(element.isAutomated); - assert.isFalse(element.hidden); + await element.updateComplete; + + 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, }; - - assert.isTrue(element.isAutomated); - assert.isFalse(element.hidden); + await element.updateComplete; + + 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: [], }; - - assert.isTrue(element.isAutomated); - assert.isFalse(element.hidden); + await element.updateComplete; + + 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, }; - - assert.isFalse(element.isAutomated); - assert.isFalse(element.hidden); + await element.updateComplete; + + 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 @@ suite('gr-message tests', () => { 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 @@ suite('gr-message tests', () => { 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 @@ suite('gr-message tests', () => { ...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 @@ suite('gr-message tests', () => { ...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 @@ suite('gr-message tests', () => { ...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 @@ suite('gr-message tests', () => { ...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 @@ suite('gr-message tests', () => { ...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('gr-message tests', () => { suite('compute messages', () => { test('empty', () => { assert.equal( - element._computeMessageContent( + element.computeMessageContent( true, '', undefined, @@ -343,7 +455,7 @@ suite('gr-message tests', () => { '' ); assert.equal( - element._computeMessageContent( + element.computeMessageContent( false, '', undefined, @@ -356,13 +468,9 @@ suite('gr-message tests', () => { 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 @@ suite('gr-message tests', () => { 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 @@ suite('gr-message tests', () => { 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 @@ suite('gr-message tests', () => { 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 @@ suite('gr-message tests', () => { 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 @@ suite('gr-message tests', () => { 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 @@ suite('gr-message tests', () => { 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 @@ suite('gr-message tests', () => { 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 @@ suite('gr-message tests', () => { message: 'testing the load', unresolved: false, path: '/PATCHSET_LEVEL', - collapsed: false, }, ], patchNum: 1 as PatchSetNum, @@ -525,23 +622,15 @@ suite('gr-message tests', () => { 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 @@ suite('gr-message tests', () => { message: 'testing the load', unresolved: false, path: '/PATCHSET_LEVEL', - collapsed: false, }, { change_message_id: '6a07f64a82f96e7337ca5f7f84cfc73abf8ac2a3', @@ -564,7 +652,6 @@ suite('gr-message tests', () => { unresolved: false, path: '/PATCHSET_LEVEL', __draft: true, - collapsed: true, }, ], patchNum: 1 as PatchSetNum, @@ -573,17 +660,9 @@ suite('gr-message tests', () => { 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('gr-message tests', () => { 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 @@ suite('gr-message tests', () => { _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 @@ suite('gr-message tests', () => { _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 28acbea805..f6b0ad4cc3 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 { 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 @@ export class GrMessagesList extends DIPolymerElement { 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 @@ export class GrMessagesList extends DIPolymerElement { ); 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 @@ export class GrMessagesList extends DIPolymerElement { } _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 283ea3579d..b9cb616d29 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 @@ import { 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 @@ suite('gr-messages-list tests', () => { 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 @@ suite('gr-messages-list tests', () => { 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/utils/comment-util.ts b/polygerrit-ui/app/utils/comment-util.ts index 2aefa9954b..669c4916c7 100644 --- a/polygerrit-ui/app/utils/comment-util.ts +++ b/polygerrit-ui/app/utils/comment-util.ts @@ -38,6 +38,7 @@ import {CommentIdToCommentThreadMap} from '../elements/diff/gr-comment-api/gr-co 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 @@ export interface ChangeMessage extends ChangeMessageInfo { 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 = |