Merge "Clean up disrespectful terms"
diff --git a/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java b/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
index 3bb88e5..03ecd91 100644
--- a/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
+++ b/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
@@ -40,15 +40,32 @@
private final DynamicItem<OAuthTokenEncrypter> encrypter;
+ public enum AccountIdSerializer implements CacheSerializer<Account.Id> {
+ INSTANCE;
+
+ private final Converter<Account.Id, Integer> converter =
+ Converter.from(Account.Id::get, Account::id);
+
+ private final Converter<Integer, Account.Id> reverse = converter.reverse();
+
+ @Override
+ public byte[] serialize(Account.Id object) {
+ return IntegerCacheSerializer.INSTANCE.serialize(converter.convert(object));
+ }
+
+ @Override
+ public Account.Id deserialize(byte[] in) {
+ return reverse.convert(IntegerCacheSerializer.INSTANCE.deserialize(in));
+ }
+ }
+
public static Module module() {
return new CacheModule() {
@Override
protected void configure() {
persist(OAUTH_TOKENS, Account.Id.class, OAuthToken.class)
.version(1)
- .keySerializer(
- CacheSerializer.convert(
- IntegerCacheSerializer.INSTANCE, Converter.from(Account.Id::get, Account::id)))
+ .keySerializer(AccountIdSerializer.INSTANCE)
.valueSerializer(new Serializer());
}
};
diff --git a/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java b/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
index 586f1bc..64fa74f 100644
--- a/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
+++ b/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
@@ -19,6 +19,7 @@
import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.Account;
import com.google.gerrit.extensions.auth.oauth.OAuthToken;
import com.google.gerrit.proto.testing.SerializedClassSubject;
import com.google.gerrit.server.cache.proto.Cache.OAuthTokenProto;
@@ -71,6 +72,23 @@
assertThat(s.deserialize(serializedWithEmptyString)).isEqualTo(tokenWithNull);
}
+ @Test
+ public void serializeAndDeserializeBackAccountId() {
+ OAuthTokenCache.AccountIdSerializer serializer = OAuthTokenCache.AccountIdSerializer.INSTANCE;
+
+ Account.Id id = Account.id(1234);
+ assertThat(serializer.deserialize(serializer.serialize(id))).isEqualTo(id);
+ }
+
+ // Anonymous classes can break some cache implementations that try to parse the
+ // serializer class name and expect a well-defined class name: test that
+ // OAuthTokenCache.AccountIdSerializer is not an anonymous class.
+ @Test
+ public void accountIdSerializerIsNotAnAnonymousClass() {
+ assertThat(OAuthTokenCache.AccountIdSerializer.INSTANCE.getDeclaringClass().getSimpleName())
+ .isNotEmpty();
+ }
+
/** See {@link SerializedClassSubject} for background and what to do if this test fails. */
@Test
public void oAuthTokenFields() throws Exception {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
deleted file mode 100644
index 4989365..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
+++ /dev/null
@@ -1,309 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 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-input/iron-input.js';
-import '../../../styles/gr-form-styles.js';
-import '../../../styles/gr-table-styles.js';
-import '../../../styles/shared-styles.js';
-import '../../shared/gr-account-link/gr-account-link.js';
-import '../../shared/gr-button/gr-button.js';
-import '../../shared/gr-date-formatter/gr-date-formatter.js';
-import '../../shared/gr-dialog/gr-dialog.js';
-import '../../shared/gr-list-view/gr-list-view.js';
-import '../../shared/gr-overlay/gr-overlay.js';
-import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import '../gr-create-pointer-dialog/gr-create-pointer-dialog.js';
-import '../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js';
-import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {htmlTemplate} from './gr-repo-detail-list_html.js';
-import {ListViewMixin} from '../../../mixins/gr-list-view-mixin/gr-list-view-mixin.js';
-import {encodeURL} from '../../../utils/url-util.js';
-
-const DETAIL_TYPES = {
- BRANCHES: 'branches',
- TAGS: 'tags',
-};
-
-const PGP_START = '-----BEGIN PGP SIGNATURE-----';
-
-/**
- * @appliesMixin ListViewMixin
- * @extends PolymerElement
- */
-class GrRepoDetailList extends ListViewMixin(GestureEventListeners(
- LegacyElementMixin(
- PolymerElement))) {
- static get template() { return htmlTemplate; }
-
- static get is() { return 'gr-repo-detail-list'; }
-
- static get properties() {
- return {
- /**
- * URL params passed from the router.
- */
- params: {
- type: Object,
- observer: '_paramsChanged',
- },
- /**
- * The kind of detail we are displaying, possibilities are determined by
- * the const DETAIL_TYPES.
- */
- detailType: String,
-
- _editing: {
- type: Boolean,
- value: false,
- },
- _isOwner: {
- type: Boolean,
- value: false,
- },
- _loggedIn: {
- type: Boolean,
- value: false,
- },
- /**
- * Offset of currently visible query results.
- */
- _offset: Number,
- _repo: Object,
- _items: Array,
- /**
- * Because we request one more than the projectsPerPage, _shownProjects
- * maybe one less than _projects.
- */
- _shownItems: {
- type: Array,
- computed: 'computeShownItems(_items)',
- },
- _itemsPerPage: {
- type: Number,
- value: 25,
- },
- _loading: {
- type: Boolean,
- value: true,
- },
- _filter: String,
- _refName: String,
- _hasNewItemName: Boolean,
- _isEditing: Boolean,
- _revisedRef: String,
- };
- }
-
- _determineIfOwner(repo) {
- return this.$.restAPI.getRepoAccess(repo)
- .then(access =>
- this._isOwner = access && !!access[repo].is_owner);
- }
-
- _paramsChanged(params) {
- if (!params || !params.repo) { return; }
-
- this._repo = params.repo;
-
- this._getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
- if (loggedIn) {
- this._determineIfOwner(this._repo);
- }
- });
-
- this.detailType = params.detail;
-
- this._filter = this.getFilterValue(params);
- this._offset = this.getOffsetValue(params);
-
- return this._getItems(this._filter, this._repo,
- this._itemsPerPage, this._offset, this.detailType);
- }
-
- _getItems(filter, repo, itemsPerPage, offset, detailType) {
- this._loading = true;
- this._items = [];
- flush();
- const errFn = response => {
- this.dispatchEvent(new CustomEvent('page-error', {
- detail: {response},
- composed: true, bubbles: true,
- }));
- };
- if (detailType === DETAIL_TYPES.BRANCHES) {
- return this.$.restAPI.getRepoBranches(
- filter, repo, itemsPerPage, offset, errFn).then(items => {
- if (!items) { return; }
- this._items = items;
- this._loading = false;
- });
- } else if (detailType === DETAIL_TYPES.TAGS) {
- return this.$.restAPI.getRepoTags(
- filter, repo, itemsPerPage, offset, errFn).then(items => {
- if (!items) { return; }
- this._items = items;
- this._loading = false;
- });
- }
- }
-
- _getPath(repo) {
- return `/admin/repos/${encodeURL(repo, false)},` +
- `${this.detailType}`;
- }
-
- _computeWeblink(repo) {
- if (!repo.web_links) { return ''; }
- const webLinks = repo.web_links;
- return webLinks.length ? webLinks : null;
- }
-
- _computeMessage(message) {
- if (!message) { return; }
- // Strip PGP info.
- return message.split(PGP_START)[0];
- }
-
- _stripRefs(item, detailType) {
- if (detailType === DETAIL_TYPES.BRANCHES) {
- return item.replace('refs/heads/', '');
- } else if (detailType === DETAIL_TYPES.TAGS) {
- return item.replace('refs/tags/', '');
- }
- }
-
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
-
- _computeEditingClass(isEditing) {
- return isEditing ? 'editing' : '';
- }
-
- _computeCanEditClass(ref, detailType, isOwner) {
- return isOwner && this._stripRefs(ref, detailType) === 'HEAD' ?
- 'canEdit' : '';
- }
-
- _handleEditRevision(e) {
- this._revisedRef = e.model.get('item.revision');
- this._isEditing = true;
- }
-
- _handleCancelRevision() {
- this._isEditing = false;
- }
-
- _handleSaveRevision(e) {
- this._setRepoHead(this._repo, this._revisedRef, e);
- }
-
- _setRepoHead(repo, ref, e) {
- return this.$.restAPI.setRepoHead(repo, ref).then(res => {
- if (res.status < 400) {
- this._isEditing = false;
- e.model.set('item.revision', ref);
- // This is needed to refresh _items property with fresh data,
- // specifically can_delete from the json response.
- this._getItems(
- this._filter, this._repo, this._itemsPerPage,
- this._offset, this.detailType);
- }
- });
- }
-
- _computeItemName(detailType) {
- if (detailType === DETAIL_TYPES.BRANCHES) {
- return 'Branch';
- } else if (detailType === DETAIL_TYPES.TAGS) {
- return 'Tag';
- }
- }
-
- _handleDeleteItemConfirm() {
- this.$.overlay.close();
- if (this.detailType === DETAIL_TYPES.BRANCHES) {
- return this.$.restAPI.deleteRepoBranches(this._repo, this._refName)
- .then(itemDeleted => {
- if (itemDeleted.status === 204) {
- this._getItems(
- this._filter, this._repo, this._itemsPerPage,
- this._offset, this.detailType);
- }
- });
- } else if (this.detailType === DETAIL_TYPES.TAGS) {
- return this.$.restAPI.deleteRepoTags(this._repo, this._refName)
- .then(itemDeleted => {
- if (itemDeleted.status === 204) {
- this._getItems(
- this._filter, this._repo, this._itemsPerPage,
- this._offset, this.detailType);
- }
- });
- }
- }
-
- _handleConfirmDialogCancel() {
- this.$.overlay.close();
- }
-
- _handleDeleteItem(e) {
- const name = this._stripRefs(e.model.get('item.ref'), this.detailType);
- if (!name) { return; }
- this._refName = name;
- this.$.overlay.open();
- }
-
- _computeHideDeleteClass(owner, canDelete) {
- if (canDelete || owner) {
- return 'show';
- }
-
- return '';
- }
-
- _handleCreateItem() {
- this.$.createNewModal.handleCreateItem();
- this._handleCloseCreate();
- }
-
- _handleCloseCreate() {
- this.$.createOverlay.close();
- }
-
- _handleCreateClicked() {
- this.$.createOverlay.open();
- }
-
- _hideIfBranch(type) {
- if (type === DETAIL_TYPES.BRANCHES) {
- return 'hideItem';
- }
-
- return '';
- }
-
- _computeHideTagger(tagger) {
- return tagger ? '' : 'hide';
- }
-}
-
-customElements.define(GrRepoDetailList.is, GrRepoDetailList);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts
new file mode 100644
index 0000000..828d447
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts
@@ -0,0 +1,381 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-input/iron-input';
+import '../../../styles/gr-form-styles';
+import '../../../styles/gr-table-styles';
+import '../../../styles/shared-styles';
+import '../../shared/gr-account-link/gr-account-link';
+import '../../shared/gr-button/gr-button';
+import '../../shared/gr-date-formatter/gr-date-formatter';
+import '../../shared/gr-dialog/gr-dialog';
+import '../../shared/gr-list-view/gr-list-view';
+import '../../shared/gr-overlay/gr-overlay';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface';
+import '../gr-create-pointer-dialog/gr-create-pointer-dialog';
+import '../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog';
+import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {htmlTemplate} from './gr-repo-detail-list_html';
+import {ListViewMixin} from '../../../mixins/gr-list-view-mixin/gr-list-view-mixin';
+import {encodeURL} from '../../../utils/url-util';
+import {customElement, property} from '@polymer/decorators';
+import {
+ ErrorCallback,
+ RestApiService,
+} from '../../../services/services/gr-rest-api/gr-rest-api';
+import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
+import {GrCreatePointerDialog} from '../gr-create-pointer-dialog/gr-create-pointer-dialog';
+import {
+ RepoName,
+ ProjectInfo,
+ BranchInfo,
+ GitRef,
+ TagInfo,
+ GitPersonInfo,
+} from '../../../types/common';
+import {AppElementRepoParams} from '../../gr-app-types';
+import {PolymerDomRepeatEvent} from '../../../types/types';
+import {RepoDetailView} from '../../core/gr-navigation/gr-navigation';
+
+const PGP_START = '-----BEGIN PGP SIGNATURE-----';
+
+export interface GrRepoDetailList {
+ $: {
+ restAPI: RestApiService & Element;
+ overlay: GrOverlay;
+ createOverlay: GrOverlay;
+ createNewModal: GrCreatePointerDialog;
+ };
+}
+@customElement('gr-repo-detail-list')
+export class GrRepoDetailList extends ListViewMixin(
+ GestureEventListeners(LegacyElementMixin(PolymerElement))
+) {
+ static get template() {
+ return htmlTemplate;
+ }
+
+ @property({type: Object, observer: '_paramsChanged'})
+ params?: AppElementRepoParams;
+
+ @property({type: String})
+ detailType?: RepoDetailView;
+
+ @property({type: Boolean})
+ _editing = false;
+
+ @property({type: Boolean})
+ _isOwner = false;
+
+ @property({type: Boolean})
+ _loggedIn = false;
+
+ @property({type: Number})
+ _offset?: number;
+
+ @property({type: String})
+ _repo?: RepoName;
+
+ @property({type: Array})
+ _items?: BranchInfo[] | TagInfo[];
+
+ @property({type: Array, computed: 'computeShownItems(_items)'})
+ _shownItems?: BranchInfo[] | TagInfo[];
+
+ @property({type: Number})
+ _itemsPerPage = 25;
+
+ @property({type: Boolean})
+ _loading = true;
+
+ @property({type: String})
+ _filter?: string;
+
+ @property({type: String})
+ _refName?: GitRef;
+
+ @property({type: Boolean})
+ _hasNewItemName?: boolean;
+
+ @property({type: Boolean})
+ _isEditing?: boolean;
+
+ @property({type: String})
+ _revisedRef?: GitRef;
+
+ _determineIfOwner(repo: RepoName) {
+ return this.$.restAPI
+ .getRepoAccess(repo)
+ .then(access => (this._isOwner = !!access && !!access[repo].is_owner));
+ }
+
+ _paramsChanged(params?: AppElementRepoParams) {
+ if (!params?.repo) {
+ return Promise.reject(new Error('undefined repo'));
+ }
+
+ this._repo = params.repo;
+
+ this._getLoggedIn().then(loggedIn => {
+ this._loggedIn = loggedIn;
+ if (loggedIn && this._repo) {
+ this._determineIfOwner(this._repo);
+ }
+ });
+
+ this.detailType = params.detail;
+
+ this._filter = this.getFilterValue(params);
+ this._offset = this.getOffsetValue(params);
+ if (!this.detailType)
+ return Promise.reject(new Error('undefined detailType'));
+
+ return this._getItems(
+ this._filter,
+ this._repo,
+ this._itemsPerPage,
+ this._offset,
+ this.detailType
+ );
+ }
+
+ // TODO(TS) Move this to object for easier read, understand.
+ _getItems(
+ filter: string | undefined,
+ repo: RepoName | undefined,
+ itemsPerPage: number,
+ offset: number | undefined,
+ detailType: string
+ ) {
+ if (filter === undefined || !repo || offset === undefined) {
+ return Promise.reject(new Error('filter or repo or offset undefined'));
+ }
+ this._loading = true;
+ this._items = [];
+ flush();
+ const errFn: ErrorCallback = response => {
+ this.dispatchEvent(
+ new CustomEvent('page-error', {
+ detail: {response},
+ composed: true,
+ bubbles: true,
+ })
+ );
+ };
+ if (detailType === RepoDetailView.BRANCHES) {
+ return this.$.restAPI
+ .getRepoBranches(filter, repo, itemsPerPage, offset, errFn)
+ .then(items => {
+ if (!items) {
+ return;
+ }
+ this._items = items;
+ this._loading = false;
+ });
+ } else if (detailType === RepoDetailView.TAGS) {
+ return this.$.restAPI
+ .getRepoTags(filter, repo, itemsPerPage, offset, errFn)
+ .then(items => {
+ if (!items) {
+ return;
+ }
+ this._items = items;
+ this._loading = false;
+ });
+ }
+ return Promise.reject(new Error('unknown detail type'));
+ }
+
+ _getPath(repo: RepoName) {
+ return `/admin/repos/${encodeURL(repo, false)},${this.detailType}`;
+ }
+
+ _computeWeblink(repo: ProjectInfo) {
+ if (!repo.web_links) {
+ return '';
+ }
+ const webLinks = repo.web_links;
+ return webLinks.length ? webLinks : null;
+ }
+
+ _computeMessage(message?: string) {
+ if (!message) {
+ return;
+ }
+ // Strip PGP info.
+ return message.split(PGP_START)[0];
+ }
+
+ _stripRefs(item: GitRef, detailType?: string) {
+ if (detailType === RepoDetailView.BRANCHES) {
+ return item.replace('refs/heads/', '');
+ } else if (detailType === RepoDetailView.TAGS) {
+ return item.replace('refs/tags/', '');
+ }
+ throw new Error('unknown detailType');
+ }
+
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
+
+ _computeEditingClass(isEditing: boolean) {
+ return isEditing ? 'editing' : '';
+ }
+
+ _computeCanEditClass(ref: GitRef, detailType: string, isOwner: boolean) {
+ return isOwner && this._stripRefs(ref, detailType) === 'HEAD'
+ ? 'canEdit'
+ : '';
+ }
+
+ _handleEditRevision(e: PolymerDomRepeatEvent<BranchInfo | TagInfo>) {
+ this._revisedRef = (e.model.get('item.revision') as unknown) as GitRef;
+ this._isEditing = true;
+ }
+
+ _handleCancelRevision() {
+ this._isEditing = false;
+ }
+
+ _handleSaveRevision(e: PolymerDomRepeatEvent<GitRef>) {
+ if (this._revisedRef && this._repo)
+ this._setRepoHead(this._repo, this._revisedRef, e);
+ }
+
+ _setRepoHead(repo: RepoName, ref: GitRef, e: PolymerDomRepeatEvent<GitRef>) {
+ return this.$.restAPI.setRepoHead(repo, ref).then(res => {
+ if (res.status < 400) {
+ this._isEditing = false;
+ e.model.set('item.revision', ref);
+ // This is needed to refresh _items property with fresh data,
+ // specifically can_delete from the json response.
+ this._getItems(
+ this._filter,
+ this._repo,
+ this._itemsPerPage,
+ this._offset,
+ this.detailType!
+ );
+ }
+ });
+ }
+
+ _computeItemName(detailType: string) {
+ if (detailType === RepoDetailView.BRANCHES) {
+ return 'Branch';
+ } else if (detailType === RepoDetailView.TAGS) {
+ return 'Tag';
+ }
+ throw new Error('unknown detailType');
+ }
+
+ _handleDeleteItemConfirm() {
+ this.$.overlay.close();
+ if (!this._repo || !this._refName) {
+ return Promise.reject(new Error('undefined repo or refName'));
+ }
+ if (this.detailType === RepoDetailView.BRANCHES) {
+ return this.$.restAPI
+ .deleteRepoBranches(this._repo, this._refName)
+ .then(itemDeleted => {
+ if (itemDeleted.status === 204) {
+ this._getItems(
+ this._filter,
+ this._repo,
+ this._itemsPerPage,
+ this._offset,
+ this.detailType!
+ );
+ }
+ });
+ } else if (this.detailType === RepoDetailView.TAGS) {
+ return this.$.restAPI
+ .deleteRepoTags(this._repo, this._refName)
+ .then(itemDeleted => {
+ if (itemDeleted.status === 204) {
+ this._getItems(
+ this._filter,
+ this._repo,
+ this._itemsPerPage,
+ this._offset,
+ this.detailType!
+ );
+ }
+ });
+ }
+ return Promise.reject(new Error('unknown detail type'));
+ }
+
+ _handleConfirmDialogCancel() {
+ this.$.overlay.close();
+ }
+
+ _handleDeleteItem(e: PolymerDomRepeatEvent<GitRef>) {
+ const name = this._stripRefs(
+ e.model.get('item.ref'),
+ this.detailType
+ ) as GitRef;
+ if (!name) {
+ return;
+ }
+ this._refName = name;
+ this.$.overlay.open();
+ }
+
+ _computeHideDeleteClass(owner: boolean, canDelete: boolean) {
+ if (canDelete || owner) {
+ return 'show';
+ }
+
+ return '';
+ }
+
+ _handleCreateItem() {
+ this.$.createNewModal.handleCreateItem();
+ this._handleCloseCreate();
+ }
+
+ _handleCloseCreate() {
+ this.$.createOverlay.close();
+ }
+
+ _handleCreateClicked() {
+ this.$.createOverlay.open();
+ }
+
+ _hideIfBranch(type: string) {
+ if (type === RepoDetailView.BRANCHES) {
+ return 'hideItem';
+ }
+
+ return '';
+ }
+
+ _computeHideTagger(tagger: GitPersonInfo) {
+ return tagger ? '' : 'hide';
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-repo-detail-list': GrRepoDetailList;
+ }
+}
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.js b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.js
index 7f6ba08..f86f613 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.js
@@ -17,6 +17,7 @@
import '../../../test/common-test-setup-karma.js';
import './gr-repo-detail-list.js';
+import 'lodash/lodash.js';
import {page} from '../../../utils/page-wrapper-utils.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
deleted file mode 100644
index 2230c38..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ /dev/null
@@ -1,441 +0,0 @@
-/**
- * @license
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../styles/gr-change-list-styles.js';
-import '../../shared/gr-cursor-manager/gr-cursor-manager.js';
-import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import '../gr-change-list-item/gr-change-list-item.js';
-import '../../../styles/shared-styles.js';
-import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
-import {afterNextRender} from '@polymer/polymer/lib/utils/render-status.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-change-list_html.js';
-import {appContext} from '../../../services/app-context.js';
-import {ChangeTableMixin} from '../../../mixins/gr-change-table-mixin/gr-change-table-mixin.js';
-import {KeyboardShortcutMixin, Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
-import {changeIsOpen} from '../../../utils/change-util.js';
-
-const NUMBER_FIXED_COLUMNS = 3;
-const CLOSED_STATUS = ['MERGED', 'ABANDONED'];
-const LABEL_PREFIX_INVALID_PROLOG = 'Invalid-Prolog-Rules-Label-Name--';
-const MAX_SHORTCUT_CHARS = 5;
-
-/**
- * @extends PolymerElement
- */
-class GrChangeList extends ChangeTableMixin(
- KeyboardShortcutMixin(GestureEventListeners(
- LegacyElementMixin(PolymerElement)))) {
- static get template() { return htmlTemplate; }
-
- static get is() { return 'gr-change-list'; }
- /**
- * Fired when next page key shortcut was pressed.
- *
- * @event next-page
- */
-
- /**
- * Fired when previous page key shortcut was pressed.
- *
- * @event previous-page
- */
-
- static get properties() {
- return {
- /**
- * The logged-in user's account, or an empty object if no user is logged
- * in.
- */
- account: {
- type: Object,
- value: null,
- },
- /**
- * An array of ChangeInfo objects to render.
- * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info
- */
- changes: {
- type: Array,
- observer: '_changesChanged',
- },
- /**
- * ChangeInfo objects grouped into arrays. The sections and changes
- * properties should not be used together.
- *
- * @type {!Array<{
- * name: string,
- * query: string,
- * results: !Array<!Object>
- * }>}
- */
- sections: {
- type: Array,
- value() { return []; },
- },
- labelNames: {
- type: Array,
- computed: '_computeLabelNames(sections)',
- },
- _dynamicHeaderEndpoints: {
- type: Array,
- },
- selectedIndex: {
- type: Number,
- notify: true,
- },
- showNumber: Boolean, // No default value to prevent flickering.
- showStar: {
- type: Boolean,
- value: false,
- },
- showReviewedState: {
- type: Boolean,
- value: false,
- },
- keyEventTarget: {
- type: Object,
- value() { return document.body; },
- },
- changeTableColumns: Array,
- visibleChangeTableColumns: Array,
- preferences: Object,
- _config: Object,
- };
- }
-
- static get observers() {
- return [
- '_sectionsChanged(sections.*)',
- '_computePreferences(account, preferences, _config)',
- ];
- }
-
- keyboardShortcuts() {
- return {
- [Shortcut.CURSOR_NEXT_CHANGE]: '_nextChange',
- [Shortcut.CURSOR_PREV_CHANGE]: '_prevChange',
- [Shortcut.NEXT_PAGE]: '_nextPage',
- [Shortcut.PREV_PAGE]: '_prevPage',
- [Shortcut.OPEN_CHANGE]: '_openChange',
- [Shortcut.TOGGLE_CHANGE_REVIEWED]: '_toggleChangeReviewed',
- [Shortcut.TOGGLE_CHANGE_STAR]: '_toggleChangeStar',
- [Shortcut.REFRESH_CHANGE_LIST]: '_refreshChangeList',
- };
- }
-
- constructor() {
- super();
- this.flagsService = appContext.flagsService;
- }
-
- /** @override */
- created() {
- super.created();
- this.addEventListener('keydown',
- e => this._scopedKeydownHandler(e));
- }
-
- /** @override */
- ready() {
- super.ready();
- this.$.restAPI.getConfig().then(config => {
- this._config = config;
- });
- }
-
- /** @override */
- attached() {
- super.attached();
- getPluginLoader().awaitPluginsLoaded()
- .then(() => {
- this._dynamicHeaderEndpoints = getPluginEndpoints().
- getDynamicEndpoints('change-list-header');
- });
- }
-
- /**
- * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard
- * events must be scoped to a component level (e.g. `enter`) in order to not
- * override native browser functionality.
- *
- * Context: Issue 7294
- */
- _scopedKeydownHandler(e) {
- if (e.keyCode === 13) {
- // Enter.
- this._openChange(e);
- }
- }
-
- _lowerCase(column) {
- return column.toLowerCase();
- }
-
- _computePreferences(account, preferences, config) {
- // Polymer 2: check for undefined
- if ([account, preferences, config].includes(undefined)) {
- return;
- }
-
- this.changeTableColumns = this.columnNames;
- this.showNumber = false;
- this.visibleChangeTableColumns = this.getEnabledColumns(this.columnNames,
- config, this.flagsService.enabledExperiments);
-
- if (account) {
- this.showNumber = !!(preferences &&
- preferences.legacycid_in_change_table);
- if (preferences.change_table &&
- preferences.change_table.length > 0) {
- const prefColumns = this.getVisibleColumns(preferences.change_table);
- this.visibleChangeTableColumns = this.getEnabledColumns(prefColumns,
- config, this.flagsService.enabledExperiments);
- }
- }
- }
-
- _computeColspan(changeTableColumns, labelNames) {
- if (!changeTableColumns || !labelNames) return;
- return changeTableColumns.length + labelNames.length +
- NUMBER_FIXED_COLUMNS;
- }
-
- _computeLabelNames(sections) {
- if (!sections) { return []; }
- let labels = [];
- const nonExistingLabel = function(item) {
- return !labels.includes(item);
- };
- for (const section of sections) {
- if (!section.results) { continue; }
- for (const change of section.results) {
- if (!change.labels) { continue; }
- const currentLabels = Object.keys(change.labels);
- labels = labels.concat(currentLabels.filter(nonExistingLabel));
- }
- }
- return labels.sort();
- }
-
- _computeLabelShortcut(labelName) {
- if (labelName.startsWith(LABEL_PREFIX_INVALID_PROLOG)) {
- labelName = labelName.slice(LABEL_PREFIX_INVALID_PROLOG.length);
- }
- return labelName.split('-')
- .reduce((a, i) => {
- if (!i) { return a; }
- return a + i[0].toUpperCase();
- }, '')
- .slice(0, MAX_SHORTCUT_CHARS);
- }
-
- _changesChanged(changes) {
- this.sections = changes ? [{results: changes}] : [];
- }
-
- _processQuery(query) {
- let tokens = query.split(' ');
- const invalidTokens = ['limit:', 'age:', '-age:'];
- tokens = tokens.filter(token => !invalidTokens
- .some(invalidToken => token.startsWith(invalidToken)));
- return tokens.join(' ');
- }
-
- _sectionHref(query) {
- return GerritNav.getUrlForSearchQuery(this._processQuery(query));
- }
-
- /**
- * Maps an index local to a particular section to the absolute index
- * across all the changes on the page.
- *
- * @param {number} sectionIndex index of section
- * @param {number} localIndex index of row within section
- * @return {number} absolute index of row in the aggregate dashboard
- */
- _computeItemAbsoluteIndex(sectionIndex, localIndex) {
- let idx = 0;
- for (let i = 0; i < sectionIndex; i++) {
- idx += this.sections[i].results.length;
- }
- return idx + localIndex;
- }
-
- _computeItemSelected(sectionIndex, index, selectedIndex) {
- const idx = this._computeItemAbsoluteIndex(sectionIndex, index);
- return idx == selectedIndex;
- }
-
- _computeTabIndex(sectionIndex, index, selectedIndex) {
- return this._computeItemSelected(sectionIndex, index, selectedIndex)
- ? 0 : undefined;
- }
-
- _computeItemNeedsReview(account, change, showReviewedState, config) {
- const isAttentionSetEnabled =
- !!config && !!config.change && config.change.enable_attention_set;
- return !isAttentionSetEnabled && showReviewedState && !change.reviewed &&
- !change.work_in_progress &&
- changeIsOpen(change) &&
- (!account || account._account_id != change.owner._account_id);
- }
-
- _computeItemHighlight(account, change) {
- // Do not show the assignee highlight if the change is not open.
- if (!change ||!change.assignee ||
- !account ||
- CLOSED_STATUS.indexOf(change.status) !== -1) {
- return false;
- }
- return account._account_id === change.assignee._account_id;
- }
-
- _nextChange(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this.$.cursor.next();
- }
-
- _prevChange(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this.$.cursor.previous();
- }
-
- _openChange(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- GerritNav.navigateToChange(this._changeForIndex(this.selectedIndex));
- }
-
- _nextPage(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) {
- return;
- }
-
- e.preventDefault();
- this.dispatchEvent(new CustomEvent('next-page', {
- composed: true, bubbles: true,
- }));
- }
-
- _prevPage(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) {
- return;
- }
-
- e.preventDefault();
- this.dispatchEvent(new CustomEvent('previous-page', {
- composed: true, bubbles: true,
- }));
- }
-
- _toggleChangeReviewed(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this._toggleReviewedForIndex(this.selectedIndex);
- }
-
- _toggleReviewedForIndex(index) {
- const changeEls = this._getListItems();
- if (index >= changeEls.length || !changeEls[index]) {
- return;
- }
-
- const changeEl = changeEls[index];
- changeEl.toggleReviewed();
- }
-
- _refreshChangeList(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- e.preventDefault();
- this._reloadWindow();
- }
-
- _reloadWindow() {
- window.location.reload();
- }
-
- _toggleChangeStar(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this._toggleStarForIndex(this.selectedIndex);
- }
-
- _toggleStarForIndex(index) {
- const changeEls = this._getListItems();
- if (index >= changeEls.length || !changeEls[index]) {
- return;
- }
-
- const changeEl = changeEls[index];
- changeEl.shadowRoot
- .querySelector('gr-change-star').toggleStar();
- }
-
- _changeForIndex(index) {
- const changeEls = this._getListItems();
- if (index < changeEls.length && changeEls[index]) {
- return changeEls[index].change;
- }
- return null;
- }
-
- _getListItems() {
- return Array.from(
- this.root.querySelectorAll('gr-change-list-item'));
- }
-
- _sectionsChanged() {
- // Flush DOM operations so that the list item elements will be loaded.
- afterNextRender(this, () => {
- this.$.cursor.stops = this._getListItems();
- this.$.cursor.moveToStart();
- });
- }
-
- _getSpecialEmptySlot(section) {
- if (section.isOutgoing) return 'empty-outgoing';
- if (section.name === 'Your Turn') return 'empty-your-turn';
- return '';
- }
-
- _isEmpty(section) {
- return !section.results.length;
- }
-}
-
-customElements.define(GrChangeList.is, GrChangeList);
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
new file mode 100644
index 0000000..a05ccf4
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
@@ -0,0 +1,508 @@
+/**
+ * @license
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../styles/gr-change-list-styles';
+import '../../shared/gr-cursor-manager/gr-cursor-manager';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface';
+import '../gr-change-list-item/gr-change-list-item';
+import '../../../styles/shared-styles';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
+import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
+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-change-list_html';
+import {appContext} from '../../../services/app-context';
+import {ChangeTableMixin} from '../../../mixins/gr-change-table-mixin/gr-change-table-mixin';
+import {
+ KeyboardShortcutMixin,
+ Shortcut,
+ CustomKeyboardEvent,
+ Modifier,
+} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
+import {
+ GerritNav,
+ DashboardSection,
+} from '../../core/gr-navigation/gr-navigation';
+import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
+import {changeIsOpen} from '../../../utils/change-util';
+import {customElement, property, observe} from '@polymer/decorators';
+import {RestApiService} from '../../../services/services/gr-rest-api/gr-rest-api';
+import {GrCursorManager} from '../../shared/gr-cursor-manager/gr-cursor-manager';
+import {
+ AccountInfo,
+ ChangeInfo,
+ ServerInfo,
+ PreferencesInput,
+} from '../../../types/common';
+
+const NUMBER_FIXED_COLUMNS = 3;
+const CLOSED_STATUS = ['MERGED', 'ABANDONED'];
+const LABEL_PREFIX_INVALID_PROLOG = 'Invalid-Prolog-Rules-Label-Name--';
+const MAX_SHORTCUT_CHARS = 5;
+
+export interface ChangeListSection {
+ results: ChangeInfo[];
+}
+export interface GrChangeList {
+ $: {
+ restAPI: RestApiService & Element;
+ cursor: GrCursorManager;
+ };
+}
+@customElement('gr-change-list')
+export class GrChangeList extends ChangeTableMixin(
+ KeyboardShortcutMixin(
+ GestureEventListeners(LegacyElementMixin(PolymerElement))
+ )
+) {
+ static get template() {
+ return htmlTemplate;
+ }
+
+ /**
+ * Fired when next page key shortcut was pressed.
+ *
+ * @event next-page
+ */
+
+ /**
+ * Fired when previous page key shortcut was pressed.
+ *
+ * @event previous-page
+ */
+
+ /**
+ * The logged-in user's account, or an empty object if no user is logged
+ * in.
+ */
+ @property({type: Object})
+ account: AccountInfo | undefined = undefined;
+
+ @property({type: Array, observer: '_changesChanged'})
+ changes?: ChangeInfo[];
+
+ /**
+ * ChangeInfo objects grouped into arrays. The sections and changes
+ * properties should not be used together.
+ */
+ @property({type: Array})
+ sections: ChangeListSection[] = [];
+
+ @property({type: Array, computed: '_computeLabelNames(sections)'})
+ labelNames?: string[];
+
+ @property({type: Array})
+ _dynamicHeaderEndpoints?: string[];
+
+ @property({type: Number, notify: true})
+ selectedIndex?: number;
+
+ @property({type: Boolean})
+ showNumber?: boolean; // No default value to prevent flickering.
+
+ @property({type: Boolean})
+ showStar = false;
+
+ @property({type: Boolean})
+ showReviewedState = false;
+
+ @property({type: Object})
+ keyEventTarget: HTMLElement = document.body;
+
+ @property({type: Array})
+ changeTableColumns?: string[];
+
+ @property({type: Array})
+ visibleChangeTableColumns?: string[];
+
+ @property({type: Object})
+ preferences?: PreferencesInput;
+
+ @property({type: Object})
+ _config?: ServerInfo;
+
+ flagsService = appContext.flagsService;
+
+ keyboardShortcuts() {
+ return {
+ [Shortcut.CURSOR_NEXT_CHANGE]: '_nextChange',
+ [Shortcut.CURSOR_PREV_CHANGE]: '_prevChange',
+ [Shortcut.NEXT_PAGE]: '_nextPage',
+ [Shortcut.PREV_PAGE]: '_prevPage',
+ [Shortcut.OPEN_CHANGE]: '_openChange',
+ [Shortcut.TOGGLE_CHANGE_REVIEWED]: '_toggleChangeReviewed',
+ [Shortcut.TOGGLE_CHANGE_STAR]: '_toggleChangeStar',
+ [Shortcut.REFRESH_CHANGE_LIST]: '_refreshChangeList',
+ };
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('keydown', e => this._scopedKeydownHandler(e));
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ this.$.restAPI.getConfig().then(config => {
+ this._config = config;
+ });
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ getPluginLoader()
+ .awaitPluginsLoaded()
+ .then(() => {
+ this._dynamicHeaderEndpoints = getPluginEndpoints().getDynamicEndpoints(
+ 'change-list-header'
+ );
+ });
+ }
+
+ /**
+ * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard
+ * events must be scoped to a component level (e.g. `enter`) in order to not
+ * override native browser functionality.
+ *
+ * Context: Issue 7294
+ */
+ _scopedKeydownHandler(e: KeyboardEvent) {
+ if (e.keyCode === 13) {
+ // Enter.
+ this._openChange((e as unknown) as CustomKeyboardEvent);
+ }
+ }
+
+ _lowerCase(column: string) {
+ return column.toLowerCase();
+ }
+
+ @observe('account', 'preferences', '_config')
+ _computePreferences(
+ account?: AccountInfo,
+ preferences?: PreferencesInput,
+ config?: ServerInfo
+ ) {
+ if (!config) {
+ return;
+ }
+
+ this.changeTableColumns = this.columnNames;
+ this.showNumber = false;
+ this.visibleChangeTableColumns = this.getEnabledColumns(
+ this.columnNames,
+ config,
+ this.flagsService.enabledExperiments
+ );
+
+ if (account && preferences) {
+ this.showNumber = !!(
+ preferences && preferences.legacycid_in_change_table
+ );
+ if (preferences.change_table && preferences.change_table.length > 0) {
+ const prefColumns = this.getVisibleColumns(preferences.change_table);
+ this.visibleChangeTableColumns = this.getEnabledColumns(
+ prefColumns,
+ config,
+ this.flagsService.enabledExperiments
+ );
+ }
+ }
+ }
+
+ _computeColspan(changeTableColumns: string[], labelNames: string[]) {
+ if (!changeTableColumns || !labelNames) return;
+ return changeTableColumns.length + labelNames.length + NUMBER_FIXED_COLUMNS;
+ }
+
+ _computeLabelNames(sections: ChangeListSection[]) {
+ if (!sections) {
+ return [];
+ }
+ let labels: string[] = [];
+ const nonExistingLabel = function (item: string) {
+ return !labels.includes(item);
+ };
+ for (const section of sections) {
+ if (!section.results) {
+ continue;
+ }
+ for (const change of section.results) {
+ if (!change.labels) {
+ continue;
+ }
+ const currentLabels = Object.keys(change.labels);
+ labels = labels.concat(currentLabels.filter(nonExistingLabel));
+ }
+ }
+ return labels.sort();
+ }
+
+ _computeLabelShortcut(labelName: string) {
+ if (labelName.startsWith(LABEL_PREFIX_INVALID_PROLOG)) {
+ labelName = labelName.slice(LABEL_PREFIX_INVALID_PROLOG.length);
+ }
+ return labelName
+ .split('-')
+ .reduce((a, i) => {
+ if (!i) {
+ return a;
+ }
+ return a + i[0].toUpperCase();
+ }, '')
+ .slice(0, MAX_SHORTCUT_CHARS);
+ }
+
+ _changesChanged(changes: ChangeInfo[]) {
+ this.sections = changes ? [{results: changes}] : [];
+ }
+
+ _processQuery(query: string) {
+ let tokens = query.split(' ');
+ const invalidTokens = ['limit:', 'age:', '-age:'];
+ tokens = tokens.filter(
+ token =>
+ !invalidTokens.some(invalidToken => token.startsWith(invalidToken))
+ );
+ return tokens.join(' ');
+ }
+
+ _sectionHref(query: string) {
+ return GerritNav.getUrlForSearchQuery(this._processQuery(query));
+ }
+
+ /**
+ * Maps an index local to a particular section to the absolute index
+ * across all the changes on the page.
+ *
+ * @param sectionIndex index of section
+ * @param localIndex index of row within section
+ * @return absolute index of row in the aggregate dashboard
+ */
+ _computeItemAbsoluteIndex(sectionIndex: number, localIndex: number) {
+ let idx = 0;
+ for (let i = 0; i < sectionIndex; i++) {
+ idx += this.sections[i].results.length;
+ }
+ return idx + localIndex;
+ }
+
+ _computeItemSelected(
+ sectionIndex: number,
+ index: number,
+ selectedIndex: number
+ ) {
+ const idx = this._computeItemAbsoluteIndex(sectionIndex, index);
+ return idx === selectedIndex;
+ }
+
+ _computeTabIndex(sectionIndex: number, index: number, selectedIndex: number) {
+ return this._computeItemSelected(sectionIndex, index, selectedIndex)
+ ? 0
+ : undefined;
+ }
+
+ _computeItemNeedsReview(
+ account: AccountInfo | undefined,
+ change: ChangeInfo,
+ showReviewedState: boolean,
+ config?: ServerInfo
+ ) {
+ const isAttentionSetEnabled =
+ !!config && !!config.change && config.change.enable_attention_set;
+ return (
+ !isAttentionSetEnabled &&
+ showReviewedState &&
+ !change.reviewed &&
+ !change.work_in_progress &&
+ changeIsOpen(change) &&
+ (!account || account._account_id !== change.owner._account_id)
+ );
+ }
+
+ _computeItemHighlight(account?: AccountInfo, change?: ChangeInfo) {
+ // Do not show the assignee highlight if the change is not open.
+ if (
+ !change ||
+ !change.assignee ||
+ !account ||
+ CLOSED_STATUS.indexOf(change.status) !== -1
+ ) {
+ return false;
+ }
+ return account._account_id === change.assignee._account_id;
+ }
+
+ _nextChange(e: CustomKeyboardEvent) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ return;
+ }
+
+ e.preventDefault();
+ this.$.cursor.next();
+ }
+
+ _prevChange(e: CustomKeyboardEvent) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ return;
+ }
+
+ e.preventDefault();
+ this.$.cursor.previous();
+ }
+
+ _openChange(e: CustomKeyboardEvent) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ return;
+ }
+
+ e.preventDefault();
+ const change = this._changeForIndex(this.selectedIndex);
+ if (change) GerritNav.navigateToChange(change);
+ }
+
+ _nextPage(e: CustomKeyboardEvent) {
+ if (
+ this.shouldSuppressKeyboardShortcut(e) ||
+ (this.modifierPressed(e) &&
+ !this.isModifierPressed(e, Modifier.SHIFT_KEY))
+ ) {
+ return;
+ }
+
+ e.preventDefault();
+ this.dispatchEvent(
+ new CustomEvent('next-page', {
+ composed: true,
+ bubbles: true,
+ })
+ );
+ }
+
+ _prevPage(e: CustomKeyboardEvent) {
+ if (
+ this.shouldSuppressKeyboardShortcut(e) ||
+ (this.modifierPressed(e) &&
+ !this.isModifierPressed(e, Modifier.SHIFT_KEY))
+ ) {
+ return;
+ }
+
+ e.preventDefault();
+ this.dispatchEvent(
+ new CustomEvent('previous-page', {
+ composed: true,
+ bubbles: true,
+ })
+ );
+ }
+
+ _toggleChangeReviewed(e: CustomKeyboardEvent) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ return;
+ }
+
+ e.preventDefault();
+ this._toggleReviewedForIndex(this.selectedIndex);
+ }
+
+ _toggleReviewedForIndex(index?: number) {
+ const changeEls = this._getListItems();
+ if (index === undefined || index >= changeEls.length || !changeEls[index]) {
+ return;
+ }
+
+ const changeEl = changeEls[index];
+ changeEl.toggleReviewed();
+ }
+
+ _refreshChangeList(e: CustomKeyboardEvent) {
+ if (this.shouldSuppressKeyboardShortcut(e)) {
+ return;
+ }
+
+ e.preventDefault();
+ this._reloadWindow();
+ }
+
+ _reloadWindow() {
+ window.location.reload();
+ }
+
+ _toggleChangeStar(e: CustomKeyboardEvent) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ return;
+ }
+
+ e.preventDefault();
+ this._toggleStarForIndex(this.selectedIndex);
+ }
+
+ _toggleStarForIndex(index?: number) {
+ const changeEls = this._getListItems();
+ if (index === undefined || index >= changeEls.length || !changeEls[index]) {
+ return;
+ }
+
+ const changeEl = changeEls[index];
+ const grChangeStar = changeEl?.shadowRoot?.querySelector('gr-change-star');
+ if (grChangeStar) grChangeStar.toggleStar();
+ }
+
+ _changeForIndex(index?: number) {
+ const changeEls = this._getListItems();
+ if (index !== undefined && index < changeEls.length && changeEls[index]) {
+ return changeEls[index].change;
+ }
+ return null;
+ }
+
+ _getListItems() {
+ const items = this.root?.querySelectorAll('gr-change-list-item');
+ return !items ? [] : Array.from(items);
+ }
+
+ @observe('sections.*')
+ _sectionsChanged() {
+ // Flush DOM operations so that the list item elements will be loaded.
+ afterNextRender(this, () => {
+ this.$.cursor.stops = this._getListItems();
+ this.$.cursor.moveToStart();
+ });
+ }
+
+ _getSpecialEmptySlot(section: DashboardSection) {
+ if (section.isOutgoing) return 'empty-outgoing';
+ if (section.name === 'Your Turn') return 'empty-your-turn';
+ return '';
+ }
+
+ _isEmpty(section: DashboardSection) {
+ return !section.results?.length;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-change-list': GrChangeList;
+ }
+}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
index 0493966..56a16b5 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
@@ -50,8 +50,8 @@
suite('test show change number not logged in', () => {
setup(() => {
element = basicFixture.instantiate();
- element.account = null;
- element.preferences = null;
+ element.account = undefined;
+ element.preferences = undefined;
element._config = {};
});
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
index 179caaa..0fa8e31 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
@@ -789,6 +789,9 @@
suite('_recomputeComments', () => {
setup(() => {
+ element._changeNum = '1';
+ element._change = {_number: '1'};
+ flushAsynchronousOperations();
// Fake computeDraftCount as its required for ChangeComments,
// see gr-comment-api#reloadDrafts.
sinon.stub(element.$.commentAPI, 'reloadDrafts')
@@ -927,7 +930,9 @@
rev4: {_number: 4, commit: {parents: []}},
},
current_revision: 'rev4',
+ _number: '1',
};
+ element._changeNum = '1';
element._commentThreads = THREADS;
const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[3]);
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
index 358ebb0..c70678f 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
@@ -1508,7 +1508,9 @@
.querySelector('gr-button.send'));
assert.isFalse(sendStub.called);
- element.draftCommentThreads = [{comments: [{__draft: true}]}];
+ element.draftCommentThreads = [{comments: [
+ {__draft: true, path: 'test', line: 1, patch_set: 1},
+ ]}];
flushAsynchronousOperations();
MockInteractions.tap(element.shadowRoot
@@ -1521,7 +1523,9 @@
// computed to false.
element.draftCommentThreads = [];
assert.equal(element.getFocusStops().end, element.$.cancelButton);
- element.draftCommentThreads = [{comments: [{__draft: true}]}];
+ element.draftCommentThreads = [{comments: [
+ {__draft: true, path: 'test', line: 1, patch_set: 1},
+ ]}];
assert.equal(element.getFocusStops().end, element.$.sendButton);
});
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
index 5016c40..19e733e 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
@@ -133,6 +133,7 @@
hideIfEmpty?: boolean;
assigneeOnly?: boolean;
isOutgoing?: boolean;
+ results?: ChangeInfo[];
}
export interface UserDashboardConfig {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
deleted file mode 100644
index 2828cf1..0000000
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
+++ /dev/null
@@ -1,904 +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-autogrow-textarea/iron-autogrow-textarea.js';
-import '../../../styles/shared-styles.js';
-import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
-import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
-import '../gr-button/gr-button.js';
-import '../gr-dialog/gr-dialog.js';
-import '../gr-date-formatter/gr-date-formatter.js';
-import '../gr-formatted-text/gr-formatted-text.js';
-import '../gr-icons/gr-icons.js';
-import '../gr-overlay/gr-overlay.js';
-import '../gr-rest-api-interface/gr-rest-api-interface.js';
-import '../gr-storage/gr-storage.js';
-import '../gr-textarea/gr-textarea.js';
-import '../gr-tooltip-content/gr-tooltip-content.js';
-import '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js';
-import '../gr-account-label/gr-account-label.js';
-import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {htmlTemplate} from './gr-comment_html.js';
-import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
-import {getRootElement} from '../../../scripts/rootElement.js';
-import {appContext} from '../../../services/app-context.js';
-
-const STORAGE_DEBOUNCE_INTERVAL = 400;
-const TOAST_DEBOUNCE_INTERVAL = 200;
-
-const SAVING_MESSAGE = 'Saving';
-const DRAFT_SINGULAR = 'draft...';
-const DRAFT_PLURAL = 'drafts...';
-const SAVED_MESSAGE = 'All changes saved';
-const UNSAVED_MESSAGE = 'Unable to save draft';
-
-const REPORT_CREATE_DRAFT = 'CreateDraftComment';
-const REPORT_UPDATE_DRAFT = 'UpdateDraftComment';
-const REPORT_DISCARD_DRAFT = 'DiscardDraftComment';
-
-const FILE = 'FILE';
-
-export const __testOnly_UNSAVED_MESSAGE = UNSAVED_MESSAGE;
-
-/**
- * All candidates tips to show, will pick randomly.
- */
-const RESPECTFUL_REVIEW_TIPS= [
- 'Assume competence.',
- 'Provide rationale or context.',
- 'Consider how comments may be interpreted.',
- 'Avoid harsh language.',
- 'Make your comments specific and actionable.',
- 'When disagreeing, explain the advantage of your approach.',
-];
-
-/**
- * @extends PolymerElement
- */
-class GrComment extends KeyboardShortcutMixin(GestureEventListeners(
- LegacyElementMixin(PolymerElement))) {
- static get template() { return htmlTemplate; }
-
- static get is() { return 'gr-comment'; }
- /**
- * Fired when the create fix comment action is triggered.
- *
- * @event create-fix-comment
- */
-
- /**
- * Fired when the show fix preview action is triggered.
- *
- * @event open-fix-preview
- */
-
- /**
- * Fired when this comment is discarded.
- *
- * @event comment-discard
- */
-
- /**
- * Fired when this comment is saved.
- *
- * @event comment-save
- */
-
- /**
- * Fired when this comment is updated.
- *
- * @event comment-update
- */
-
- /**
- * Fired when editing status changed.
- *
- * @event comment-editing-changed
- */
-
- /**
- * Fired when the comment's timestamp is tapped.
- *
- * @event comment-anchor-tap
- */
-
- static get properties() {
- return {
- changeNum: String,
- /** @type {!Gerrit.Comment} */
- comment: {
- type: Object,
- notify: true,
- observer: '_commentChanged',
- },
- comments: {
- type: Array,
- },
- isRobotComment: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- disabled: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- draft: {
- type: Boolean,
- value: false,
- observer: '_draftChanged',
- },
- editing: {
- type: Boolean,
- value: false,
- observer: '_editingChanged',
- },
- discarding: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- hasChildren: Boolean,
- patchNum: String,
- showActions: Boolean,
- _showHumanActions: Boolean,
- _showRobotActions: Boolean,
- collapsed: {
- type: Boolean,
- value: true,
- reflectToAttribute: true,
- observer: '_toggleCollapseClass',
- },
- /** @type {?} */
- projectConfig: Object,
- robotButtonDisabled: Boolean,
- _hasHumanReply: Boolean,
- _isAdmin: {
- type: Boolean,
- value: false,
- },
-
- _xhrPromise: Object, // Used for testing.
- _messageText: {
- type: String,
- value: '',
- observer: '_messageTextChanged',
- },
- commentSide: String,
- side: String,
-
- resolved: Boolean,
-
- _numPendingDraftRequests: {
- type: Object,
- value:
- {number: 0}, // Intentional to share the object across instances.
- },
-
- _enableOverlay: {
- type: Boolean,
- value: false,
- },
-
- /**
- * Property for storing references to overlay elements. When the overlays
- * are moved to getRootElement() to be shown they are no-longer
- * children, so they can't be queried along the tree, so they are stored
- * here.
- */
- _overlays: {
- type: Object,
- value: () => { return {}; },
- },
-
- _showRespectfulTip: {
- type: Boolean,
- value: false,
- },
- showPatchset: {
- type: Boolean,
- value: true,
- },
- _respectfulReviewTip: String,
- _respectfulTipDismissed: {
- type: Boolean,
- value: false,
- },
- _unableToSave: {
- type: Boolean,
- value: false,
- },
- _selfAccount: Object,
- };
- }
-
- static get observers() {
- return [
- '_commentMessageChanged(comment.message)',
- '_loadLocalDraft(changeNum, patchNum, comment)',
- '_isRobotComment(comment)',
- '_calculateActionstoShow(showActions, isRobotComment)',
- '_computeHasHumanReply(comment, comments.*)',
- '_onEditingChange(editing)',
- ];
- }
-
- get keyBindings() {
- return {
- 'ctrl+enter meta+enter ctrl+s meta+s': '_handleSaveKey',
- 'esc': '_handleEsc',
- };
- }
-
- constructor() {
- super();
- this.reporting = appContext.reportingService;
- }
-
- /** @override */
- attached() {
- super.attached();
- this.$.restAPI.getAccount().then(account => {
- this._selfAccount = account;
- });
- if (this.editing) {
- this.collapsed = false;
- } else if (this.comment) {
- this.collapsed = this.comment.collapsed;
- }
- this._getIsAdmin().then(isAdmin => {
- this._isAdmin = isAdmin;
- });
- }
-
- /** @override */
- detached() {
- super.detached();
- this.cancelDebouncer('fire-update');
- if (this.textarea) {
- this.textarea.closeDropdown();
- }
- }
-
- _getAuthor(comment) {
- return comment.author || this._selfAccount;
- }
-
- _onEditingChange(editing) {
- this.dispatchEvent(new CustomEvent('comment-editing-changed', {
- detail: !!editing,
- bubbles: true,
- composed: true,
- }));
- if (!editing) return;
- // visibility based on cache this will make sure we only and always show
- // a tip once every Math.max(a day, period between creating comments)
- const cachedVisibilityOfRespectfulTip =
- this.$.storage.getRespectfulTipVisibility();
- if (!cachedVisibilityOfRespectfulTip) {
- // we still want to show the tip with a probability of 30%
- if (this.getRandomNum(0, 3) >= 1) return;
- this._showRespectfulTip = true;
- const randomIdx = this.getRandomNum(0, RESPECTFUL_REVIEW_TIPS.length);
- this._respectfulReviewTip = RESPECTFUL_REVIEW_TIPS[randomIdx];
- this.reporting.reportInteraction(
- 'respectful-tip-appeared',
- {tip: this._respectfulReviewTip}
- );
- // update cache
- this.$.storage.setRespectfulTipVisibility();
- }
- }
-
- /** Set as a separate method so easy to stub. */
- getRandomNum(min, max) {
- return Math.floor(Math.random() * (max - min) + min);
- }
-
- _computeVisibilityOfTip(showTip, tipDismissed) {
- return showTip && !tipDismissed;
- }
-
- _dismissRespectfulTip() {
- this._respectfulTipDismissed = true;
- this.reporting.reportInteraction(
- 'respectful-tip-dismissed',
- {tip: this._respectfulReviewTip}
- );
- // add a 14-day delay to the tip cache
- this.$.storage.setRespectfulTipVisibility(/* delayDays= */ 14);
- }
-
- _onRespectfulReadMoreClick() {
- this.reporting.reportInteraction('respectful-read-more-clicked');
- }
-
- get textarea() {
- return this.shadowRoot.querySelector('#editTextarea');
- }
-
- get confirmDeleteOverlay() {
- if (!this._overlays.confirmDelete) {
- this._enableOverlay = true;
- flush();
- this._overlays.confirmDelete = this.shadowRoot
- .querySelector('#confirmDeleteOverlay');
- }
- return this._overlays.confirmDelete;
- }
-
- get confirmDiscardOverlay() {
- if (!this._overlays.confirmDiscard) {
- this._enableOverlay = true;
- flush();
- this._overlays.confirmDiscard = this.shadowRoot
- .querySelector('#confirmDiscardOverlay');
- }
- return this._overlays.confirmDiscard;
- }
-
- _computeShowHideIcon(collapsed) {
- return collapsed ? 'gr-icons:expand-more' : 'gr-icons:expand-less';
- }
-
- _computeShowHideAriaLabel(collapsed) {
- return collapsed ? 'Expand' : 'Collapse';
- }
-
- _calculateActionstoShow(showActions, isRobotComment) {
- // Polymer 2: check for undefined
- if ([showActions, isRobotComment].includes(undefined)) {
- return;
- }
-
- this._showHumanActions = showActions && !isRobotComment;
- this._showRobotActions = showActions && isRobotComment;
- }
-
- _isRobotComment(comment) {
- this.isRobotComment = !!comment.robot_id;
- }
-
- isOnParent() {
- return this.side === 'PARENT';
- }
-
- _getIsAdmin() {
- return this.$.restAPI.getIsAdmin();
- }
-
- _computeDraftTooltip(unableToSave) {
- return unableToSave ? `Unable to save draft. Please try to save again.` :
- `This draft is only visible to you. To publish drafts, click the 'Reply'`
- + `or 'Start review' button at the top of the change or press the 'A' key.`;
- }
-
- _computeDraftText(unableToSave) {
- return 'DRAFT' + (unableToSave ? '(Failed to save)' : '');
- }
-
- /**
- * @param {*=} opt_comment
- */
- save(opt_comment) {
- let comment = opt_comment;
- if (!comment) {
- comment = this.comment;
- }
-
- this.set('comment.message', this._messageText);
- this.editing = false;
- this.disabled = true;
-
- if (!this._messageText) {
- return this._discardDraft();
- }
-
- this._xhrPromise = this._saveDraft(comment).then(response => {
- this.disabled = false;
- if (!response.ok) { return response; }
-
- this._eraseDraftComment();
- return this.$.restAPI.getResponseObject(response).then(obj => {
- const resComment = obj;
- resComment.__draft = true;
- // Maintain the ephemeral draft ID for identification by other
- // elements.
- if (this.comment.__draftID) {
- resComment.__draftID = this.comment.__draftID;
- }
- resComment.__commentSide = this.commentSide;
- this.comment = resComment;
- this._fireSave();
- return obj;
- });
- })
- .catch(err => {
- this.disabled = false;
- throw err;
- });
-
- return this._xhrPromise;
- }
-
- _eraseDraftComment() {
- // Prevents a race condition in which removing the draft comment occurs
- // prior to it being saved.
- this.cancelDebouncer('store');
-
- this.$.storage.eraseDraftComment({
- changeNum: this.changeNum,
- patchNum: this._getPatchNum(),
- path: this.comment.path,
- line: this.comment.line,
- range: this.comment.range,
- });
- }
-
- _commentChanged(comment) {
- this.editing = !!comment.__editing;
- this.resolved = !comment.unresolved;
- if (this.editing) { // It's a new draft/reply, notify.
- this._fireUpdate();
- }
- }
-
- _computeHasHumanReply() {
- if (!this.comment || !this.comments) return;
- // hide please fix button for robot comment that has human reply
- this._hasHumanReply = this.comments
- .some(c => c.in_reply_to && c.in_reply_to === this.comment.id &&
- !c.robot_id);
- }
-
- /**
- * @param {!Object=} opt_mixin
- *
- * @return {!Object}
- */
- _getEventPayload(opt_mixin) {
- return {...opt_mixin, comment: this.comment,
- patchNum: this.patchNum};
- }
-
- _fireSave() {
- this.dispatchEvent(new CustomEvent('comment-save', {
- detail: this._getEventPayload(),
- composed: true, bubbles: true,
- }));
- }
-
- _fireUpdate() {
- this.debounce('fire-update', () => {
- this.dispatchEvent(new CustomEvent('comment-update', {
- detail: this._getEventPayload(),
- composed: true, bubbles: true,
- }));
- });
- }
-
- _computeAccountLabelClass(draft) {
- return draft ? 'draft' : '';
- }
-
- _draftChanged(draft) {
- this.$.container.classList.toggle('draft', draft);
- }
-
- _editingChanged(editing, previousValue) {
- // Polymer 2: observer fires when at least one property is defined.
- // Do nothing to prevent comment.__editing being overwritten
- // if previousValue is undefined
- if (previousValue === undefined) return;
-
- this.$.container.classList.toggle('editing', editing);
- if (this.comment && this.comment.id) {
- const cancelButton = this.shadowRoot.querySelector('.cancel');
- if (cancelButton) {
- cancelButton.hidden = !editing;
- }
- }
- if (this.comment) {
- this.comment.__editing = this.editing;
- }
- if (editing != !!previousValue) {
- // To prevent event firing on comment creation.
- this._fireUpdate();
- }
- if (editing) {
- this.async(() => {
- flush();
- this.textarea && this.textarea.putCursorAtEnd();
- }, 1);
- }
- }
-
- _computeDeleteButtonClass(isAdmin, draft) {
- return isAdmin && !draft ? 'showDeleteButtons' : '';
- }
-
- _computeSaveDisabled(draft, comment, resolved) {
- // If resolved state has changed and a msg exists, save should be enabled.
- if (!comment || comment.unresolved === resolved && draft) {
- return false;
- }
- return !draft || draft.trim() === '';
- }
-
- _handleSaveKey(e) {
- if (!this._computeSaveDisabled(this._messageText, this.comment,
- this.resolved)) {
- e.preventDefault();
- this._handleSave(e);
- }
- }
-
- _handleEsc(e) {
- if (!this._messageText.length) {
- e.preventDefault();
- this._handleCancel(e);
- }
- }
-
- _handleToggleCollapsed() {
- this.collapsed = !this.collapsed;
- }
-
- _toggleCollapseClass(collapsed) {
- if (collapsed) {
- this.$.container.classList.add('collapsed');
- } else {
- this.$.container.classList.remove('collapsed');
- }
- }
-
- _commentMessageChanged(message) {
- this._messageText = message || '';
- }
-
- _messageTextChanged(newValue, oldValue) {
- if (!this.comment || (this.comment && this.comment.id)) {
- return;
- }
-
- this.debounce('store', () => {
- const message = this._messageText;
- const commentLocation = {
- changeNum: this.changeNum,
- patchNum: this._getPatchNum(),
- path: this.comment.path,
- line: this.comment.line,
- range: this.comment.range,
- };
-
- if ((!this._messageText || !this._messageText.length) && oldValue) {
- // If the draft has been modified to be empty, then erase the storage
- // entry.
- this.$.storage.eraseDraftComment(commentLocation);
- } else {
- this.$.storage.setDraftComment(commentLocation, message);
- }
- }, STORAGE_DEBOUNCE_INTERVAL);
- }
-
- _handleAnchorClick(e) {
- e.preventDefault();
- if (!this.comment.line) {
- return;
- }
- this.dispatchEvent(new CustomEvent('comment-anchor-tap', {
- bubbles: true,
- composed: true,
- detail: {
- number: this.comment.line || FILE,
- side: this.side,
- },
- }));
- }
-
- _handleEdit(e) {
- e.preventDefault();
- this._messageText = this.comment.message;
- this.editing = true;
- this.reporting.recordDraftInteraction();
- }
-
- _handleSave(e) {
- e.preventDefault();
-
- // Ignore saves started while already saving.
- if (this.disabled) {
- return;
- }
- const timingLabel = this.comment.id ?
- REPORT_UPDATE_DRAFT : REPORT_CREATE_DRAFT;
- const timer = this.reporting.getTimer(timingLabel);
- this.set('comment.__editing', false);
- return this.save().then(() => { timer.end(); });
- }
-
- _handleCancel(e) {
- e.preventDefault();
-
- if (!this.comment.message ||
- this.comment.message.trim().length === 0 ||
- !this.comment.id) {
- this._fireDiscard();
- return;
- }
- this._messageText = this.comment.message;
- this.editing = false;
- }
-
- _fireDiscard() {
- this.cancelDebouncer('fire-update');
- this.dispatchEvent(new CustomEvent('comment-discard', {
- detail: this._getEventPayload(),
- composed: true, bubbles: true,
- }));
- }
-
- _handleFix() {
- this.dispatchEvent(new CustomEvent('create-fix-comment', {
- bubbles: true,
- composed: true,
- detail: this._getEventPayload(),
- }));
- }
-
- _handleShowFix() {
- this.dispatchEvent(new CustomEvent('open-fix-preview', {
- bubbles: true,
- composed: true,
- detail: this._getEventPayload(),
- }));
- }
-
- _hasNoFix(comment) {
- return !comment || !comment.fix_suggestions;
- }
-
- _handleDiscard(e) {
- e.preventDefault();
- this.reporting.recordDraftInteraction();
-
- if (!this._messageText) {
- this._discardDraft();
- return;
- }
-
- this._openOverlay(this.confirmDiscardOverlay).then(() => {
- this.confirmDiscardOverlay.querySelector('#confirmDiscardDialog')
- .resetFocus();
- });
- }
-
- _handleConfirmDiscard(e) {
- e.preventDefault();
- const timer = this.reporting.getTimer(REPORT_DISCARD_DRAFT);
- this._closeConfirmDiscardOverlay();
- return this._discardDraft().then(() => { timer.end(); });
- }
-
- _discardDraft() {
- if (!this.comment.__draft) {
- throw Error('Cannot discard a non-draft comment.');
- }
- this.discarding = true;
- this.editing = false;
- this.disabled = true;
- this._eraseDraftComment();
-
- if (!this.comment.id) {
- this.disabled = false;
- this._fireDiscard();
- return;
- }
-
- this._xhrPromise = this._deleteDraft(this.comment).then(response => {
- this.disabled = false;
- if (!response.ok) {
- this.discarding = false;
- return response;
- }
-
- this._fireDiscard();
- })
- .catch(err => {
- this.disabled = false;
- throw err;
- });
-
- return this._xhrPromise;
- }
-
- _closeConfirmDiscardOverlay() {
- this._closeOverlay(this.confirmDiscardOverlay);
- }
-
- _getSavingMessage(numPending, requestFailed) {
- if (requestFailed) {
- return UNSAVED_MESSAGE;
- }
- if (numPending === 0) {
- return SAVED_MESSAGE;
- }
- return [
- SAVING_MESSAGE,
- numPending,
- numPending === 1 ? DRAFT_SINGULAR : DRAFT_PLURAL,
- ].join(' ');
- }
-
- _showStartRequest() {
- const numPending = ++this._numPendingDraftRequests.number;
- this._updateRequestToast(numPending);
- }
-
- _showEndRequest() {
- const numPending = --this._numPendingDraftRequests.number;
- this._updateRequestToast(numPending);
- }
-
- _handleFailedDraftRequest() {
- this._numPendingDraftRequests.number--;
-
- // Cancel the debouncer so that error toasts from the error-manager will
- // not be overridden.
- this.cancelDebouncer('draft-toast');
- this._updateRequestToast(this._numPendingDraftRequests.number,
- /* requestFailed=*/true);
- }
-
- _updateRequestToast(numPending, requestFailed) {
- const message = this._getSavingMessage(numPending, requestFailed);
- this.debounce('draft-toast', () => {
- // Note: the event is fired on the body rather than this element because
- // this element may not be attached by the time this executes, in which
- // case the event would not bubble.
- document.body.dispatchEvent(new CustomEvent(
- 'show-alert', {detail: {message}, bubbles: true, composed: true}));
- }, TOAST_DEBOUNCE_INTERVAL);
- }
-
- _handleDraftFailure() {
- this.$.container.classList.add('unableToSave');
- this._unableToSave = true;
- this._handleFailedDraftRequest();
- }
-
- _saveDraft(draft) {
- this._showStartRequest();
- return this.$.restAPI.saveDiffDraft(this.changeNum, this.patchNum, draft)
- .then(result => {
- if (result.ok) { // remove
- this._unableToSave = false;
- this.$.container.classList.remove('unableToSave');
- this._showEndRequest();
- } else {
- this._handleDraftFailure();
- }
- return result;
- })
- .catch(err => {
- this._handleDraftFailure();
- throw (err);
- });
- }
-
- _deleteDraft(draft) {
- this._showStartRequest();
- return this.$.restAPI.deleteDiffDraft(this.changeNum, this.patchNum,
- draft).then(result => {
- if (result.ok) {
- this._showEndRequest();
- } else {
- this._handleFailedDraftRequest();
- }
- return result;
- });
- }
-
- _getPatchNum() {
- return this.isOnParent() ? 'PARENT' : this.patchNum;
- }
-
- _loadLocalDraft(changeNum, patchNum, comment) {
- // Polymer 2: check for undefined
- if ([changeNum, patchNum, comment].includes(undefined)) {
- return;
- }
-
- // Only apply local drafts to comments that haven't been saved
- // remotely, and haven't been given a default message already.
- //
- // Don't get local draft if there is another comment that is currently
- // in an editing state.
- if (!comment || comment.id || comment.message || comment.__otherEditing) {
- delete comment.__otherEditing;
- return;
- }
-
- const draft = this.$.storage.getDraftComment({
- changeNum,
- patchNum: this._getPatchNum(),
- path: comment.path,
- line: comment.line,
- range: comment.range,
- });
-
- if (draft) {
- this.set('comment.message', draft.message);
- }
- }
-
- _handleToggleResolved() {
- this.reporting.recordDraftInteraction();
- this.resolved = !this.resolved;
- // Modify payload instead of this.comment, as this.comment is passed from
- // the parent by ref.
- const payload = this._getEventPayload();
- payload.comment.unresolved = !this.$.resolvedCheckbox.checked;
- this.dispatchEvent(new CustomEvent('comment-update', {
- detail: payload,
- composed: true, bubbles: true,
- }));
- if (!this.editing) {
- // Save the resolved state immediately.
- this.save(payload.comment);
- }
- }
-
- _handleCommentDelete() {
- this._openOverlay(this.confirmDeleteOverlay);
- }
-
- _handleCancelDeleteComment() {
- this._closeOverlay(this.confirmDeleteOverlay);
- }
-
- _openOverlay(overlay) {
- getRootElement().appendChild(overlay);
- return overlay.open();
- }
-
- _computeHideRunDetails(comment, collapsed) {
- if (!comment) return true;
- return !(comment.robot_id && comment.url && !collapsed);
- }
-
- _closeOverlay(overlay) {
- getRootElement().removeChild(overlay);
- overlay.close();
- }
-
- _handleConfirmDeleteComment() {
- const dialog =
- this.confirmDeleteOverlay.querySelector('#confirmDeleteComment');
- this.$.restAPI.deleteComment(
- this.changeNum, this.patchNum, this.comment.id, dialog.message)
- .then(newComment => {
- this._handleCancelDeleteComment();
- this.comment = newComment;
- });
- }
-}
-
-customElements.define(GrComment.is, GrComment);
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
new file mode 100644
index 0000000..209b2c9
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -0,0 +1,1050 @@
+/**
+ * @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-autogrow-textarea/iron-autogrow-textarea';
+import '../../../styles/shared-styles';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param';
+import '../gr-button/gr-button';
+import '../gr-dialog/gr-dialog';
+import '../gr-date-formatter/gr-date-formatter';
+import '../gr-formatted-text/gr-formatted-text';
+import '../gr-icons/gr-icons';
+import '../gr-overlay/gr-overlay';
+import '../gr-rest-api-interface/gr-rest-api-interface';
+import '../gr-storage/gr-storage';
+import '../gr-textarea/gr-textarea';
+import '../gr-tooltip-content/gr-tooltip-content';
+import '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog';
+import '../gr-account-label/gr-account-label';
+import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {htmlTemplate} from './gr-comment_html';
+import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
+import {getRootElement} from '../../../scripts/rootElement';
+import {appContext} from '../../../services/app-context';
+import {customElement, property, observe} from '@polymer/decorators';
+import {RestApiService} from '../../../services/services/gr-rest-api/gr-rest-api';
+import {GrTextarea} from '../gr-textarea/gr-textarea';
+import {GrStorage, StorageLocation} from '../gr-storage/gr-storage';
+import {GrOverlay} from '../gr-overlay/gr-overlay';
+import {
+ RobotCommentInfo,
+ PatchSetNum,
+ CommentInfo,
+ ConfigInfo,
+ AccountDetailInfo,
+} from '../../../types/common';
+import {GrButton} from '../gr-button/gr-button';
+import {GrConfirmDeleteCommentDialog} from '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog';
+import {GrDialog} from '../gr-dialog/gr-dialog';
+
+const STORAGE_DEBOUNCE_INTERVAL = 400;
+const TOAST_DEBOUNCE_INTERVAL = 200;
+
+const SAVING_MESSAGE = 'Saving';
+const DRAFT_SINGULAR = 'draft...';
+const DRAFT_PLURAL = 'drafts...';
+const SAVED_MESSAGE = 'All changes saved';
+const UNSAVED_MESSAGE = 'Unable to save draft';
+
+const REPORT_CREATE_DRAFT = 'CreateDraftComment';
+const REPORT_UPDATE_DRAFT = 'UpdateDraftComment';
+const REPORT_DISCARD_DRAFT = 'DiscardDraftComment';
+
+const FILE = 'FILE';
+
+export const __testOnly_UNSAVED_MESSAGE = UNSAVED_MESSAGE;
+
+/**
+ * All candidates tips to show, will pick randomly.
+ */
+const RESPECTFUL_REVIEW_TIPS = [
+ 'Assume competence.',
+ 'Provide rationale or context.',
+ 'Consider how comments may be interpreted.',
+ 'Avoid harsh language.',
+ 'Make your comments specific and actionable.',
+ 'When disagreeing, explain the advantage of your approach.',
+];
+
+interface Draft {
+ collapsed?: boolean;
+ __editing?: boolean;
+ __otherEditing?: boolean;
+ __draft?: boolean;
+ __draftID?: number;
+ __commentSide?: string;
+}
+
+export type Comment = Draft & CommentInfo;
+export type RobotComment = Draft & RobotCommentInfo;
+
+interface CommentOverlays {
+ confirmDelete?: GrOverlay | null;
+ confirmDiscard?: GrOverlay | null;
+}
+
+export interface GrComment {
+ $: {
+ restAPI: RestApiService & Element;
+ storage: GrStorage;
+ container: HTMLDivElement;
+ resolvedCheckbox: HTMLInputElement;
+ };
+}
+@customElement('gr-comment')
+export class GrComment extends KeyboardShortcutMixin(
+ GestureEventListeners(LegacyElementMixin(PolymerElement))
+) {
+ static get template() {
+ return htmlTemplate;
+ }
+
+ /**
+ * Fired when the create fix comment action is triggered.
+ *
+ * @event create-fix-comment
+ */
+
+ /**
+ * Fired when the show fix preview action is triggered.
+ *
+ * @event open-fix-preview
+ */
+
+ /**
+ * Fired when this comment is discarded.
+ *
+ * @event comment-discard
+ */
+
+ /**
+ * Fired when this comment is saved.
+ *
+ * @event comment-save
+ */
+
+ /**
+ * Fired when this comment is updated.
+ *
+ * @event comment-update
+ */
+
+ /**
+ * Fired when editing status changed.
+ *
+ * @event comment-editing-changed
+ */
+
+ /**
+ * Fired when the comment's timestamp is tapped.
+ *
+ * @event comment-anchor-tap
+ */
+
+ @property({type: Number})
+ changeNum?: number;
+
+ @property({type: Object, notify: true, observer: '_commentChanged'})
+ comment?: Comment | RobotComment;
+
+ @property({type: Array})
+ comments?: (Comment | RobotComment)[];
+
+ @property({type: Boolean, reflectToAttribute: true})
+ isRobotComment = false;
+
+ @property({type: Boolean, reflectToAttribute: true})
+ disabled = false;
+
+ @property({type: Boolean, observer: '_draftChanged'})
+ draft = false;
+
+ @property({type: Boolean, observer: '_editingChanged'})
+ editing = false;
+
+ @property({type: Boolean, reflectToAttribute: true})
+ discarding = false;
+
+ @property({type: Boolean})
+ hasChildren?: boolean;
+
+ @property({type: String})
+ patchNum?: PatchSetNum;
+
+ @property({type: Boolean})
+ showActions?: boolean;
+
+ @property({type: Boolean})
+ _showHumanActions?: boolean;
+
+ @property({type: Boolean})
+ _showRobotActions?: boolean;
+
+ @property({
+ type: Boolean,
+ reflectToAttribute: true,
+ observer: '_toggleCollapseClass',
+ })
+ collapsed = true;
+
+ @property({type: Object})
+ projectConfig?: ConfigInfo;
+
+ @property({type: Boolean})
+ robotButtonDisabled?: boolean;
+
+ @property({type: Boolean})
+ _hasHumanReply?: boolean;
+
+ @property({type: Boolean})
+ _isAdmin = false;
+
+ @property({type: Object})
+ _xhrPromise?: Promise<any>; // Used for testing.
+
+ @property({type: String, observer: '_messageTextChanged'})
+ _messageText = '';
+
+ @property({type: String})
+ commentSide?: string;
+
+ @property({type: String})
+ side?: string;
+
+ @property({type: Boolean})
+ resolved?: boolean;
+
+ // Intentional to share the object across instances.
+ @property({type: Object})
+ _numPendingDraftRequests: {number: number} = {number: 0};
+
+ @property({type: Boolean})
+ _enableOverlay = false;
+
+ /**
+ * Property for storing references to overlay elements. When the overlays
+ * are moved to getRootElement() to be shown they are no-longer
+ * children, so they can't be queried along the tree, so they are stored
+ * here.
+ */
+ @property({type: Object})
+ _overlays: CommentOverlays = {};
+
+ @property({type: Boolean})
+ _showRespectfulTip = false;
+
+ @property({type: Boolean})
+ showPatchset = true;
+
+ @property({type: String})
+ _respectfulReviewTip?: string;
+
+ @property({type: Boolean})
+ _respectfulTipDismissed = false;
+
+ @property({type: Boolean})
+ _unableToSave = false;
+
+ @property({type: Object})
+ _selfAccount?: AccountDetailInfo;
+
+ get keyBindings() {
+ return {
+ 'ctrl+enter meta+enter ctrl+s meta+s': '_handleSaveKey',
+ esc: '_handleEsc',
+ };
+ }
+
+ reporting = appContext.reportingService;
+
+ /** @override */
+ attached() {
+ super.attached();
+ this.$.restAPI.getAccount().then(account => {
+ this._selfAccount = account;
+ });
+ if (this.editing) {
+ this.collapsed = false;
+ } else if (this.comment) {
+ this.collapsed = !!this.comment.collapsed;
+ }
+ this._getIsAdmin().then(isAdmin => {
+ this._isAdmin = !!isAdmin;
+ });
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this.cancelDebouncer('fire-update');
+ if (this.textarea) {
+ this.textarea.closeDropdown();
+ }
+ }
+
+ _getAuthor(comment: Comment) {
+ return comment.author || this._selfAccount;
+ }
+
+ @observe('editing')
+ _onEditingChange(editing?: boolean) {
+ this.dispatchEvent(
+ new CustomEvent('comment-editing-changed', {
+ detail: !!editing,
+ bubbles: true,
+ composed: true,
+ })
+ );
+ if (!editing) return;
+ // visibility based on cache this will make sure we only and always show
+ // a tip once every Math.max(a day, period between creating comments)
+ const cachedVisibilityOfRespectfulTip = this.$.storage.getRespectfulTipVisibility();
+ if (!cachedVisibilityOfRespectfulTip) {
+ // we still want to show the tip with a probability of 30%
+ if (this.getRandomNum(0, 3) >= 1) return;
+ this._showRespectfulTip = true;
+ const randomIdx = this.getRandomNum(0, RESPECTFUL_REVIEW_TIPS.length);
+ this._respectfulReviewTip = RESPECTFUL_REVIEW_TIPS[randomIdx];
+ this.reporting.reportInteraction('respectful-tip-appeared', {
+ tip: this._respectfulReviewTip,
+ });
+ // update cache
+ this.$.storage.setRespectfulTipVisibility();
+ }
+ }
+
+ /** Set as a separate method so easy to stub. */
+ getRandomNum(min: number, max: number) {
+ return Math.floor(Math.random() * (max - min) + min);
+ }
+
+ _computeVisibilityOfTip(showTip: boolean, tipDismissed: boolean) {
+ return showTip && !tipDismissed;
+ }
+
+ _dismissRespectfulTip() {
+ this._respectfulTipDismissed = true;
+ this.reporting.reportInteraction('respectful-tip-dismissed', {
+ tip: this._respectfulReviewTip,
+ });
+ // add a 14-day delay to the tip cache
+ this.$.storage.setRespectfulTipVisibility(/* delayDays= */ 14);
+ }
+
+ _onRespectfulReadMoreClick() {
+ this.reporting.reportInteraction('respectful-read-more-clicked');
+ }
+
+ get textarea(): GrTextarea | null {
+ return this.shadowRoot?.querySelector('#editTextarea') as GrTextarea | null;
+ }
+
+ get confirmDeleteOverlay() {
+ if (!this._overlays.confirmDelete) {
+ this._enableOverlay = true;
+ flush();
+ this._overlays.confirmDelete = this.shadowRoot?.querySelector(
+ '#confirmDeleteOverlay'
+ ) as GrOverlay | null;
+ }
+ return this._overlays.confirmDelete;
+ }
+
+ get confirmDiscardOverlay() {
+ if (!this._overlays.confirmDiscard) {
+ this._enableOverlay = true;
+ flush();
+ this._overlays.confirmDiscard = this.shadowRoot?.querySelector(
+ '#confirmDiscardOverlay'
+ ) as GrOverlay | null;
+ }
+ return this._overlays.confirmDiscard;
+ }
+
+ _computeShowHideIcon(collapsed: boolean) {
+ return collapsed ? 'gr-icons:expand-more' : 'gr-icons:expand-less';
+ }
+
+ _computeShowHideAriaLabel(collapsed: boolean) {
+ return collapsed ? 'Expand' : 'Collapse';
+ }
+
+ @observe('showActions', 'isRobotComment')
+ _calculateActionstoShow(showActions?: boolean, isRobotComment?: boolean) {
+ // Polymer 2: check for undefined
+ if ([showActions, isRobotComment].includes(undefined)) {
+ return;
+ }
+
+ this._showHumanActions = showActions && !isRobotComment;
+ this._showRobotActions = showActions && isRobotComment;
+ }
+
+ @observe('comment')
+ _isRobotComment(comment: RobotComment) {
+ this.isRobotComment = !!comment.robot_id;
+ }
+
+ isOnParent() {
+ return this.side === 'PARENT';
+ }
+
+ _getIsAdmin() {
+ return this.$.restAPI.getIsAdmin();
+ }
+
+ _computeDraftTooltip(unableToSave: boolean) {
+ return unableToSave
+ ? 'Unable to save draft. Please try to save again.'
+ : "This draft is only visible to you. To publish drafts, click the 'Reply'" +
+ "or 'Start review' button at the top of the change or press the 'A' key.";
+ }
+
+ _computeDraftText(unableToSave: boolean) {
+ return 'DRAFT' + (unableToSave ? '(Failed to save)' : '');
+ }
+
+ save(opt_comment?: Comment) {
+ let comment = opt_comment;
+ if (!comment) {
+ comment = this.comment;
+ }
+
+ this.set('comment.message', this._messageText);
+ this.editing = false;
+ this.disabled = true;
+
+ if (!this._messageText) {
+ return this._discardDraft();
+ }
+
+ this._xhrPromise = this._saveDraft(comment)
+ .then(response => {
+ this.disabled = false;
+ if (!response.ok) {
+ return;
+ }
+
+ this._eraseDraftComment();
+ return this.$.restAPI.getResponseObject(response).then(obj => {
+ const resComment = (obj as unknown) as Comment;
+ resComment.__draft = true;
+ // Maintain the ephemeral draft ID for identification by other
+ // elements.
+ if (this.comment?.__draftID) {
+ resComment.__draftID = this.comment.__draftID;
+ }
+ resComment.__commentSide = this.commentSide;
+ this.comment = resComment;
+ this._fireSave();
+ return obj;
+ });
+ })
+ .catch(err => {
+ this.disabled = false;
+ throw err;
+ });
+
+ return this._xhrPromise;
+ }
+
+ _eraseDraftComment() {
+ // Prevents a race condition in which removing the draft comment occurs
+ // prior to it being saved.
+ this.cancelDebouncer('store');
+
+ if (!this.comment?.path || this.comment.line === undefined)
+ throw new Error('Cannot erase Draft Comment');
+ if (this.changeNum === undefined) {
+ throw new Error('undefined changeNum');
+ }
+ this.$.storage.eraseDraftComment({
+ changeNum: this.changeNum,
+ patchNum: this._getPatchNum(),
+ path: this.comment.path,
+ line: this.comment.line,
+ range: this.comment.range,
+ });
+ }
+
+ _commentChanged(comment: Comment) {
+ this.editing = !!comment.__editing;
+ this.resolved = !comment.unresolved;
+ if (this.editing) {
+ // It's a new draft/reply, notify.
+ this._fireUpdate();
+ }
+ }
+
+ @observe('comment', 'comments.*')
+ _computeHasHumanReply() {
+ const comment = this.comment;
+ if (!comment || !this.comments) return;
+ // hide please fix button for robot comment that has human reply
+ this._hasHumanReply = this.comments.some(
+ c =>
+ c.in_reply_to &&
+ c.in_reply_to === comment.id &&
+ !(c as RobotComment).robot_id
+ );
+ }
+
+ _getEventPayload(opt_mixin?: Record<string, any>) {
+ return {...opt_mixin, comment: this.comment, patchNum: this.patchNum};
+ }
+
+ _fireSave() {
+ this.dispatchEvent(
+ new CustomEvent('comment-save', {
+ detail: this._getEventPayload(),
+ composed: true,
+ bubbles: true,
+ })
+ );
+ }
+
+ _fireUpdate() {
+ this.debounce('fire-update', () => {
+ this.dispatchEvent(
+ new CustomEvent('comment-update', {
+ detail: this._getEventPayload(),
+ composed: true,
+ bubbles: true,
+ })
+ );
+ });
+ }
+
+ _computeAccountLabelClass(draft: boolean) {
+ return draft ? 'draft' : '';
+ }
+
+ _draftChanged(draft: boolean) {
+ this.$.container.classList.toggle('draft', draft);
+ }
+
+ _editingChanged(editing?: boolean, previousValue?: boolean) {
+ // Polymer 2: observer fires when at least one property is defined.
+ // Do nothing to prevent comment.__editing being overwritten
+ // if previousValue is undefined
+ if (previousValue === undefined) return;
+
+ this.$.container.classList.toggle('editing', editing);
+ if (this.comment && this.comment.id) {
+ const cancelButton = this.shadowRoot?.querySelector(
+ '.cancel'
+ ) as GrButton | null;
+ if (cancelButton) {
+ cancelButton.hidden = !editing;
+ }
+ }
+ if (this.comment) {
+ this.comment.__editing = this.editing;
+ }
+ if (!!editing !== !!previousValue) {
+ // To prevent event firing on comment creation.
+ this._fireUpdate();
+ }
+ if (editing) {
+ this.async(() => {
+ flush();
+ this.textarea && this.textarea.putCursorAtEnd();
+ }, 1);
+ }
+ }
+
+ _computeDeleteButtonClass(isAdmin: boolean, draft: boolean) {
+ return isAdmin && !draft ? 'showDeleteButtons' : '';
+ }
+
+ _computeSaveDisabled(
+ draft: string,
+ comment: Comment | undefined,
+ resolved?: boolean
+ ) {
+ // If resolved state has changed and a msg exists, save should be enabled.
+ if (!comment || (comment.unresolved === resolved && draft)) {
+ return false;
+ }
+ return !draft || draft.trim() === '';
+ }
+
+ _handleSaveKey(e: Event) {
+ if (
+ !this._computeSaveDisabled(this._messageText, this.comment, this.resolved)
+ ) {
+ e.preventDefault();
+ this._handleSave(e);
+ }
+ }
+
+ _handleEsc(e: Event) {
+ if (!this._messageText.length) {
+ e.preventDefault();
+ this._handleCancel(e);
+ }
+ }
+
+ _handleToggleCollapsed() {
+ this.collapsed = !this.collapsed;
+ }
+
+ _toggleCollapseClass(collapsed: boolean) {
+ if (collapsed) {
+ this.$.container.classList.add('collapsed');
+ } else {
+ this.$.container.classList.remove('collapsed');
+ }
+ }
+
+ @observe('comment.message')
+ _commentMessageChanged(message: string) {
+ this._messageText = message || '';
+ }
+
+ _messageTextChanged(_: string, oldValue: string) {
+ if (!this.comment || (this.comment && this.comment.id)) {
+ return;
+ }
+
+ const patchNum = this.comment.patch_set
+ ? this.comment.patch_set
+ : this._getPatchNum();
+ this.debounce(
+ 'store',
+ () => {
+ const message = this._messageText;
+ if (!this.comment?.path || this.comment.line === undefined)
+ throw new Error('missing path or line in comment');
+ if (this.changeNum === undefined) {
+ throw new Error('undefined changeNum');
+ }
+ const commentLocation: StorageLocation = {
+ changeNum: this.changeNum,
+ patchNum,
+ path: this.comment.path,
+ line: this.comment.line,
+ range: this.comment.range,
+ };
+
+ if ((!this._messageText || !this._messageText.length) && oldValue) {
+ // If the draft has been modified to be empty, then erase the storage
+ // entry.
+ this.$.storage.eraseDraftComment(commentLocation);
+ } else {
+ this.$.storage.setDraftComment(commentLocation, message);
+ }
+ },
+ STORAGE_DEBOUNCE_INTERVAL
+ );
+ }
+
+ _handleAnchorClick(e: Event) {
+ e.preventDefault();
+ if (!this.comment?.line) {
+ return;
+ }
+ this.dispatchEvent(
+ new CustomEvent('comment-anchor-tap', {
+ bubbles: true,
+ composed: true,
+ detail: {
+ number: this.comment.line || FILE,
+ side: this.side,
+ },
+ })
+ );
+ }
+
+ _handleEdit(e: Event) {
+ e.preventDefault();
+ if (!this.comment?.message) throw new Error('message undefined');
+ this._messageText = this.comment.message;
+ this.editing = true;
+ this.reporting.recordDraftInteraction();
+ }
+
+ _handleSave(e: Event) {
+ e.preventDefault();
+
+ // Ignore saves started while already saving.
+ if (this.disabled) {
+ return;
+ }
+ const timingLabel = this.comment?.id
+ ? REPORT_UPDATE_DRAFT
+ : REPORT_CREATE_DRAFT;
+ const timer = this.reporting.getTimer(timingLabel);
+ this.set('comment.__editing', false);
+ return this.save().then(() => {
+ timer.end();
+ });
+ }
+
+ _handleCancel(e: Event) {
+ e.preventDefault();
+
+ if (
+ !this.comment?.message ||
+ this.comment.message.trim().length === 0 ||
+ !this.comment.id
+ ) {
+ this._fireDiscard();
+ return;
+ }
+ this._messageText = this.comment.message;
+ this.editing = false;
+ }
+
+ _fireDiscard() {
+ this.cancelDebouncer('fire-update');
+ this.dispatchEvent(
+ new CustomEvent('comment-discard', {
+ detail: this._getEventPayload(),
+ composed: true,
+ bubbles: true,
+ })
+ );
+ }
+
+ _handleFix() {
+ this.dispatchEvent(
+ new CustomEvent('create-fix-comment', {
+ bubbles: true,
+ composed: true,
+ detail: this._getEventPayload(),
+ })
+ );
+ }
+
+ _handleShowFix() {
+ this.dispatchEvent(
+ new CustomEvent('open-fix-preview', {
+ bubbles: true,
+ composed: true,
+ detail: this._getEventPayload(),
+ })
+ );
+ }
+
+ _hasNoFix(comment: Comment) {
+ return !comment || !(comment as RobotComment).fix_suggestions;
+ }
+
+ _handleDiscard(e: Event) {
+ e.preventDefault();
+ this.reporting.recordDraftInteraction();
+
+ if (!this._messageText) {
+ this._discardDraft();
+ return;
+ }
+
+ this._openOverlay(this.confirmDiscardOverlay).then(() => {
+ const dialog = this.confirmDiscardOverlay?.querySelector(
+ '#confirmDiscardDialog'
+ ) as GrDialog | null;
+ if (dialog) dialog.resetFocus();
+ });
+ }
+
+ _handleConfirmDiscard(e: Event) {
+ e.preventDefault();
+ const timer = this.reporting.getTimer(REPORT_DISCARD_DRAFT);
+ this._closeConfirmDiscardOverlay();
+ return this._discardDraft().then(() => {
+ timer.end();
+ });
+ }
+
+ _discardDraft() {
+ if (!this.comment) return Promise.reject(new Error('undefined comment'));
+ if (!this.comment.__draft) {
+ return Promise.reject(new Error('Cannot discard a non-draft comment.'));
+ }
+ this.discarding = true;
+ this.editing = false;
+ this.disabled = true;
+ this._eraseDraftComment();
+
+ if (!this.comment.id) {
+ this.disabled = false;
+ this._fireDiscard();
+ return Promise.resolve();
+ }
+
+ this._xhrPromise = this._deleteDraft(this.comment)
+ .then(response => {
+ this.disabled = false;
+ if (!response.ok) {
+ this.discarding = false;
+ }
+
+ this._fireDiscard();
+ return response;
+ })
+ .catch(err => {
+ this.disabled = false;
+ throw err;
+ });
+
+ return this._xhrPromise;
+ }
+
+ _closeConfirmDiscardOverlay() {
+ this._closeOverlay(this.confirmDiscardOverlay);
+ }
+
+ _getSavingMessage(numPending: number, requestFailed?: boolean) {
+ if (requestFailed) {
+ return UNSAVED_MESSAGE;
+ }
+ if (numPending === 0) {
+ return SAVED_MESSAGE;
+ }
+ return [
+ SAVING_MESSAGE,
+ numPending,
+ numPending === 1 ? DRAFT_SINGULAR : DRAFT_PLURAL,
+ ].join(' ');
+ }
+
+ _showStartRequest() {
+ const numPending = ++this._numPendingDraftRequests.number;
+ this._updateRequestToast(numPending);
+ }
+
+ _showEndRequest() {
+ const numPending = --this._numPendingDraftRequests.number;
+ this._updateRequestToast(numPending);
+ }
+
+ _handleFailedDraftRequest() {
+ this._numPendingDraftRequests.number--;
+
+ // Cancel the debouncer so that error toasts from the error-manager will
+ // not be overridden.
+ this.cancelDebouncer('draft-toast');
+ this._updateRequestToast(
+ this._numPendingDraftRequests.number,
+ /* requestFailed=*/ true
+ );
+ }
+
+ _updateRequestToast(numPending: number, requestFailed?: boolean) {
+ const message = this._getSavingMessage(numPending, requestFailed);
+ this.debounce(
+ 'draft-toast',
+ () => {
+ // Note: the event is fired on the body rather than this element because
+ // this element may not be attached by the time this executes, in which
+ // case the event would not bubble.
+ document.body.dispatchEvent(
+ new CustomEvent('show-alert', {
+ detail: {message},
+ bubbles: true,
+ composed: true,
+ })
+ );
+ },
+ TOAST_DEBOUNCE_INTERVAL
+ );
+ }
+
+ _handleDraftFailure() {
+ this.$.container.classList.add('unableToSave');
+ this._unableToSave = true;
+ this._handleFailedDraftRequest();
+ }
+
+ _saveDraft(draft?: Comment) {
+ if (!draft || this.changeNum === undefined || this.patchNum === undefined) {
+ throw new Error('undefined draft or changeNum or patchNum');
+ }
+ this._showStartRequest();
+ return this.$.restAPI
+ .saveDiffDraft(this.changeNum, this.patchNum, draft)
+ .then(result => {
+ if (result.ok) {
+ // remove
+ this._unableToSave = false;
+ this.$.container.classList.remove('unableToSave');
+ this._showEndRequest();
+ } else {
+ this._handleDraftFailure();
+ }
+ return result;
+ })
+ .catch(err => {
+ this._handleDraftFailure();
+ throw err;
+ });
+ }
+
+ _deleteDraft(draft: Comment) {
+ if (this.changeNum === undefined || this.patchNum === undefined) {
+ throw new Error('undefined changeNum or patchNum');
+ }
+ this._showStartRequest();
+ return this.$.restAPI
+ .deleteDiffDraft(this.changeNum, this.patchNum, draft)
+ .then(result => {
+ if (result.ok) {
+ this._showEndRequest();
+ } else {
+ this._handleFailedDraftRequest();
+ }
+ return result;
+ });
+ }
+
+ _getPatchNum(): PatchSetNum {
+ const patchNum = this.isOnParent()
+ ? ('PARENT' as PatchSetNum)
+ : this.patchNum;
+ if (patchNum === undefined) throw new Error('patchNum undefined');
+ return patchNum;
+ }
+
+ @observe('changeNum', 'patchNum', 'comment')
+ _loadLocalDraft(
+ changeNum: number,
+ patchNum?: PatchSetNum,
+ comment?: Comment
+ ) {
+ // Polymer 2: check for undefined
+ if ([changeNum, patchNum, comment].includes(undefined)) {
+ return;
+ }
+
+ // Only apply local drafts to comments that haven't been saved
+ // remotely, and haven't been given a default message already.
+ //
+ // Don't get local draft if there is another comment that is currently
+ // in an editing state.
+ if (
+ !comment ||
+ comment.id ||
+ comment.message ||
+ comment.__otherEditing ||
+ !comment.path ||
+ !comment.line
+ ) {
+ if (comment) delete comment.__otherEditing;
+ return;
+ }
+
+ const draft = this.$.storage.getDraftComment({
+ changeNum,
+ patchNum: this._getPatchNum(),
+ path: comment.path,
+ line: comment.line,
+ range: comment.range,
+ });
+
+ if (draft) {
+ this.set('comment.message', draft.message);
+ }
+ }
+
+ _handleToggleResolved() {
+ this.reporting.recordDraftInteraction();
+ this.resolved = !this.resolved;
+ // Modify payload instead of this.comment, as this.comment is passed from
+ // the parent by ref.
+ const payload = this._getEventPayload();
+ if (!payload.comment) {
+ throw new Error('comment not defined in payload');
+ }
+ payload.comment.unresolved = !this.$.resolvedCheckbox.checked;
+ this.dispatchEvent(
+ new CustomEvent('comment-update', {
+ detail: payload,
+ composed: true,
+ bubbles: true,
+ })
+ );
+ if (!this.editing) {
+ // Save the resolved state immediately.
+ this.save(payload.comment);
+ }
+ }
+
+ _handleCommentDelete() {
+ this._openOverlay(this.confirmDeleteOverlay);
+ }
+
+ _handleCancelDeleteComment() {
+ this._closeOverlay(this.confirmDeleteOverlay);
+ }
+
+ _openOverlay(overlay?: GrOverlay | null) {
+ if (!overlay) {
+ return Promise.reject(new Error('undefined overlay'));
+ }
+ getRootElement().appendChild(overlay);
+ return overlay.open();
+ }
+
+ _computeHideRunDetails(comment: RobotComment, collapsed: boolean) {
+ if (!comment) return true;
+ return !(comment.robot_id && comment.url && !collapsed);
+ }
+
+ _closeOverlay(overlay?: GrOverlay | null) {
+ if (overlay) {
+ getRootElement().removeChild(overlay);
+ overlay.close();
+ }
+ }
+
+ _handleConfirmDeleteComment() {
+ const dialog = this.confirmDeleteOverlay?.querySelector(
+ '#confirmDeleteComment'
+ ) as GrConfirmDeleteCommentDialog | null;
+ if (!dialog || !dialog.message) {
+ throw new Error('missing confirm delete dialog');
+ }
+ if (
+ !this.comment ||
+ this.changeNum === undefined ||
+ this.patchNum === undefined
+ ) {
+ throw new Error('undefined comment or changeNum or patchNum');
+ }
+ this.$.restAPI
+ .deleteComment(
+ this.changeNum,
+ this.patchNum,
+ this.comment.id,
+ dialog.message
+ )
+ .then(newComment => {
+ this._handleCancelDeleteComment();
+ this.comment = newComment;
+ });
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-comment': GrComment;
+ }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
index 1299b90..a9a1d32 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
@@ -149,6 +149,7 @@
email: 'tenn1sballchaser@aol.com',
},
line: 5,
+ path: 'test',
};
flush(() => {
assert.isTrue(loadSpy.called);
@@ -368,11 +369,13 @@
test('failed save draft request', done => {
element.draft = true;
+ element.changeNum = 1;
+ element.patchNum = 1;
const updateRequestStub = sinon.stub(element, '_updateRequestToast');
const diffDraftStub =
sinon.stub(element.$.restAPI, 'saveDiffDraft').returns(
Promise.resolve({ok: false}));
- element._saveDraft();
+ element._saveDraft({id: 'abc_123'});
flush(() => {
let args = updateRequestStub.lastCall.args;
assert.deepEqual(args, [0, true]);
@@ -384,7 +387,7 @@
.querySelector('.save')), 'save is visible');
diffDraftStub.returns(
Promise.resolve({ok: true}));
- element._saveDraft();
+ element._saveDraft({id: 'abc_123'});
flush(() => {
args = updateRequestStub.lastCall.args;
assert.deepEqual(args, [0]);
@@ -402,11 +405,13 @@
test('failed save draft request with promise failure', done => {
element.draft = true;
+ element.changeNum = 1;
+ element.patchNum = 1;
const updateRequestStub = sinon.stub(element, '_updateRequestToast');
const diffDraftStub =
sinon.stub(element.$.restAPI, 'saveDiffDraft').returns(
Promise.reject(new Error()));
- element._saveDraft();
+ element._saveDraft({id: 'abc_123'});
flush(() => {
let args = updateRequestStub.lastCall.args;
assert.deepEqual(args, [0, true]);
@@ -418,7 +423,7 @@
.querySelector('.save')), 'save is visible');
diffDraftStub.returns(
Promise.resolve({ok: true}));
- element._saveDraft();
+ element._saveDraft({id: 'abc_123'});
flush(() => {
args = updateRequestStub.lastCall.args;
assert.deepEqual(args, [0]);
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts
index c4ebe1e..a3233be 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts
@@ -137,6 +137,11 @@
this.listen(this, 'mouseleave', 'unlock');
}
+ detached() {
+ super.detached();
+ this.unlock();
+ }
+
/** @override */
ready() {
super.ready();
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
index f4111e7..8461027 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
@@ -125,6 +125,7 @@
ProjectAccessInfo,
CapabilityInfoMap,
ProjectInfoWithName,
+ TagInfo,
} from '../../../types/common';
import {
CancelConditionCallback,
@@ -1795,11 +1796,11 @@
`/projects/${encodedRepo}/tags` + `?n=${n}&S=${offset}` + encodedFilter;
// TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
// supports it.
- return this._restApiHelper.fetchJSON({
+ return (this._restApiHelper.fetchJSON({
url,
errFn,
anonymizedUrl: '/projects/*/tags',
- });
+ }) as unknown) as Promise<TagInfo[]>;
}
getPlugins(
@@ -3226,7 +3227,7 @@
commentID: UrlEncodedCommentId,
reason: string
) {
- return this._getChangeURLAndSend({
+ return (this._getChangeURLAndSend({
changeNum,
method: HttpMethod.POST,
patchNum,
@@ -3234,7 +3235,7 @@
body: {reason},
parseResponse: true,
anonymizedEndpoint: '/comments/*/delete',
- });
+ }) as unknown) as Promise<CommentInfo>;
}
/**
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.ts b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.ts
index 176f6c9..15914c5 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.ts
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.ts
@@ -25,7 +25,7 @@
patchNum: PatchSetNum;
path: string;
line: number;
- range: CommentRange;
+ range?: CommentRange;
}
export interface StorageObject {
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 e8fc3f9..5565180 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
@@ -78,6 +78,9 @@
GroupAuditEventInfo,
EncodedGroupId,
Base64FileContent,
+ UrlEncodedCommentId,
+ TagInfo,
+ GitRef,
} from '../../../types/common';
import {ParsedChangeInfo} from '../../../elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser';
import {HttpMethod} from '../../../constants/constants';
@@ -604,6 +607,18 @@
label: string
): Promise<Response>;
+ deleteComment(
+ changeNum: ChangeNum,
+ patchNum: PatchSetNum,
+ commentID: UrlEncodedCommentId,
+ reason: string
+ ): Promise<CommentInfo>;
+ deleteDiffDraft(
+ changeNum: ChangeNum,
+ patchNum: PatchSetNum,
+ draft: {id: UrlEncodedCommentId}
+ ): Promise<Response>;
+
deleteChangeCommitMessage(
changeNum: ChangeNum,
messageId: ChangeMessageId
@@ -662,4 +677,15 @@
path: string,
contents: string
): Promise<Response>;
+ getRepoTags(
+ filter: string,
+ repo: RepoName,
+ reposTagsPerPage: number,
+ offset?: number,
+ errFn?: ErrorCallback
+ ): Promise<TagInfo[]>;
+
+ setRepoHead(repo: RepoName, ref: GitRef): Promise<Response>;
+ deleteRepoTags(repo: RepoName, ref: GitRef): Promise<Response>;
+ deleteRepoBranches(repo: RepoName, ref: GitRef): Promise<Response>;
}
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index cc74ef1..ea7fb75 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -1039,7 +1039,7 @@
* The CommentInfo entity contains information about an inline comment.
* https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#comment-info
*/
-export interface CommentInfo {
+export interface CommentInfo extends CommentInput {
patch_set?: PatchSetNum;
id: UrlEncodedCommentId;
path?: string;
@@ -1047,9 +1047,9 @@
parent?: number;
line?: number;
range?: CommentRange;
- in_reply_to?: string;
+ in_reply_to?: UrlEncodedCommentId;
message?: string;
- updated: string;
+ updated: Timestamp;
author?: AccountInfo;
tag?: string;
unresolved?: boolean;
@@ -2008,3 +2008,18 @@
title: string;
url: string;
}
+
+/**
+ * The TagInfo entity contains information about a tag.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#tag-info
+ **/
+export interface TagInfo {
+ ref: GitRef;
+ revision: string;
+ object?: string;
+ message?: string;
+ tagger?: GitPersonInfo;
+ created?: string;
+ can_delete: boolean;
+ web_links?: WebLinkInfo[];
+}