Merge changes from topic "gr-change-list-to-ts"

* changes:
  Convert gr-change-list to typescript
  Rename files to preserve history
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/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/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index d240f2c..209b2c9 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -159,10 +159,10 @@
    */
 
   @property({type: Number})
-  changeNum!: number;
+  changeNum?: number;
 
   @property({type: Object, notify: true, observer: '_commentChanged'})
-  comment!: Comment | RobotComment;
+  comment?: Comment | RobotComment;
 
   @property({type: Array})
   comments?: (Comment | RobotComment)[];
@@ -186,7 +186,7 @@
   hasChildren?: boolean;
 
   @property({type: String})
-  patchNum!: PatchSetNum;
+  patchNum?: PatchSetNum;
 
   @property({type: Boolean})
   showActions?: boolean;
@@ -275,18 +275,6 @@
   reporting = appContext.reportingService;
 
   /** @override */
-  ready() {
-    super.ready();
-    if (
-      this.changeNum === undefined ||
-      this.patchNum === undefined ||
-      this.comment === undefined
-    ) {
-      throw new Error('Not all required properties are defined.');
-    }
-  }
-
-  /** @override */
   attached() {
     super.attached();
     this.$.restAPI.getAccount().then(account => {
@@ -460,7 +448,7 @@
           resComment.__draft = true;
           // Maintain the ephemeral draft ID for identification by other
           // elements.
-          if (this.comment.__draftID) {
+          if (this.comment?.__draftID) {
             resComment.__draftID = this.comment.__draftID;
           }
           resComment.__commentSide = this.commentSide;
@@ -482,8 +470,11 @@
     // prior to it being saved.
     this.cancelDebouncer('store');
 
-    if (!this.comment.path || this.comment.line === undefined)
+    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(),
@@ -504,12 +495,13 @@
 
   @observe('comment', 'comments.*')
   _computeHasHumanReply() {
-    if (!this.comment || !this.comments) return;
+    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 === this.comment.id &&
+        c.in_reply_to === comment.id &&
         !(c as RobotComment).robot_id
     );
   }
@@ -582,7 +574,11 @@
     return isAdmin && !draft ? 'showDeleteButtons' : '';
   }
 
-  _computeSaveDisabled(draft: string, comment: Comment, resolved?: boolean) {
+  _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;
@@ -628,15 +624,21 @@
       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)
+        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: this._getPatchNum(),
+          patchNum,
           path: this.comment.path,
           line: this.comment.line,
           range: this.comment.range,
@@ -656,7 +658,7 @@
 
   _handleAnchorClick(e: Event) {
     e.preventDefault();
-    if (!this.comment.line) {
+    if (!this.comment?.line) {
       return;
     }
     this.dispatchEvent(
@@ -673,7 +675,7 @@
 
   _handleEdit(e: Event) {
     e.preventDefault();
-    if (!this.comment.message) throw new Error('message undefined');
+    if (!this.comment?.message) throw new Error('message undefined');
     this._messageText = this.comment.message;
     this.editing = true;
     this.reporting.recordDraftInteraction();
@@ -686,7 +688,7 @@
     if (this.disabled) {
       return;
     }
-    const timingLabel = this.comment.id
+    const timingLabel = this.comment?.id
       ? REPORT_UPDATE_DRAFT
       : REPORT_CREATE_DRAFT;
     const timer = this.reporting.getTimer(timingLabel);
@@ -700,7 +702,7 @@
     e.preventDefault();
 
     if (
-      !this.comment.message ||
+      !this.comment?.message ||
       this.comment.message.trim().length === 0 ||
       !this.comment.id
     ) {
@@ -773,6 +775,7 @@
   }
 
   _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.'));
     }
@@ -871,7 +874,10 @@
     this._handleFailedDraftRequest();
   }
 
-  _saveDraft(draft: Comment) {
+  _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)
@@ -893,6 +899,9 @@
   }
 
   _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)
@@ -907,7 +916,11 @@
   }
 
   _getPatchNum(): PatchSetNum {
-    return this.isOnParent() ? ('PARENT' as PatchSetNum) : this.patchNum;
+    const patchNum = this.isOnParent()
+      ? ('PARENT' as PatchSetNum)
+      : this.patchNum;
+    if (patchNum === undefined) throw new Error('patchNum undefined');
+    return patchNum;
   }
 
   @observe('changeNum', 'patchNum', 'comment')
@@ -932,8 +945,7 @@
       comment.message ||
       comment.__otherEditing ||
       !comment.path ||
-      !comment.line ||
-      !comment.range
+      !comment.line
     ) {
       if (comment) delete comment.__otherEditing;
       return;
@@ -958,6 +970,9 @@
     // 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', {
@@ -1007,6 +1022,13 @@
     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,
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();