Merge "Update stale comment in ModuleOverloader"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
index b311efc..f0b8372 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
@@ -753,6 +753,7 @@
class="scrollable"
no-cancel-on-outside-click=""
no-cancel-on-esc-key=""
+ scroll-action="lock"
with-backdrop=""
>
<gr-reply-dialog
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
deleted file mode 100644
index 0da687f..0000000
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ /dev/null
@@ -1,430 +0,0 @@
-/**
- * @license
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '@polymer/iron-icon/iron-icon.js';
-import '../../shared/gr-account-label/gr-account-label.js';
-import '../../shared/gr-account-chip/gr-account-chip.js';
-import '../../shared/gr-button/gr-button.js';
-import '../../shared/gr-date-formatter/gr-date-formatter.js';
-import '../../shared/gr-formatted-text/gr-formatted-text.js';
-import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import '../../../styles/shared-styles.js';
-import '../../../styles/gr-voting-styles.js';
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {htmlTemplate} from './gr-message_html.js';
-import {SpecialFilePath} from '../../../constants/constants.js';
-
-const PATCH_SET_PREFIX_PATTERN = /^Patch Set \d+:\s*(.*)/;
-const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?$/;
-
-/**
- * @extends PolymerElement
- */
-class GrMessage extends GestureEventListeners(
- LegacyElementMixin(PolymerElement)) {
- static get template() { return htmlTemplate; }
-
- static get is() { return 'gr-message'; }
- /**
- * Fired when this message's reply link is tapped.
- *
- * @event reply
- */
-
- /**
- * Fired when the message's timestamp is tapped.
- *
- * @event message-anchor-tap
- */
-
- /**
- * Fired when a change message is deleted.
- *
- * @event change-message-deleted
- */
-
- static get properties() {
- return {
- /** @type {?} */
- change: Object,
- changeNum: Number,
- /** @type {?} */
- message: Object,
- author: {
- type: Object,
- computed: '_computeAuthor(message)',
- },
- /**
- * TODO(taoalpha): remove once the change log experiment is launched
- *
- * @type {Object} - a map on file and comments on it
- */
- comments: {
- type: Object,
- },
- config: Object,
- hideAutomated: {
- type: Boolean,
- value: false,
- },
- hidden: {
- type: Boolean,
- computed: '_computeIsHidden(hideAutomated, isAutomated)',
- reflectToAttribute: true,
- },
- isAutomated: {
- type: Boolean,
- computed: '_computeIsAutomated(message)',
- },
- showOnBehalfOf: {
- type: Boolean,
- computed: '_computeShowOnBehalfOf(message)',
- },
- showReplyButton: {
- type: Boolean,
- computed: '_computeShowReplyButton(message, _loggedIn)',
- },
- projectName: {
- type: String,
- observer: '_projectNameChanged',
- },
-
- /**
- * A mapping from label names to objects representing the minimum and
- * maximum possible values for that label.
- */
- labelExtremes: Object,
-
- /**
- * @type {{ commentlinks: Array }}
- */
- _projectConfig: Object,
- // Computed property needed to trigger Polymer value observing.
- _expanded: {
- type: Object,
- computed: '_computeExpanded(message.expanded)',
- },
- _messageContentExpanded: {
- type: String,
- computed:
- '_computeMessageContentExpanded(message.message, message.tag)',
- },
- _messageContentCollapsed: {
- type: String,
- computed:
- '_computeMessageContentCollapsed(message.message, message.tag,' +
- ' message.commentThreads)',
- },
- _commentCountText: {
- type: Number,
- computed: '_computeCommentCountText(message.commentThreads.length)',
- },
- _loggedIn: {
- type: Boolean,
- value: false,
- },
- _isAdmin: {
- type: Boolean,
- value: false,
- },
- _isDeletingChangeMsg: {
- type: Boolean,
- value: false,
- },
- };
- }
-
- static get observers() {
- return [
- '_updateExpandedClass(message.expanded)',
- ];
- }
-
- constructor() {
- super();
- }
-
- /** @override */
- created() {
- super.created();
- this.addEventListener('click',
- e => this._handleClick(e));
- }
-
- /** @override */
- ready() {
- super.ready();
- this.$.restAPI.getConfig().then(config => {
- this.config = config;
- });
- this.$.restAPI.getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
- });
- this.$.restAPI.getIsAdmin().then(isAdmin => {
- this._isAdmin = isAdmin;
- });
- }
-
- _updateExpandedClass(expanded) {
- if (expanded) {
- this.classList.add('expanded');
- } else {
- this.classList.remove('expanded');
- }
- }
-
- _computeCommentCountText(threadsLength) {
- if (threadsLength === 0) {
- return undefined;
- } else if (threadsLength === 1) {
- return '1 comment';
- } else {
- return `${threadsLength} comments`;
- }
- }
-
- _onThreadListModified() {
- // TODO(taoalpha): this won't propagate the changes to the files
- // should consider replacing this with either top level events
- // or gerrit level events
-
- // emit the event so change-view can also get updated with latest changes
- this.dispatchEvent(new CustomEvent('comment-refresh', {
- composed: true, bubbles: true,
- }));
- }
-
- _computeMessageContentExpanded(content, tag) {
- return this._computeMessageContent(content, tag, true);
- }
-
- _patchsetCommentSummary(commentThreads) {
- const id = this.message.id;
- if (!id) return '';
- const patchsetThreads = commentThreads.filter(thread =>
- thread.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS);
- for (const thread of patchsetThreads) {
- // Find if there was a patchset level comment created through the reply
- // dialog and use it to determine the summary
- if (thread.comments[0].change_message_id === id) {
- return thread.comments[0].message;
- }
- }
- // Find if there is a reply to some patchset comment left
- for (const thread of patchsetThreads) {
- for (const comment of thread.comments) {
- if (comment.change_message_id === id) { return comment.message; }
- }
- }
- return '';
- }
-
- _computeMessageContentCollapsed(content, tag, commentThreads) {
- const summary =
- this._computeMessageContent(content, tag, false);
- if (summary || !commentThreads) return summary;
- return this._patchsetCommentSummary(commentThreads);
- }
-
- _computeMessageContent(content, tag, isExpanded) {
- content = content || '';
- tag = tag || '';
- const isNewPatchSet = tag.endsWith(':newPatchSet') ||
- tag.endsWith(':newWipPatchSet');
- const lines = content.split('\n');
- const filteredLines = lines.filter(line => {
- if (!isExpanded && line.startsWith('>')) {
- return false;
- }
- if (line.startsWith('(') && line.endsWith(' comment)')) {
- return false;
- }
- if (line.startsWith('(') && line.endsWith(' comments)')) {
- return false;
- }
- if (!isNewPatchSet && line.match(PATCH_SET_PREFIX_PATTERN)) {
- return false;
- }
- return true;
- });
- const mappedLines = filteredLines.map(line => {
- // The change message formatting is not very consistent, so
- // unfortunately we have to do a bit of tweaking here:
- // Labels should be stripped from lines like this:
- // Patch Set 29: Verified+1
- // Rebase messages (which have a ':newPatchSet' tag) should be kept on
- // lines like this:
- // Patch Set 27: Patch Set 26 was rebased
- if (isNewPatchSet) {
- line = line.replace(PATCH_SET_PREFIX_PATTERN, '$1');
- }
- return line;
- });
- return mappedLines.join('\n').trim();
- }
-
- _computeAuthor(message) {
- return message.author || message.updated_by;
- }
-
- _computeShowOnBehalfOf(message) {
- const author = message.author || message.updated_by;
- return !!(author && message.real_author &&
- author._account_id != message.real_author._account_id);
- }
-
- _computeShowReplyButton(message, loggedIn) {
- return message && !!message.message && loggedIn &&
- !this._computeIsAutomated(message);
- }
-
- _computeExpanded(expanded) {
- return expanded;
- }
-
- _handleClick(e) {
- if (this.message.expanded) { return; }
- e.stopPropagation();
- this.set('message.expanded', true);
- }
-
- _handleAuthorClick(e) {
- if (!this.message.expanded) { return; }
- e.stopPropagation();
- this.set('message.expanded', false);
- }
-
- _computeIsAutomated(message) {
- return !!(message.reviewer ||
- this._computeIsReviewerUpdate(message) ||
- (message.tag && message.tag.startsWith('autogenerated')));
- }
-
- _computeIsHidden(hideAutomated, isAutomated) {
- return hideAutomated && isAutomated;
- }
-
- _computeIsReviewerUpdate(message) {
- return message.type === 'REVIEWER_UPDATE';
- }
-
- _getScores(message, labelExtremes) {
- if (!message || !message.message || !labelExtremes) {
- return [];
- }
- const line = message.message.split('\n', 1)[0];
- const patchSetPrefix = PATCH_SET_PREFIX_PATTERN;
- if (!line.match(patchSetPrefix)) {
- return [];
- }
- const scoresRaw = line.split(patchSetPrefix)[1];
- if (!scoresRaw) {
- return [];
- }
- return scoresRaw.split(' ')
- .map(s => s.match(LABEL_TITLE_SCORE_PATTERN))
- .filter(ms =>
- ms && ms.length === 4 && labelExtremes.hasOwnProperty(ms[2]))
- .map(ms => {
- const label = ms[2];
- const value = ms[1] === '-' ? 'removed' : ms[3];
- return {label, value};
- });
- }
-
- _computeScoreClass(score, labelExtremes) {
- // Polymer 2: check for undefined
- if ([score, labelExtremes].includes(undefined)) {
- return '';
- }
- if (score.value === 'removed') {
- return 'removed';
- }
- const classes = [];
- if (score.value > 0) {
- classes.push('positive');
- } else if (score.value < 0) {
- classes.push('negative');
- }
- const extremes = labelExtremes[score.label];
- if (extremes) {
- const intScore = parseInt(score.value, 10);
- if (intScore === extremes.max) {
- classes.push('max');
- } else if (intScore === extremes.min) {
- classes.push('min');
- }
- }
- return classes.join(' ');
- }
-
- _computeClass(expanded) {
- const classes = [];
- classes.push(expanded ? 'expanded' : 'collapsed');
- return classes.join(' ');
- }
-
- _handleAnchorClick(e) {
- e.preventDefault();
- this.dispatchEvent(new CustomEvent('message-anchor-tap', {
- bubbles: true,
- composed: true,
- detail: {id: this.message.id},
- }));
- }
-
- _handleReplyTap(e) {
- e.preventDefault();
- this.dispatchEvent(new CustomEvent('reply', {
- detail: {message: this.message},
- composed: true, bubbles: true,
- }));
- }
-
- _handleDeleteMessage(e) {
- e.preventDefault();
- if (!this.message || !this.message.id) return;
- this._isDeletingChangeMsg = true;
- this.$.restAPI.deleteChangeCommitMessage(this.changeNum, this.message.id)
- .then(() => {
- this._isDeletingChangeMsg = false;
- this.dispatchEvent(new CustomEvent('change-message-deleted', {
- detail: {message: this.message},
- composed: true, bubbles: true,
- }));
- });
- }
-
- _projectNameChanged(name) {
- this.$.restAPI.getProjectConfig(name).then(config => {
- this._projectConfig = config;
- });
- }
-
- _computeExpandToggleIcon(expanded) {
- return expanded ? 'gr-icons:expand-less' : 'gr-icons:expand-more';
- }
-
- _toggleExpanded(e) {
- e.stopPropagation();
- this.set('message.expanded', !this.message.expanded);
- }
-}
-
-customElements.define(GrMessage.is, GrMessage);
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
new file mode 100644
index 0000000..2b5d94d
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -0,0 +1,493 @@
+/**
+ * @license
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '@polymer/iron-icon/iron-icon';
+import '../../shared/gr-account-label/gr-account-label';
+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 '../../shared/gr-rest-api-interface/gr-rest-api-interface';
+import '../../../styles/shared-styles';
+import '../../../styles/gr-voting-styles';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {htmlTemplate} from './gr-message_html';
+import {SpecialFilePath} from '../../../constants/constants';
+import {customElement, property, computed, observe} from '@polymer/decorators';
+import {
+ ChangeInfo,
+ ChangeMessageInfo,
+ ServerInfo,
+ ConfigInfo,
+ RepoName,
+ ReviewInputTag,
+ VotingRangeInfo,
+} from '../../../types/common';
+import {RestApiService} from '../../../services/services/gr-rest-api/gr-rest-api';
+import {CommentThread} from '../../diff/gr-comment-api/gr-comment-api';
+import {hasOwnProperty} from '../../../utils/common-util';
+
+const PATCH_SET_PREFIX_PATTERN = /^Patch Set \d+:\s*(.*)/;
+const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?$/;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-message': GrMessage;
+ }
+}
+
+export interface GrMessage {
+ $: {
+ restAPI: RestApiService & Element;
+ };
+}
+
+interface ChangeMessage extends ChangeMessageInfo {
+ // TODO(TS): maybe should be an enum instead
+ type: string;
+ expanded: boolean;
+ commentThreads: CommentThread[];
+}
+
+export type LabelExtreme = {[labelName: string]: VotingRangeInfo};
+
+interface Score {
+ label?: string;
+ value?: string;
+}
+
+@customElement('gr-message')
+export class GrMessage extends GestureEventListeners(
+ LegacyElementMixin(PolymerElement)
+) {
+ static get template() {
+ return htmlTemplate;
+ }
+
+ /**
+ * Fired when this message's reply link is tapped.
+ *
+ * @event reply
+ */
+
+ /**
+ * Fired when the message's timestamp is tapped.
+ *
+ * @event message-anchor-tap
+ */
+
+ /**
+ * Fired when a change message is deleted.
+ *
+ * @event change-message-deleted
+ */
+
+ @property({type: Object})
+ change?: ChangeInfo;
+
+ @property({type: Number})
+ changeNum?: number;
+
+ @property({type: Object})
+ message: ChangeMessage | undefined;
+
+ @computed('message')
+ get author() {
+ return this.message?.author || this.message?.updated_by;
+ }
+
+ @property({type: Object})
+ config?: ServerInfo;
+
+ @property({type: Boolean})
+ hideAutomated = false;
+
+ @property({
+ type: Boolean,
+ reflectToAttribute: true,
+ computed: '_computeIsHidden(hideAutomated, isAutomated)',
+ })
+ 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;
+
+ /**
+ * A mapping from label names to objects representing the minimum and
+ * maximum possible values for that label.
+ */
+ @property({type: Object})
+ labelExtremes?: LabelExtreme;
+
+ @property({type: Object})
+ _projectConfig?: ConfigInfo;
+
+ @property({type: Boolean})
+ _loggedIn = false;
+
+ @property({type: Boolean})
+ _isAdmin = false;
+
+ @property({type: Boolean})
+ _isDeletingChangeMsg = false;
+
+ @property({type: Boolean, computed: '_computeExpanded(message.expanded)'})
+ _expanded = false;
+
+ @property({
+ type: String,
+ computed: '_computeMessageContentExpanded(message.message, message.tag)',
+ })
+ _messageContentExpanded = '';
+
+ @property({
+ type: String,
+ computed:
+ '_computeMessageContentCollapsed(message.message, message.tag,' +
+ ' message.commentThreads)',
+ })
+ _messageContentCollapsed = '';
+
+ @property({
+ type: String,
+ computed: '_computeCommentCountText(message.commentThreads.length)',
+ })
+ _commentCountText = '';
+
+ created() {
+ super.created();
+ this.addEventListener('click', e => this._handleClick(e));
+ }
+
+ attached() {
+ super.attached();
+ this.$.restAPI.getConfig().then(config => {
+ this.config = config;
+ });
+ this.$.restAPI.getLoggedIn().then(loggedIn => {
+ this._loggedIn = loggedIn;
+ });
+ this.$.restAPI.getIsAdmin().then(isAdmin => {
+ this._isAdmin = !!isAdmin;
+ });
+ }
+
+ @observe('message.expanded')
+ _updateExpandedClass(expanded: boolean) {
+ if (expanded) {
+ this.classList.add('expanded');
+ } else {
+ this.classList.remove('expanded');
+ }
+ }
+
+ _computeCommentCountText(threadsLength?: number) {
+ if (threadsLength === 0) {
+ return undefined;
+ } else if (threadsLength === 1) {
+ return '1 comment';
+ } else {
+ return `${threadsLength} comments`;
+ }
+ }
+
+ _onThreadListModified() {
+ // TODO(taoalpha): this won't propagate the changes to the files
+ // should consider replacing this with either top level events
+ // or gerrit level events
+
+ // emit the event so change-view can also get updated with latest changes
+ this.dispatchEvent(
+ new CustomEvent('comment-refresh', {
+ composed: true,
+ bubbles: true,
+ })
+ );
+ }
+
+ _computeMessageContentExpanded(content?: string, tag?: ReviewInputTag) {
+ return this._computeMessageContent(content, tag, true);
+ }
+
+ _patchsetCommentSummary(commentThreads: CommentThread[] = []) {
+ const id = this.message?.id;
+ if (!id) return '';
+ const patchsetThreads = commentThreads.filter(
+ thread => thread.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS
+ );
+ for (const thread of patchsetThreads) {
+ // Find if there was a patchset level comment created through the reply
+ // dialog and use it to determine the summary
+ if (thread.comments[0].change_message_id === id) {
+ return thread.comments[0].message;
+ }
+ }
+ // Find if there is a reply to some patchset comment left
+ for (const thread of patchsetThreads) {
+ for (const comment of thread.comments) {
+ if (comment.change_message_id === id) {
+ return comment.message;
+ }
+ }
+ }
+ return '';
+ }
+
+ _computeMessageContentCollapsed(
+ content?: string,
+ tag?: ReviewInputTag,
+ commentThreads?: CommentThread[]
+ ) {
+ const summary = this._computeMessageContent(content, tag, false);
+ if (summary || !commentThreads) return summary;
+ return this._patchsetCommentSummary(commentThreads);
+ }
+
+ _computeMessageContent(
+ content = '',
+ tag: ReviewInputTag = '' as ReviewInputTag,
+ isExpanded: boolean
+ ) {
+ const isNewPatchSet =
+ tag.endsWith(':newPatchSet') || tag.endsWith(':newWipPatchSet');
+ const lines = content.split('\n');
+ const filteredLines = lines.filter(line => {
+ if (!isExpanded && line.startsWith('>')) {
+ return false;
+ }
+ if (line.startsWith('(') && line.endsWith(' comment)')) {
+ return false;
+ }
+ if (line.startsWith('(') && line.endsWith(' comments)')) {
+ return false;
+ }
+ if (!isNewPatchSet && line.match(PATCH_SET_PREFIX_PATTERN)) {
+ return false;
+ }
+ return true;
+ });
+ const mappedLines = filteredLines.map(line => {
+ // The change message formatting is not very consistent, so
+ // unfortunately we have to do a bit of tweaking here:
+ // Labels should be stripped from lines like this:
+ // Patch Set 29: Verified+1
+ // Rebase messages (which have a ':newPatchSet' tag) should be kept on
+ // lines like this:
+ // Patch Set 27: Patch Set 26 was rebased
+ if (isNewPatchSet) {
+ line = line.replace(PATCH_SET_PREFIX_PATTERN, '$1');
+ }
+ return line;
+ });
+ return mappedLines.join('\n').trim();
+ }
+
+ _computeAuthor(message: ChangeMessage) {
+ return message.author || message.updated_by;
+ }
+
+ _computeShowOnBehalfOf(message: ChangeMessage) {
+ const author = this._computeAuthor(message);
+ return !!(
+ author &&
+ message.real_author &&
+ author._account_id !== message.real_author._account_id
+ );
+ }
+
+ _computeShowReplyButton(message?: ChangeMessage, loggedIn?: boolean) {
+ return (
+ message &&
+ !!message.message &&
+ loggedIn &&
+ !this._computeIsAutomated(message)
+ );
+ }
+
+ _computeExpanded(expanded: boolean) {
+ return expanded;
+ }
+
+ _handleClick(e: Event) {
+ if (this.message?.expanded) {
+ return;
+ }
+ e.stopPropagation();
+ this.set('message.expanded', true);
+ }
+
+ _handleAuthorClick(e: Event) {
+ if (!this.message?.expanded) {
+ return;
+ }
+ e.stopPropagation();
+ this.set('message.expanded', false);
+ }
+
+ _computeIsAutomated(message: ChangeMessage) {
+ return !!(
+ message.reviewer ||
+ this._computeIsReviewerUpdate(message) ||
+ (message.tag && message.tag.startsWith('autogenerated'))
+ );
+ }
+
+ _computeIsHidden(hideAutomated: boolean, isAutomated: boolean) {
+ return hideAutomated && isAutomated;
+ }
+
+ _computeIsReviewerUpdate(message: ChangeMessage) {
+ return message.type === 'REVIEWER_UPDATE';
+ }
+
+ _getScores(message?: ChangeMessage, labelExtremes?: LabelExtreme): Score[] {
+ if (!message || !message.message || !labelExtremes) {
+ return [];
+ }
+ const line = message.message.split('\n', 1)[0];
+ const patchSetPrefix = PATCH_SET_PREFIX_PATTERN;
+ if (!line.match(patchSetPrefix)) {
+ return [];
+ }
+ const scoresRaw = line.split(patchSetPrefix)[1];
+ if (!scoresRaw) {
+ return [];
+ }
+ return scoresRaw
+ .split(' ')
+ .map(s => s.match(LABEL_TITLE_SCORE_PATTERN))
+ .filter(
+ ms => ms && ms.length === 4 && hasOwnProperty(labelExtremes, ms[2])
+ )
+ .map(ms => {
+ const label = ms?.[2];
+ const value = ms?.[1] === '-' ? 'removed' : ms?.[3];
+ return {label, value};
+ });
+ }
+
+ _computeScoreClass(score?: Score, labelExtremes?: LabelExtreme) {
+ // Polymer 2: check for undefined
+ if (score === undefined || labelExtremes === undefined) {
+ return '';
+ }
+ if (!score.value) {
+ return '';
+ }
+ if (score.value === 'removed') {
+ return 'removed';
+ }
+ const classes = [];
+ if (Number(score.value) > 0) {
+ classes.push('positive');
+ } else if (Number(score.value) < 0) {
+ classes.push('negative');
+ }
+ if (score.label) {
+ const extremes = labelExtremes[score.label];
+ if (extremes) {
+ const intScore = Number(score.value);
+ if (intScore === extremes.max) {
+ classes.push('max');
+ } else if (intScore === extremes.min) {
+ classes.push('min');
+ }
+ }
+ }
+ return classes.join(' ');
+ }
+
+ _computeClass(expanded: boolean) {
+ const classes = [];
+ classes.push(expanded ? 'expanded' : 'collapsed');
+ return classes.join(' ');
+ }
+
+ _handleAnchorClick(e: Event) {
+ e.preventDefault();
+ this.dispatchEvent(
+ new CustomEvent('message-anchor-tap', {
+ bubbles: true,
+ composed: true,
+ detail: {id: this.message?.id},
+ })
+ );
+ }
+
+ _handleReplyTap(e: Event) {
+ e.preventDefault();
+ this.dispatchEvent(
+ new CustomEvent('reply', {
+ detail: {message: this.message},
+ composed: true,
+ bubbles: true,
+ })
+ );
+ }
+
+ _handleDeleteMessage(e: Event) {
+ e.preventDefault();
+ if (!this.message || !this.message.id || !this.changeNum) return;
+ this._isDeletingChangeMsg = true;
+ this.$.restAPI
+ .deleteChangeCommitMessage(this.changeNum, this.message.id)
+ .then(() => {
+ this._isDeletingChangeMsg = false;
+ this.dispatchEvent(
+ new CustomEvent('change-message-deleted', {
+ detail: {message: this.message},
+ composed: true,
+ bubbles: true,
+ })
+ );
+ });
+ }
+
+ @observe('projectName')
+ _projectNameChanged(name: string) {
+ this.$.restAPI.getProjectConfig(name as RepoName).then(config => {
+ this._projectConfig = config;
+ });
+ }
+
+ _computeExpandToggleIcon(expanded: boolean) {
+ return expanded ? 'gr-icons:expand-less' : 'gr-icons:expand-more';
+ }
+
+ _toggleExpanded(e: Event) {
+ e.stopPropagation();
+ this.set('message.expanded', !this.message?.expanded);
+ }
+}
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.js b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.js
index c617d2c..66b8ed2 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.js
@@ -80,6 +80,7 @@
});
test('delete change message', done => {
+ element.changeNum = 314159;
element.message = {
id: '47c43261_55aa2c41',
author: {
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
deleted file mode 100644
index 7608137..0000000
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
+++ /dev/null
@@ -1,132 +0,0 @@
-/**
- * @license
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import '../../shared/gr-button/gr-button.js';
-import '../../shared/gr-dropdown/gr-dropdown.js';
-import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import '../../../styles/shared-styles.js';
-import '../../shared/gr-avatar/gr-avatar.js';
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {htmlTemplate} from './gr-account-dropdown_html.js';
-import {getUserName} from '../../../utils/display-name-util.js';
-
-const INTERPOLATE_URL_PATTERN = /\${([\w]+)}/g;
-
-/**
- * @extends PolymerElement
- */
-class GrAccountDropdown extends GestureEventListeners(
- LegacyElementMixin(
- PolymerElement)) {
- static get template() { return htmlTemplate; }
-
- static get is() { return 'gr-account-dropdown'; }
-
- static get properties() {
- return {
- account: Object,
- config: Object,
- links: {
- type: Array,
- computed: '_getLinks(_switchAccountUrl, _path)',
- },
- topContent: {
- type: Array,
- computed: '_getTopContent(account)',
- },
- _path: {
- type: String,
- value: '/',
- },
- _hasAvatars: Boolean,
- _switchAccountUrl: String,
- };
- }
-
- /** @override */
- attached() {
- super.attached();
- this._handleLocationChange();
- this.listen(window, 'location-change', '_handleLocationChange');
- this.$.restAPI.getConfig().then(cfg => {
- this.config = cfg;
-
- if (cfg && cfg.auth && cfg.auth.switch_account_url) {
- this._switchAccountUrl = cfg.auth.switch_account_url;
- } else {
- this._switchAccountUrl = '';
- }
- this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
- });
- }
-
- /** @override */
- detached() {
- super.detached();
- this.unlisten(window, 'location-change', '_handleLocationChange');
- }
-
- _getLinks(switchAccountUrl, path) {
- // Polymer 2: check for undefined
- if ([switchAccountUrl, path].includes(undefined)) {
- return undefined;
- }
-
- const links = [];
- links.push({name: 'Settings', url: '/settings/'});
- links.push({name: 'Keyboard Shortcuts', id: 'shortcuts'});
- if (switchAccountUrl) {
- const replacements = {path};
- const url = this._interpolateUrl(switchAccountUrl, replacements);
- links.push({name: 'Switch account', url, external: true});
- }
- links.push({name: 'Sign out', url: '/logout'});
- return links;
- }
-
- _getTopContent(account) {
- return [
- {text: this._accountName(account), bold: true},
- {text: account.email ? account.email : ''},
- ];
- }
-
- _handleShortcutsTap(e) {
- this.dispatchEvent(new CustomEvent('show-keyboard-shortcuts',
- {bubbles: true, composed: true}));
- }
-
- _handleLocationChange() {
- this._path =
- window.location.pathname +
- window.location.search +
- window.location.hash;
- }
-
- _interpolateUrl(url, replacements) {
- return url.replace(
- INTERPOLATE_URL_PATTERN,
- (match, p1) => replacements[p1] || '');
- }
-
- _accountName(account) {
- return getUserName(this.config, account);
- }
-}
-
-customElements.define(GrAccountDropdown.is, GrAccountDropdown);
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
new file mode 100644
index 0000000..ef0ced8
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
@@ -0,0 +1,146 @@
+/**
+ * @license
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../../shared/gr-button/gr-button';
+import '../../shared/gr-dropdown/gr-dropdown';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface';
+import '../../../styles/shared-styles';
+import '../../shared/gr-avatar/gr-avatar';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {htmlTemplate} from './gr-account-dropdown_html';
+import {getUserName} from '../../../utils/display-name-util';
+import {customElement, property} from '@polymer/decorators';
+import {AccountInfo, ServerInfo} from '../../../types/common';
+import {RestApiService} from '../../../services/services/gr-rest-api/gr-rest-api';
+
+const INTERPOLATE_URL_PATTERN = /\${([\w]+)}/g;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-account-dropdown': GrAccountDropdown;
+ }
+}
+
+export interface GrAccountDropdown {
+ $: {
+ restAPI: RestApiService & Element;
+ };
+}
+
+@customElement('gr-account-dropdown')
+export class GrAccountDropdown extends GestureEventListeners(
+ LegacyElementMixin(PolymerElement)
+) {
+ static get template() {
+ return htmlTemplate;
+ }
+
+ @property({type: Object})
+ account?: AccountInfo;
+
+ @property({type: Object})
+ config?: ServerInfo;
+
+ @property({type: Array, computed: '_getLinks(_switchAccountUrl, _path)'})
+ links?: string[];
+
+ @property({type: Array, computed: '_getTopContent(account)'})
+ topContent?: string[];
+
+ @property({type: String})
+ _path = '/';
+
+ @property({type: Boolean})
+ _hasAvatars = false;
+
+ @property({type: String})
+ _switchAccountUrl = '';
+
+ /** @override */
+ attached() {
+ super.attached();
+ this._handleLocationChange();
+ this.listen(window, 'location-change', '_handleLocationChange');
+ this.$.restAPI.getConfig().then(cfg => {
+ this.config = cfg;
+
+ if (cfg && cfg.auth && cfg.auth.switch_account_url) {
+ this._switchAccountUrl = cfg.auth.switch_account_url;
+ } else {
+ this._switchAccountUrl = '';
+ }
+ this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
+ });
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this.unlisten(window, 'location-change', '_handleLocationChange');
+ }
+
+ _getLinks(switchAccountUrl: string, path: string) {
+ // Polymer 2: check for undefined
+ if (switchAccountUrl === undefined || path === undefined) {
+ return undefined;
+ }
+
+ const links = [];
+ links.push({name: 'Settings', url: '/settings/'});
+ links.push({name: 'Keyboard Shortcuts', id: 'shortcuts'});
+ if (switchAccountUrl) {
+ const replacements = {path};
+ const url = this._interpolateUrl(switchAccountUrl, replacements);
+ links.push({name: 'Switch account', url, external: true});
+ }
+ links.push({name: 'Sign out', url: '/logout'});
+ return links;
+ }
+
+ _getTopContent(account?: AccountInfo) {
+ return [
+ {text: this._accountName(account), bold: true},
+ {text: account?.email ? account.email : ''},
+ ];
+ }
+
+ _handleShortcutsTap() {
+ this.dispatchEvent(
+ new CustomEvent('show-keyboard-shortcuts', {
+ bubbles: true,
+ composed: true,
+ })
+ );
+ }
+
+ _handleLocationChange() {
+ this._path =
+ window.location.pathname + window.location.search + window.location.hash;
+ }
+
+ _interpolateUrl(url: string, replacements: {[key: string]: string}) {
+ return url.replace(
+ INTERPOLATE_URL_PATTERN,
+ (_, p1) => replacements[p1] || ''
+ );
+ }
+
+ _accountName(account?: AccountInfo) {
+ return getUserName(this.config, account);
+ }
+}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
index 37d8819..cd43fdb 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
@@ -979,12 +979,6 @@
assert.equal(element.$.diff.displayLine, value);
});
- test('passes in commitRange', () => {
- const value = {};
- element.commitRange = value;
- assert.equal(element.$.diff.commitRange, value);
- });
-
test('passes in hidden', () => {
const value = true;
element.hidden = value;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
index 1c52b5f..2502e45 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
@@ -361,28 +361,31 @@
});
}
+ // TODO(brohlfs): Rewrite gr-diff to be agnostic of GrCommentThread, because
+ // other users of gr-diff may use different comment widgets.
_updateRanges(
addedThreadEls: GrCommentThread[],
removedThreadEls: GrCommentThread[]
) {
function commentRangeFromThreadEl(
threadEl: GrCommentThread
- ): CommentRangeLayer {
+ ): CommentRangeLayer | undefined {
const side = getSide(threadEl);
const rangeAtt = threadEl.getAttribute('range');
- if (!rangeAtt) throw Error('comment thread without range');
+ if (!rangeAtt) return undefined;
const range = JSON.parse(rangeAtt) as CommentRange;
return {side, range, hovering: false, rootId: threadEl.rootId};
}
+ // TODO(brohlfs): Rewrite `.map().filter() as ...` with `.reduce()` instead.
const addedCommentRanges = addedThreadEls
.map(commentRangeFromThreadEl)
- .filter(({range}) => range);
+ .filter(range => !!range) as CommentRangeLayer[];
const removedCommentRanges = removedThreadEls
.map(commentRangeFromThreadEl)
- .filter(({range}) => range);
+ .filter(range => !!range) as CommentRangeLayer[];
for (const removedCommentRange of removedCommentRanges) {
const i = this._commentRanges.findIndex(
cr =>
diff --git a/polygerrit-ui/app/elements/gr-app-global-var-init.js b/polygerrit-ui/app/elements/gr-app-global-var-init.ts
similarity index 69%
rename from polygerrit-ui/app/elements/gr-app-global-var-init.js
rename to polygerrit-ui/app/elements/gr-app-global-var-init.ts
index 07d37fa..e609e6f 100644
--- a/polygerrit-ui/app/elements/gr-app-global-var-init.js
+++ b/polygerrit-ui/app/elements/gr-app-global-var-init.ts
@@ -22,53 +22,81 @@
* expose these variables until plugins switch to direct import from polygerrit.
*/
-import {getAccountDisplayName, getDisplayName, getGroupDisplayName, getUserName} from '../utils/display-name-util.js';
-import {GrAnnotation} from './diff/gr-diff-highlight/gr-annotation.js';
-import {GrAttributeHelper} from './plugins/gr-attribute-helper/gr-attribute-helper.js';
-import {GrDiffLine, GrDiffLineType} from './diff/gr-diff/gr-diff-line.js';
-import {GrDiffGroup, GrDiffGroupType} from './diff/gr-diff/gr-diff-group.js';
-import {GrDiffBuilder} from './diff/gr-diff-builder/gr-diff-builder.js';
-import {GrDiffBuilderSideBySide} from './diff/gr-diff-builder/gr-diff-builder-side-by-side.js';
-import {GrDiffBuilderImage} from './diff/gr-diff-builder/gr-diff-builder-image.js';
-import {GrDiffBuilderUnified} from './diff/gr-diff-builder/gr-diff-builder-unified.js';
-import {GrDiffBuilderBinary} from './diff/gr-diff-builder/gr-diff-builder-binary.js';
-import {GrChangeActionsInterface} from './shared/gr-js-api-interface/gr-change-actions-js-api.js';
-import {GrChangeReplyInterface} from './shared/gr-js-api-interface/gr-change-reply-js-api.js';
-import {GrEditConstants} from './edit/gr-edit-constants.js';
-import {GrDomHooksManager, GrDomHook} from './plugins/gr-dom-hooks/gr-dom-hooks.js';
-import {GrEtagDecorator} from './shared/gr-rest-api-interface/gr-etag-decorator.js';
-import {GrThemeApi} from './plugins/gr-theme-api/gr-theme-api.js';
-import {SiteBasedCache, FetchPromisesCache, GrRestApiHelper} from './shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js';
-import {GrLinkTextParser} from './shared/gr-linked-text/link-text-parser.js';
-import {getPluginEndpoints, GrPluginEndpoints} from './shared/gr-js-api-interface/gr-plugin-endpoints.js';
-import {GrReviewerUpdatesParser} from './shared/gr-rest-api-interface/gr-reviewer-updates-parser.js';
-import {GrPopupInterface} from './plugins/gr-popup-interface/gr-popup-interface.js';
-import {GrCountStringFormatter} from './shared/gr-count-string-formatter/gr-count-string-formatter.js';
-import {GrReviewerSuggestionsProvider, SUGGESTIONS_PROVIDERS_USERS_TYPES} from '../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js';
-import {util} from '../scripts/util.js';
-import {page} from '../utils/page-wrapper-utils.js';
-import {appContext} from '../services/app-context.js';
-import {GrAdminApi} from './plugins/gr-admin-api/gr-admin-api.js';
-import {GrAnnotationActionsContext} from './shared/gr-js-api-interface/gr-annotation-actions-context.js';
-import {GrAnnotationActionsInterface} from './shared/gr-js-api-interface/gr-annotation-actions-js-api.js';
-import {GrChangeMetadataApi} from './plugins/gr-change-metadata-api/gr-change-metadata-api.js';
-import {GrEmailSuggestionsProvider} from '../scripts/gr-email-suggestions-provider/gr-email-suggestions-provider.js';
-import {GrGroupSuggestionsProvider} from '../scripts/gr-group-suggestions-provider/gr-group-suggestions-provider.js';
-import {GrEventHelper} from './plugins/gr-event-helper/gr-event-helper.js';
-import {GrPluginRestApi} from './shared/gr-js-api-interface/gr-plugin-rest-api.js';
-import {GrRepoApi} from './plugins/gr-repo-api/gr-repo-api.js';
-import {GrSettingsApi} from './plugins/gr-settings-api/gr-settings-api.js';
-import {GrStylesApi} from './plugins/gr-styles-api/gr-styles-api.js';
-import {getPluginLoader, PluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader.js';
-import {GrPluginActionContext} from './shared/gr-js-api-interface/gr-plugin-action-context.js';
-import {getPluginNameFromUrl, getRestAPI, PLUGIN_LOADING_TIMEOUT_MS, PRELOADED_PROTOCOL, send} from './shared/gr-js-api-interface/gr-api-utils.js';
-import {getBaseUrl} from '../utils/url-util.js';
-import {GerritNav} from './core/gr-navigation/gr-navigation.js';
-import {getRootElement} from '../scripts/rootElement.js';
-import {rangesEqual} from './diff/gr-diff/gr-diff-utils.js';
-import {RevisionInfo} from './shared/revision-info/revision-info.js';
-import {CoverageType} from '../types/types.js';
-import {_setHiddenScroll, getHiddenScroll} from '../scripts/hiddenscroll.js';
+import {
+ getAccountDisplayName,
+ getDisplayName,
+ getGroupDisplayName,
+ getUserName,
+} from '../utils/display-name-util';
+import {GrAnnotation} from './diff/gr-diff-highlight/gr-annotation';
+import {GrAttributeHelper} from './plugins/gr-attribute-helper/gr-attribute-helper';
+import {GrDiffLine, GrDiffLineType} from './diff/gr-diff/gr-diff-line';
+import {GrDiffGroup, GrDiffGroupType} from './diff/gr-diff/gr-diff-group';
+import {GrDiffBuilder} from './diff/gr-diff-builder/gr-diff-builder';
+import {GrDiffBuilderSideBySide} from './diff/gr-diff-builder/gr-diff-builder-side-by-side';
+import {GrDiffBuilderImage} from './diff/gr-diff-builder/gr-diff-builder-image';
+import {GrDiffBuilderUnified} from './diff/gr-diff-builder/gr-diff-builder-unified';
+import {GrDiffBuilderBinary} from './diff/gr-diff-builder/gr-diff-builder-binary';
+import {GrChangeActionsInterface} from './shared/gr-js-api-interface/gr-change-actions-js-api';
+import {GrChangeReplyInterface} from './shared/gr-js-api-interface/gr-change-reply-js-api';
+import {GrEditConstants} from './edit/gr-edit-constants';
+import {
+ GrDomHooksManager,
+ GrDomHook,
+} from './plugins/gr-dom-hooks/gr-dom-hooks';
+import {GrEtagDecorator} from './shared/gr-rest-api-interface/gr-etag-decorator';
+import {GrThemeApi} from './plugins/gr-theme-api/gr-theme-api';
+import {
+ SiteBasedCache,
+ FetchPromisesCache,
+ GrRestApiHelper,
+} from './shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {GrLinkTextParser} from './shared/gr-linked-text/link-text-parser';
+import {
+ getPluginEndpoints,
+ GrPluginEndpoints,
+} from './shared/gr-js-api-interface/gr-plugin-endpoints';
+import {GrReviewerUpdatesParser} from './shared/gr-rest-api-interface/gr-reviewer-updates-parser';
+import {GrPopupInterface} from './plugins/gr-popup-interface/gr-popup-interface';
+import {GrCountStringFormatter} from './shared/gr-count-string-formatter/gr-count-string-formatter';
+import {
+ GrReviewerSuggestionsProvider,
+ SUGGESTIONS_PROVIDERS_USERS_TYPES,
+} from '../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
+import {util} from '../scripts/util';
+import {page} from '../utils/page-wrapper-utils';
+import {appContext} from '../services/app-context';
+import {GrAdminApi} from './plugins/gr-admin-api/gr-admin-api';
+import {GrAnnotationActionsContext} from './shared/gr-js-api-interface/gr-annotation-actions-context';
+import {GrAnnotationActionsInterface} from './shared/gr-js-api-interface/gr-annotation-actions-js-api';
+import {GrChangeMetadataApi} from './plugins/gr-change-metadata-api/gr-change-metadata-api';
+import {GrEmailSuggestionsProvider} from '../scripts/gr-email-suggestions-provider/gr-email-suggestions-provider';
+import {GrGroupSuggestionsProvider} from '../scripts/gr-group-suggestions-provider/gr-group-suggestions-provider';
+import {GrEventHelper} from './plugins/gr-event-helper/gr-event-helper';
+import {GrPluginRestApi} from './shared/gr-js-api-interface/gr-plugin-rest-api';
+import {GrRepoApi} from './plugins/gr-repo-api/gr-repo-api';
+import {GrSettingsApi} from './plugins/gr-settings-api/gr-settings-api';
+import {GrStylesApi} from './plugins/gr-styles-api/gr-styles-api';
+import {
+ getPluginLoader,
+ PluginLoader,
+} from './shared/gr-js-api-interface/gr-plugin-loader';
+import {GrPluginActionContext} from './shared/gr-js-api-interface/gr-plugin-action-context';
+import {
+ getPluginNameFromUrl,
+ getRestAPI,
+ PLUGIN_LOADING_TIMEOUT_MS,
+ PRELOADED_PROTOCOL,
+ send,
+} from './shared/gr-js-api-interface/gr-api-utils';
+import {getBaseUrl} from '../utils/url-util';
+import {GerritNav} from './core/gr-navigation/gr-navigation';
+import {getRootElement} from '../scripts/rootElement';
+import {rangesEqual} from './diff/gr-diff/gr-diff-utils';
+import {RevisionInfo} from './shared/revision-info/revision-info';
+import {CoverageType} from '../types/types';
+import {_setHiddenScroll, getHiddenScroll} from '../scripts/hiddenscroll';
+import {GerritGlobal} from './shared/gr-js-api-interface/gr-gerrit';
export function initGlobalVariables() {
window.GrDisplayNameUtils = {
@@ -131,7 +159,7 @@
PLUGIN_LOADING_TIMEOUT_MS,
};
- window.Gerrit = window.Gerrit || {};
+ window.Gerrit = (window.Gerrit || {}) as GerritGlobal;
window.Gerrit.Nav = GerritNav;
window.Gerrit.getRootElement = getRootElement;
window.Gerrit.Auth = appContext.authService;
@@ -140,10 +168,10 @@
// TODO: should define as a getter
window.Gerrit._endpoints = getPluginEndpoints();
- window.Gerrit.slotToContent = slot => slot;
+ // TODO(TS): seems not used, probably just remove
+ window.Gerrit.slotToContent = (slot: any) => slot;
window.Gerrit.rangesEqual = rangesEqual;
- window.Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES =
- SUGGESTIONS_PROVIDERS_USERS_TYPES;
+ window.Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES = SUGGESTIONS_PROVIDERS_USERS_TYPES;
window.Gerrit.RevisionInfo = RevisionInfo;
window.Gerrit.CoverageType = CoverageType;
Object.defineProperty(window.Gerrit, 'hiddenscroll', {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
deleted file mode 100644
index 76899b3..0000000
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
+++ /dev/null
@@ -1,369 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 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 '../gr-account-chip/gr-account-chip.js';
-import '../gr-account-entry/gr-account-entry.js';
-import '../../../styles/shared-styles.js';
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {htmlTemplate} from './gr-account-list_html.js';
-import {appContext} from '../../../services/app-context.js';
-
-const VALID_EMAIL_ALERT = 'Please input a valid email.';
-
-/**
- * @extends PolymerElement
- */
-class GrAccountList extends GestureEventListeners(
- LegacyElementMixin(PolymerElement)) {
- static get template() { return htmlTemplate; }
-
- static get is() { return 'gr-account-list'; }
- /**
- * Fired when user inputs an invalid email address.
- *
- * @event show-alert
- */
-
- static get properties() {
- return {
- accounts: {
- type: Array,
- value() { return []; },
- notify: true,
- },
- change: Object,
- filter: Function,
- placeholder: String,
- disabled: {
- type: Function,
- value: false,
- },
-
- /**
- * Returns suggestions and convert them to list item
- *
- * @type {Gerrit.GrSuggestionsProvider}
- */
- suggestionsProvider: {
- type: Object,
- },
-
- /**
- * Needed for template checking since value is initially set to null.
- *
- * @type {?Object}
- */
- pendingConfirmation: {
- type: Object,
- value: null,
- notify: true,
- },
- readonly: {
- type: Boolean,
- value: false,
- },
- /**
- * When true, allows for non-suggested inputs to be added.
- */
- allowAnyInput: {
- type: Boolean,
- value: false,
- },
-
- /**
- * Array of values (groups/accounts) that are removable. When this prop is
- * undefined, all values are removable.
- */
- removableValues: Array,
- maxCount: {
- type: Number,
- value: 0,
- },
-
- /**
- * Returns suggestion items
- *
- * @type {!function(string): Promise<Array<Gerrit.GrSuggestionItem>>}
- */
- _querySuggestions: {
- type: Function,
- value() {
- return input => this._getSuggestions(input);
- },
- },
-
- /**
- * Set to true to disable suggestions on empty input.
- */
- skipSuggestOnEmpty: {
- type: Boolean,
- value: false,
- },
- };
- }
-
- constructor() {
- super();
- this.reporting = appContext.reportingService;
- }
-
- /** @override */
- created() {
- super.created();
- this.addEventListener('remove',
- e => this._handleRemove(e));
- }
-
- get accountChips() {
- return Array.from(
- this.root.querySelectorAll('gr-account-chip'));
- }
-
- get focusStart() {
- return this.$.entry.focusStart;
- }
-
- _getSuggestions(input) {
- if (this.skipSuggestOnEmpty && !input) {
- return Promise.resolve([]);
- }
- const provider = this.suggestionsProvider;
- if (!provider) {
- return Promise.resolve([]);
- }
- return provider.getSuggestions(input).then(suggestions => {
- if (!suggestions) { return []; }
- if (this.filter) {
- suggestions = suggestions.filter(this.filter);
- }
- return suggestions.map(suggestion =>
- provider.makeSuggestionItem(suggestion));
- });
- }
-
- _handleAdd(e) {
- this.addAccountItem(e.detail.value);
- }
-
- addAccountItem(item) {
- // Append new account or group to the accounts property. We add our own
- // internal properties to the account/group here, so we clone the object
- // to avoid cluttering up the shared change object.
- let itemTypeAdded = 'unknown';
- if (item.account) {
- const account =
- {...item.account, _pendingAdd: true};
- this.push('accounts', account);
- itemTypeAdded = 'account';
- } else if (item.group) {
- if (item.confirm) {
- this.pendingConfirmation = item;
- return;
- }
- const group = {...item.group,
- _pendingAdd: true, _group: true};
- this.push('accounts', group);
- itemTypeAdded = 'group';
- } else if (this.allowAnyInput) {
- if (!item.includes('@')) {
- // Repopulate the input with what the user tried to enter and have
- // a toast tell them why they can't enter it.
- this.$.entry.setText(item);
- this.dispatchEvent(new CustomEvent('show-alert', {
- detail: {message: VALID_EMAIL_ALERT},
- bubbles: true,
- composed: true,
- }));
- return false;
- } else {
- const account = {email: item, _pendingAdd: true};
- this.push('accounts', account);
- itemTypeAdded = 'email';
- }
- }
-
- this.reporting.reportInteraction(`Add to ${this.id}`, {itemTypeAdded});
- this.pendingConfirmation = null;
- return true;
- }
-
- confirmGroup(group) {
- group = {
- ...group, confirmed: true, _pendingAdd: true, _group: true};
- this.push('accounts', group);
- this.pendingConfirmation = null;
- }
-
- _computeChipClass(account) {
- const classes = [];
- if (account._group) {
- classes.push('group');
- }
- if (account._pendingAdd) {
- classes.push('pendingAdd');
- }
- return classes.join(' ');
- }
-
- _accountMatches(a, b) {
- if (a && b) {
- if (a._account_id) {
- return a._account_id === b._account_id;
- }
- if (a.email) {
- return a.email === b.email;
- }
- }
- return a === b;
- }
-
- _computeRemovable(account, readonly) {
- if (readonly) { return false; }
- if (this.removableValues) {
- for (let i = 0; i < this.removableValues.length; i++) {
- if (this._accountMatches(this.removableValues[i], account)) {
- return true;
- }
- }
- return !!account._pendingAdd;
- }
- return true;
- }
-
- _handleRemove(e) {
- const toRemove = e.detail.account;
- this.removeAccount(toRemove);
- this.$.entry.focus();
- }
-
- removeAccount(toRemove) {
- if (!toRemove || !this._computeRemovable(toRemove, this.readonly)) {
- return;
- }
- for (let i = 0; i < this.accounts.length; i++) {
- let matches;
- const account = this.accounts[i];
- if (toRemove._group) {
- matches = toRemove.id === account.id;
- } else {
- matches = this._accountMatches(toRemove, account);
- }
- if (matches) {
- this.splice('accounts', i, 1);
- this.reporting.reportInteraction(`Remove from ${this.id}`);
- return;
- }
- }
- console.warn('received remove event for missing account', toRemove);
- }
-
- _getNativeInput(paperInput) {
- // In Polymer 2 inputElement isn't nativeInput anymore
- return paperInput.$.nativeInput || paperInput.inputElement;
- }
-
- _handleInputKeydown(e) {
- const input = this._getNativeInput(e.detail.input);
- if (input.selectionStart !== input.selectionEnd ||
- input.selectionStart !== 0) {
- return;
- }
- switch (e.detail.keyCode) {
- case 8: // Backspace
- this.removeAccount(this.accounts[this.accounts.length - 1]);
- break;
- case 37: // Left arrow
- if (this.accountChips[this.accountChips.length - 1]) {
- this.accountChips[this.accountChips.length - 1].focus();
- }
- break;
- }
- }
-
- _handleChipKeydown(e) {
- const chip = e.target;
- const chips = this.accountChips;
- const index = chips.indexOf(chip);
- switch (e.keyCode) {
- case 8: // Backspace
- case 13: // Enter
- case 32: // Spacebar
- case 46: // Delete
- this.removeAccount(chip.account);
- // Splice from this array to avoid inconsistent ordering of
- // event handling.
- chips.splice(index, 1);
- if (index < chips.length) {
- chips[index].focus();
- } else if (index > 0) {
- chips[index - 1].focus();
- } else {
- this.$.entry.focus();
- }
- break;
- case 37: // Left arrow
- if (index > 0) {
- chip.blur();
- chips[index - 1].focus();
- }
- break;
- case 39: // Right arrow
- chip.blur();
- if (index < chips.length - 1) {
- chips[index + 1].focus();
- } else {
- this.$.entry.focus();
- }
- break;
- }
- }
-
- /**
- * Submit the text of the entry as a reviewer value, if it exists. If it is
- * a successful submit of the text, clear the entry value.
- *
- * @return {boolean} If there is text in the entry, return true if the
- * submission was successful and false if not. If there is no text,
- * return true.
- */
- submitEntryText() {
- const text = this.$.entry.getText();
- if (!text.length) { return true; }
- const wasSubmitted = this.addAccountItem(text);
- if (wasSubmitted) { this.$.entry.clear(); }
- return wasSubmitted;
- }
-
- additions() {
- return this.accounts
- .filter(account => account._pendingAdd)
- .map(account => {
- if (account._group) {
- return {group: account};
- } else {
- return {account};
- }
- });
- }
-
- _computeEntryHidden(maxCount, accountsRecord, readonly) {
- return (maxCount && maxCount <= accountsRecord.base.length) || readonly;
- }
-}
-
-customElements.define(GrAccountList.is, GrAccountList);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
new file mode 100644
index 0000000..113a881
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
@@ -0,0 +1,452 @@
+/**
+ * @license
+ * Copyright (C) 2016 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 '../gr-account-chip/gr-account-chip';
+import '../gr-account-entry/gr-account-entry';
+import '../../../styles/shared-styles';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {htmlTemplate} from './gr-account-list_html';
+import {appContext} from '../../../services/app-context';
+import {customElement, property} from '@polymer/decorators';
+import {
+ ChangeInfo,
+ Suggestion,
+ AccountInfo,
+ GroupInfo,
+} from '../../../types/common';
+import {
+ GrReviewerSuggestionsProvider,
+ SuggestionItem,
+} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
+import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
+import {GrAccountEntry} from '../gr-account-entry/gr-account-entry';
+import {PaperInputElement} from '@polymer/paper-input/paper-input';
+import {GrAccountChip} from '../gr-account-chip/gr-account-chip';
+import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
+
+const VALID_EMAIL_ALERT = 'Please input a valid email.';
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-account-list': GrAccountList;
+ }
+}
+
+export interface GrAccountList {
+ $: {
+ entry: GrAccountEntry;
+ };
+}
+
+/**
+ * For item added with account info
+ */
+export interface AccountObjectInput {
+ account: AccountInfo;
+}
+
+/**
+ * For item added with group info
+ */
+export interface GroupObjectInput {
+ group: GroupInfo;
+ confirm: boolean;
+}
+
+/** Supported input to be added */
+export type RawAccountInput = string | AccountObjectInput | GroupObjectInput;
+
+// type guards for AccountObjectInput and GroupObjectInput
+function isAccountObject(x: RawAccountInput): x is AccountObjectInput {
+ return !!(x as AccountObjectInput).account;
+}
+
+function isGroupObjectInput(x: RawAccountInput): x is GroupObjectInput {
+ return !!(x as GroupObjectInput).group;
+}
+
+// Internal input type with account info
+interface AccountInfoInput extends AccountInfo {
+ _group?: boolean;
+ _account?: boolean;
+ _pendingAdd?: boolean;
+ confirmed?: boolean;
+}
+
+// Internal input type with group info
+interface GroupInfoInput extends GroupInfo {
+ _group?: boolean;
+ _account?: boolean;
+ _pendingAdd?: boolean;
+ confirmed?: boolean;
+}
+
+type AccountInput = AccountInfoInput | GroupInfoInput;
+
+@customElement('gr-account-list')
+export class GrAccountList extends GestureEventListeners(
+ LegacyElementMixin(PolymerElement)
+) {
+ static get template() {
+ return htmlTemplate;
+ }
+
+ /**
+ * Fired when user inputs an invalid email address.
+ *
+ * @event show-alert
+ */
+
+ @property({type: Array, notify: true})
+ accounts: AccountInput[] = [];
+
+ @property({type: Object})
+ change?: ChangeInfo;
+
+ @property({type: Object})
+ filter?: (input: Suggestion) => boolean;
+
+ @property({type: String})
+ placeholder = '';
+
+ @property({type: Boolean})
+ disabled = false;
+
+ /**
+ * Returns suggestions and convert them to list item
+ */
+ @property({type: Object})
+ suggestionsProvider?: GrReviewerSuggestionsProvider;
+
+ /**
+ * Needed for template checking since value is initially set to null.
+ */
+ @property({type: Object, notify: true})
+ pendingConfirmation: RawAccountInput | null = null;
+
+ @property({type: Boolean})
+ readonly = false;
+
+ /**
+ * When true, allows for non-suggested inputs to be added.
+ */
+ @property({type: Boolean})
+ allowAnyInput = false;
+
+ /**
+ * Array of values (groups/accounts) that are removable. When this prop is
+ * undefined, all values are removable.
+ */
+ @property({type: Array})
+ removableValues?: AccountInput[];
+
+ @property({type: Number})
+ maxCount = 0;
+
+ /**
+ * Returns suggestion items
+ */
+ @property({type: Object})
+ _querySuggestions: (input: string) => Promise<SuggestionItem[]>;
+
+ /**
+ * Set to true to disable suggestions on empty input.
+ */
+ @property({type: Boolean})
+ skipSuggestOnEmpty = false;
+
+ reporting: ReportingService;
+
+ constructor() {
+ super();
+ this.reporting = appContext.reportingService;
+ this._querySuggestions = input => this._getSuggestions(input);
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('remove', e =>
+ this._handleRemove(e as CustomEvent<{account: AccountInput}>)
+ );
+ }
+
+ get accountChips() {
+ return Array.from(this.root?.querySelectorAll('gr-account-chip') || []);
+ }
+
+ get focusStart() {
+ return this.$.entry.focusStart;
+ }
+
+ _getSuggestions(input: string) {
+ if (this.skipSuggestOnEmpty && !input) {
+ return Promise.resolve([]);
+ }
+ const provider = this.suggestionsProvider;
+ if (!provider) {
+ return Promise.resolve([]);
+ }
+ return provider.getSuggestions(input).then(suggestions => {
+ if (!suggestions) {
+ return [];
+ }
+ if (this.filter) {
+ suggestions = suggestions.filter(this.filter);
+ }
+ return suggestions.map(suggestion =>
+ provider.makeSuggestionItem(suggestion)
+ );
+ });
+ }
+
+ _handleAdd(e: CustomEvent<{value: RawAccountInput}>) {
+ this.addAccountItem(e.detail.value);
+ }
+
+ addAccountItem(item: RawAccountInput) {
+ // Append new account or group to the accounts property. We add our own
+ // internal properties to the account/group here, so we clone the object
+ // to avoid cluttering up the shared change object.
+ let itemTypeAdded = 'unknown';
+ if (isAccountObject(item)) {
+ const account = {...item.account, _pendingAdd: true};
+ this.push('accounts', account);
+ itemTypeAdded = 'account';
+ } else if (isGroupObjectInput(item)) {
+ if (item.confirm) {
+ this.pendingConfirmation = item;
+ return;
+ }
+ const group = {...item.group, _pendingAdd: true, _group: true};
+ this.push('accounts', group);
+ itemTypeAdded = 'group';
+ } else if (this.allowAnyInput) {
+ if (!item.includes('@')) {
+ // Repopulate the input with what the user tried to enter and have
+ // a toast tell them why they can't enter it.
+ this.$.entry.setText(item);
+ this.dispatchEvent(
+ new CustomEvent('show-alert', {
+ detail: {message: VALID_EMAIL_ALERT},
+ bubbles: true,
+ composed: true,
+ })
+ );
+ return false;
+ } else {
+ const account = {email: item, _pendingAdd: true};
+ this.push('accounts', account);
+ itemTypeAdded = 'email';
+ }
+ }
+
+ this.reporting.reportInteraction(`Add to ${this.id}`, {itemTypeAdded});
+ this.pendingConfirmation = null;
+ return true;
+ }
+
+ confirmGroup(group: GroupObjectInput) {
+ this.push('accounts', {
+ ...group,
+ confirmed: true,
+ _pendingAdd: true,
+ _group: true,
+ });
+ this.pendingConfirmation = null;
+ }
+
+ _computeChipClass(account: AccountInput) {
+ const classes = [];
+ if (account._group) {
+ classes.push('group');
+ }
+ if (account._pendingAdd) {
+ classes.push('pendingAdd');
+ }
+ return classes.join(' ');
+ }
+
+ _accountMatches(a: AccountInput, b: AccountInput) {
+ // TODO(TS): seems a & b always exists ?
+ if (a && b) {
+ // both conditions are checking against AccountInfo
+ // and only check a not b.. typeguard won't work very good without
+ // changing logic, so keep it as inline casting
+ if ((a as AccountInfoInput)._account_id) {
+ return (
+ (a as AccountInfoInput)._account_id ===
+ (b as AccountInfoInput)._account_id
+ );
+ }
+ if ((a as AccountInfoInput).email) {
+ return (a as AccountInfoInput).email === (b as AccountInfoInput).email;
+ }
+ }
+ return a === b;
+ }
+
+ _computeRemovable(account: AccountInput, readonly: boolean) {
+ if (readonly) {
+ return false;
+ }
+ if (this.removableValues) {
+ for (let i = 0; i < this.removableValues.length; i++) {
+ if (this._accountMatches(this.removableValues[i], account)) {
+ return true;
+ }
+ }
+ return !!account._pendingAdd;
+ }
+ return true;
+ }
+
+ _handleRemove(e: CustomEvent<{account: AccountInput}>) {
+ const toRemove = e.detail.account;
+ this.removeAccount(toRemove);
+ this.$.entry.focus();
+ }
+
+ removeAccount(toRemove?: AccountInput) {
+ if (!toRemove || !this._computeRemovable(toRemove, this.readonly)) {
+ return;
+ }
+ for (let i = 0; i < this.accounts.length; i++) {
+ let matches;
+ const account = this.accounts[i];
+ if (toRemove._group) {
+ matches =
+ (toRemove as GroupInfoInput).id === (account as GroupInfoInput).id;
+ } else {
+ matches = this._accountMatches(toRemove, account);
+ }
+ if (matches) {
+ this.splice('accounts', i, 1);
+ this.reporting.reportInteraction(`Remove from ${this.id}`);
+ return;
+ }
+ }
+ console.warn('received remove event for missing account', toRemove);
+ }
+
+ _getNativeInput(paperInput: PaperInputElement) {
+ // In Polymer 2 inputElement isn't nativeInput anymore
+ return (paperInput.$.nativeInput ||
+ paperInput.inputElement) as HTMLTextAreaElement;
+ }
+
+ _handleInputKeydown(
+ e: CustomEvent<{input: PaperInputElement; keyCode: number}>
+ ) {
+ const input = this._getNativeInput(e.detail.input);
+ if (
+ input.selectionStart !== input.selectionEnd ||
+ input.selectionStart !== 0
+ ) {
+ return;
+ }
+ switch (e.detail.keyCode) {
+ case 8: // Backspace
+ this.removeAccount(this.accounts[this.accounts.length - 1]);
+ break;
+ case 37: // Left arrow
+ if (this.accountChips[this.accountChips.length - 1]) {
+ this.accountChips[this.accountChips.length - 1].focus();
+ }
+ break;
+ }
+ }
+
+ _handleChipKeydown(e: KeyboardEvent) {
+ const chip = e.target as GrAccountChip;
+ const chips = this.accountChips;
+ const index = chips.indexOf(chip);
+ switch (e.keyCode) {
+ case 8: // Backspace
+ case 13: // Enter
+ case 32: // Spacebar
+ case 46: // Delete
+ this.removeAccount(chip.account);
+ // Splice from this array to avoid inconsistent ordering of
+ // event handling.
+ chips.splice(index, 1);
+ if (index < chips.length) {
+ chips[index].focus();
+ } else if (index > 0) {
+ chips[index - 1].focus();
+ } else {
+ this.$.entry.focus();
+ }
+ break;
+ case 37: // Left arrow
+ if (index > 0) {
+ chip.blur();
+ chips[index - 1].focus();
+ }
+ break;
+ case 39: // Right arrow
+ chip.blur();
+ if (index < chips.length - 1) {
+ chips[index + 1].focus();
+ } else {
+ this.$.entry.focus();
+ }
+ break;
+ }
+ }
+
+ /**
+ * Submit the text of the entry as a reviewer value, if it exists. If it is
+ * a successful submit of the text, clear the entry value.
+ *
+ * @return If there is text in the entry, return true if the
+ * submission was successful and false if not. If there is no text,
+ * return true.
+ */
+ submitEntryText() {
+ const text = this.$.entry.getText();
+ if (!text.length) {
+ return true;
+ }
+ const wasSubmitted = this.addAccountItem(text);
+ if (wasSubmitted) {
+ this.$.entry.clear();
+ }
+ return wasSubmitted;
+ }
+
+ additions() {
+ return this.accounts
+ .filter(account => account._pendingAdd)
+ .map(account => {
+ if (account._group) {
+ return {group: account};
+ } else {
+ return {account};
+ }
+ });
+ }
+
+ _computeEntryHidden(
+ maxCount: number,
+ accountsRecord: PolymerDeepPropertyChange<AccountInput[], AccountInput[]>,
+ readonly: boolean
+ ) {
+ return (maxCount && maxCount <= accountsRecord.base.length) || readonly;
+ }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
index 7f0f5b7..9e9a0ea 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
@@ -19,7 +19,11 @@
* This defines the Gerrit instance. All methods directly attached to Gerrit
* should be defined or linked here.
*/
-import {getPluginLoader, PluginOptionMap} from './gr-plugin-loader';
+import {
+ getPluginLoader,
+ PluginOptionMap,
+ PluginLoader,
+} from './gr-plugin-loader';
import {getRestAPI, send} from './gr-api-utils';
import {appContext} from '../../../services/app-context';
import {PluginApi} from '../../plugins/gr-plugin-types';
@@ -29,8 +33,15 @@
EventCallback,
EventEmitterService,
} from '../../../services/gr-event-interface/gr-event-interface';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {getRootElement} from '../../../scripts/rootElement';
+import {GrPluginEndpoints} from './gr-plugin-endpoints';
+import {rangesEqual} from '../../diff/gr-diff/gr-diff-utils';
+import {SUGGESTIONS_PROVIDERS_USERS_TYPES} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
+import {CoverageType} from '../../../types/types';
+import {RevisionInfo} from '../revision-info/revision-info';
-interface GerritGlobal extends EventEmitterService {
+export interface GerritGlobal extends EventEmitterService {
flushPreinstalls?(): void;
css(rule: string): string;
install(
@@ -60,6 +71,18 @@
_isPluginLoaded(pathOrUrl: string): boolean;
_eventEmitter: EventEmitterService;
_customStyleSheet: CSSStyleSheet;
+
+ // exposed methods
+ Nav: typeof GerritNav;
+ Auth: typeof appContext.authService;
+ getRootElement: typeof getRootElement;
+ _pluginLoader: PluginLoader;
+ _endpoints: GrPluginEndpoints;
+ slotToContent(slot: unknown): unknown;
+ rangesEqual: typeof rangesEqual;
+ SUGGESTIONS_PROVIDERS_USERS_TYPES: typeof SUGGESTIONS_PROVIDERS_USERS_TYPES;
+ CoverageType: typeof CoverageType;
+ RevisionInfo: typeof RevisionInfo;
}
/**
@@ -67,7 +90,7 @@
* This needs to happen before Gerrit as plugin bundle overrides the Gerrit.
*/
function flushPreinstalls() {
- const Gerrit = window.Gerrit as GerritGlobal;
+ const Gerrit = window.Gerrit;
if (Gerrit.flushPreinstalls) {
Gerrit.flushPreinstalls();
}
@@ -75,9 +98,9 @@
export const _testOnly_flushPreinstalls = flushPreinstalls;
export function initGerritPluginApi() {
- window.Gerrit = {};
+ window.Gerrit = (window.Gerrit || {}) as GerritGlobal;
flushPreinstalls();
- initGerritPluginsMethods(window.Gerrit as GerritGlobal);
+ initGerritPluginsMethods(window.Gerrit);
// Preloaded plugins should be installed after Gerrit.install() is set,
// since plugin preloader substitutes Gerrit.install() temporarily.
// (Gerrit.install() is set in initGerritPluginsMethods)
@@ -86,7 +109,7 @@
export function _testOnly_initGerritPluginApi(): GerritGlobal {
initGerritPluginApi();
- return window.Gerrit as GerritGlobal;
+ return window.Gerrit;
}
export function deprecatedDelete(
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
index a2d1e1a..3955cd4 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
@@ -23,7 +23,6 @@
<style include="shared-styles">
.placeholder {
color: var(--deemphasized-text-color);
- padding-top: var(--spacing-xs);
}
.hidden {
display: none;
diff --git a/polygerrit-ui/app/run_test.sh b/polygerrit-ui/app/run_test.sh
index 2ca1118..0ceca3c 100755
--- a/polygerrit-ui/app/run_test.sh
+++ b/polygerrit-ui/app/run_test.sh
@@ -6,6 +6,11 @@
bazel_bin=bazel
fi
+# At least temporarily we want to know what is going on even when all tests are
+# passing, so we have a better chance of debugging what happens in CI test runs
+# that were supposed to catch test failures, but did not.
${bazel_bin} test \
"$@" \
+ --test_verbose_timeout_warnings \
+ --test_output=all \
//polygerrit-ui:karma_test
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
index d3b652a..29cdd1e 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
@@ -155,6 +155,7 @@
// TODO(dmfilippov): TS-fix-any unclear what is context
const catchErrors = function (opt_context?: any) {
const context = opt_context || window;
+ const oldOnError = context.onerror;
context.onerror = (
event: Event | string,
source?: string,
@@ -162,7 +163,7 @@
colno?: number,
error?: Error
) => {
- return onError(context.onerror, event, source, lineno, colno, error);
+ return onError(oldOnError, event, source, lineno, colno, error);
};
context.addEventListener(
'unhandledrejection',
diff --git a/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts b/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts
index 9fb2882..18c24d2 100644
--- a/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts
+++ b/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts
@@ -74,6 +74,7 @@
NameToProjectInfoMap,
ProjectInput,
AccountId,
+ ChangeMessageId,
} from '../../../types/common';
import {ParsedChangeInfo} from '../../../elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser';
import {HttpMethod} from '../../../constants/constants';
@@ -595,4 +596,9 @@
account: AccountId,
label: string
): Promise<Response>;
+
+ deleteChangeCommitMessage(
+ changeNum: ChangeNum,
+ messageId: ChangeMessageId
+ ): Promise<Response>;
}
diff --git a/polygerrit-ui/app/test/common-test-setup.js b/polygerrit-ui/app/test/common-test-setup.js
index 004daf7..500187a 100644
--- a/polygerrit-ui/app/test/common-test-setup.js
+++ b/polygerrit-ui/app/test/common-test-setup.js
@@ -31,6 +31,7 @@
import sinon from 'sinon/pkg/sinon-esm.js';
import {safeTypesBridge} from '../utils/safe-types-util.js';
import {_testOnly_initGerritPluginApi} from '../elements/shared/gr-js-api-interface/gr-gerrit.js';
+import {initGlobalVariables} from '../elements/gr-app-global-var-init.js';
window.sinon = sinon;
security.polymer_resin.install({
@@ -60,6 +61,9 @@
};
setup(() => {
+ window.Gerrit = {};
+ initGlobalVariables();
+
// If the following asserts fails - then window.stub is
// overwritten by some other code.
assert.equal(cleanups.length, 0);
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 52adb7b..e131b4f 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -410,6 +410,8 @@
export interface ChangeMessageInfo {
id: ChangeMessageId;
author?: AccountInfo;
+ reviewer?: AccountInfo;
+ updated_by?: AccountInfo;
real_author?: AccountInfo;
date: Timestamp;
message: string;
diff --git a/polygerrit-ui/app/types/globals.ts b/polygerrit-ui/app/types/globals.ts
index bdfb8c4..645c991 100644
--- a/polygerrit-ui/app/types/globals.ts
+++ b/polygerrit-ui/app/types/globals.ts
@@ -36,7 +36,7 @@
): void;
ASSETS_PATH?: string;
// TODO(TS): define gerrit type
- Gerrit?: unknown;
+ Gerrit?: any;
// TODO(TS): define polymer type
Polymer?: unknown;
// TODO(TS): remove page when better workaround is found
@@ -51,6 +51,56 @@
dashboardPage?: string;
};
STATIC_RESOURCE_PATH?: string;
+
+ /** Enhancements on Gr elements or utils */
+ // TODO(TS): should clean up those and removing them may break certain plugin behaviors
+ // TODO(TS): as @brohlfs suggested, to avoid importing anything from elements/ to types/
+ // use any for them for now
+ GrDisplayNameUtils: any;
+ GrAnnotation: any;
+ GrAttributeHelper: any;
+ GrDiffLine: any;
+ GrDiffLineType: any;
+ GrDiffGroup: any;
+ GrDiffGroupType: any;
+ GrDiffBuilder: any;
+ GrDiffBuilderSideBySide: any;
+ GrDiffBuilderImage: any;
+ GrDiffBuilderUnified: any;
+ GrDiffBuilderBinary: any;
+ GrChangeActionsInterface: any;
+ GrChangeReplyInterface: any;
+ GrEditConstants: any;
+ GrDomHooksManager: any;
+ GrDomHook: any;
+ GrEtagDecorator: any;
+ GrThemeApi: any;
+ SiteBasedCache: any;
+ FetchPromisesCache: any;
+ GrRestApiHelper: any;
+ GrLinkTextParser: any;
+ GrPluginEndpoints: any;
+ GrReviewerUpdatesParser: any;
+ GrPopupInterface: any;
+ GrCountStringFormatter: any;
+ GrReviewerSuggestionsProvider: any;
+ util: any;
+ Auth: any;
+ EventEmitter: any;
+ GrAdminApi: any;
+ GrAnnotationActionsContext: any;
+ GrAnnotationActionsInterface: any;
+ GrChangeMetadataApi: any;
+ GrEmailSuggestionsProvider: any;
+ GrGroupSuggestionsProvider: any;
+ GrEventHelper: any;
+ GrPluginRestApi: any;
+ GrRepoApi: any;
+ GrSettingsApi: any;
+ GrStylesApi: any;
+ PluginLoader: any;
+ GrPluginActionContext: any;
+ _apiUtils: {};
}
interface Performance {