Merge "Revert "ReceiveCommits: Retry inserting changes and patch sets""
diff --git a/Documentation/images/user-attention-set-dashboard-empty.png b/Documentation/images/user-attention-set-dashboard-empty.png
new file mode 100644
index 0000000..7b15fa0
--- /dev/null
+++ b/Documentation/images/user-attention-set-dashboard-empty.png
Binary files differ
diff --git a/Documentation/images/user-attention-set-hovercard.png b/Documentation/images/user-attention-set-hovercard.png
index b5638fd..8d6af58 100644
--- a/Documentation/images/user-attention-set-hovercard.png
+++ b/Documentation/images/user-attention-set-hovercard.png
Binary files differ
diff --git a/Documentation/images/user-attention-set-icon-click.png b/Documentation/images/user-attention-set-icon-click.png
new file mode 100644
index 0000000..32b1961
--- /dev/null
+++ b/Documentation/images/user-attention-set-icon-click.png
Binary files differ
diff --git a/Documentation/images/user-attention-set-reply-modify.png b/Documentation/images/user-attention-set-reply-modify.png
index cc12753..7705d14 100644
--- a/Documentation/images/user-attention-set-reply-modify.png
+++ b/Documentation/images/user-attention-set-reply-modify.png
Binary files differ
diff --git a/Documentation/images/user-attention-set-reply-select.png b/Documentation/images/user-attention-set-reply-select.png
index 14b2967..14fadfe3 100644
--- a/Documentation/images/user-attention-set-reply-select.png
+++ b/Documentation/images/user-attention-set-reply-select.png
Binary files differ
diff --git a/Documentation/user-attention-set.txt b/Documentation/user-attention-set.txt
index 11d1bc9..f870405 100644
--- a/Documentation/user-attention-set.txt
+++ b/Documentation/user-attention-set.txt
@@ -7,6 +7,8 @@
 
 Report a bug or send feedback using
 link:https://bugs.chromium.org/p/gerrit/issues/entry?template=Attention+Set[this Monorail template].
+You can also report a bug through the bug icon in the user hovercard and in the
+reply dialog.
 
 [[whose-turn]]
 == Whose turn is it?
@@ -46,20 +48,26 @@
   conversations that the user is replying to.
 * If a *reviewer* replies, then the change owner (and uploader) are added to the
   attention set.
+* For merged and abandoned changes the owner is added when a new human comment
+  is created.
 * Only owner, uploader, reviewers and ccs can be in the attention set.
 
 *!IMPORTANT!* These rules are not meant to be super smart and to always do the
 right thing, e.g. if the change owner sends a reply, then they are often
-expected to individually select whose turn it is instead of adding *all*
-reviewers to the attention set.
+expected to individually select whose turn it is.
 
 Note that just uploading a new patchset is not a relevant event for the
 attention set to change.
 
 === Interaction
 
-There are two ways to interact with the attention set: The hovercard of owner
-and reviewer chips and the "Reply" dialog.
+There are three ways to interact with the attention set: The attention icon,
+the hovercard of owner and reviewer chips and the "Reply" dialog.
+
+*The attention icon* can be used to quickly remove yourself (or someone else)
+from the attention set. Just click the icon, and it will disappear:
+
+image::images/user-attention-set-icon-click.png["attention set icon with tooltip", align="center"]
 
 *The hovercard* (on both the Dashboard and Change page) contains information
 about whether, why and when a user was added to the attention set. It also
@@ -72,8 +80,7 @@
 
 image::images/user-attention-set-reply-modify.png["reply dialog section for modifying", align="center"]
 
-If you do not click "MODIFY", then the backend will just apply the
-automated rules as stated above. If you click "MODIFY", then the section will
+If you click "MODIFY", then the section will
 expand and you can select and de-select users by clicking on their chips.
 Whatever you select here will be the new state of the attention set for this
 change. As a change owner make sure to remove reviewers that you don't expect to
@@ -83,23 +90,24 @@
 
 === Bots
 
-[Caveat: This is not fully implemented yet!]
-
 The attention set is meant for human reviews only. Triggering bots and reacting
 to their results is a different workflow and not in scope of the attenion set.
 Thus members of the "Service Users" group will never be added to the
-attention set. And replies by such users will not add the change owner to the
-attention set.
+attention set. And replies by such users will only add the change owner (and
+uploader) to the attention set, if it comes along with a negative vote.
 
 === Dashboard
 
 The default *dashboard* contains a new section at the top called "Your Turn". It
-lists all changes where the logged-in user is in the attention set. As an active
-developer one of your daily goals will be to iterate over this list and clear
-it.
+lists all changes where the logged-in user is in the attention set.
 
 image::images/user-attention-set-dashboard.png["dashboard with Your Turn section", align="center"]
 
+As an active developer one of your daily goals will be to iterate over this list
+and clear it.
+
+image::images/user-attention-set-dashboard-empty.png["dashboard with empty Your Turn section", align="center"]
+
 Note that you can also navigate to other users' dashboards to check their
 "Your Turn" section.
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index 1d08223..87bc9a0 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -49,6 +49,7 @@
   listChangesOptionsToHex,
 } from '../../../utils/change-util.js';
 import {NotifyType} from '../../../constants/constants.js';
+import {TargetElement, EventType} from '../../plugins/gr-plugin-types.js';
 
 const ERR_BRANCH_EMPTY = 'The destination branch can’t be empty.';
 const ERR_COMMIT_EMPTY = 'The commit message can’t be empty.';
@@ -495,7 +496,7 @@
   /** @override */
   ready() {
     super.ready();
-    this.$.jsAPI.addElement(this.$.jsAPI.Element.CHANGE_ACTIONS, this);
+    this.$.jsAPI.addElement(TargetElement.CHANGE_ACTIONS, this);
     this.$.restAPI.getConfig().then(config => {
       this._config = config;
     });
@@ -555,7 +556,7 @@
 
   _sendShowRevisionActions(detail) {
     this.$.jsAPI.handleEvent(
-        this.$.jsAPI.EventType.SHOW_REVISION_ACTIONS,
+        EventType.SHOW_REVISION_ACTIONS,
         detail
     );
   }
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 1094809..b1b5a9b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -58,7 +58,6 @@
 import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
 import {RevisionInfo} from '../../shared/revision-info/revision-info.js';
-
 import {PrimaryTab, SecondaryTab} from '../../../constants/constants.js';
 import {NO_ROBOT_COMMENTS_THREADS_MSG} from '../../../constants/messages.js';
 import {appContext} from '../../../services/app-context.js';
@@ -73,6 +72,7 @@
   SPECIAL_PATCH_SET_NUM,
 } from '../../../utils/patch-set-util.js';
 import {changeStatuses, changeStatusString} from '../../../utils/change-util.js';
+import {EventType} from '../../plugins/gr-plugin-types.js';
 
 const CHANGE_ID_ERROR = {
   MISMATCH: 'mismatch',
@@ -1098,7 +1098,7 @@
   }
 
   _sendShowChangeEvent() {
-    this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, {
+    this.$.jsAPI.handleEvent(EventType.SHOW_CHANGE, {
       change: this._change,
       patchNum: this._patchRange.patchNum,
       info: {mergeable: this._mergeable},
@@ -1569,7 +1569,7 @@
       this._handleLabelRemoved(changeRecord.value.indexSplices,
           changeRecord.path);
     }
-    this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.LABEL_CHANGE, {
+    this.$.jsAPI.handleEvent(EventType.LABEL_CHANGE, {
       change: this._change,
     });
   }
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 a66c6a2..3975be7 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
@@ -27,6 +27,7 @@
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
 import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
+import {EventType} from '../../plugins/gr-plugin-types.js';
 
 import 'lodash/lodash.js';
 import {generateChange, TestKeyboardShortcutBinder} from '../../../test/test-utils.js';
@@ -2254,8 +2255,7 @@
     const showStub = sinon.stub(element.$.jsAPI, 'handleEvent');
     element._sendShowChangeEvent();
     assert.isTrue(showStub.calledOnce);
-    assert.equal(
-        showStub.lastCall.args[0], element.$.jsAPI.EventType.SHOW_CHANGE);
+    assert.equal(showStub.lastCall.args[0], EventType.SHOW_CHANGE);
     assert.deepEqual(showStub.lastCall.args[1], {
       change: {labels: {}},
       patchNum: 4,
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 4ab66d8..c1ab028 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -41,6 +41,7 @@
 import {removeServiceUsers} from '../../../utils/account-util.js';
 import {getDisplayName} from '../../../utils/display-name-util.js';
 import {IronA11yAnnouncer} from '@polymer/iron-a11y-announcer/iron-a11y-announcer.js';
+import {TargetElement} from '../../plugins/gr-plugin-types.js';
 
 const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
 
@@ -361,7 +362,7 @@
     super.ready();
     this._isPatchsetCommentsExperimentEnabled = this.flagsService
         .isEnabled(KnownExperimentId.PATCHSET_COMMENTS);
-    this.$.jsAPI.addElement(this.$.jsAPI.Element.REPLY_DIALOG, this);
+    this.$.jsAPI.addElement(TargetElement.REPLY_DIALOG, this);
   }
 
   open(opt_focusTarget) {
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.js b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.js
index 6d8be66..f997e3a 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.js
@@ -19,6 +19,7 @@
 import './gr-edit-controls.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {createIronOverlayBackdropStyleEl} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-edit-controls');
 
@@ -31,14 +32,7 @@
   let ironOverlayBackdropStyleEl;
 
   setup(() => {
-    // Forcing an opacity of 0 onto the ironOverlayBackdrop is required, because
-    // otherwise the backdrop stays around in the DOM for too long waiting for
-    // an animation to finish.
-    ironOverlayBackdropStyleEl = document.createElement('style');
-    document.head.appendChild(ironOverlayBackdropStyleEl);
-    ironOverlayBackdropStyleEl.sheet.insertRule(
-        'body { --iron-overlay-backdrop-opacity: 0; }');
-
+    ironOverlayBackdropStyleEl = createIronOverlayBackdropStyleEl();
     element = basicFixture.instantiate();
     element.change = {_number: '42'};
     showDialogSpy = sinon.spy(element, '_showDialog');
diff --git a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.ts b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.ts
index 51ff0ab..1332118 100644
--- a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.ts
@@ -15,6 +15,8 @@
  * limitations under the License.
  */
 
+import {PluginApi} from '../gr-plugin-types';
+
 /** Interface for menu link */
 export interface MenuLink {
   text: string;
@@ -22,11 +24,6 @@
   capability: string | null;
 }
 
-// TODO(TS): replace with Plugin once gr-public-js-api migrated
-interface PluginApi {
-  on(eventName: string, adminApi: GrAdminApi): void;
-}
-
 /**
  * GrAdminApi class.
  *
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.ts b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.ts
index 2db11105..8eeb184 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.ts
@@ -18,6 +18,7 @@
 export class GrAttributeHelper {
   private readonly _promises = new Map<string, Promise<any>>();
 
+  // TOOD(TS): Change any to something more like HTMLElement.
   constructor(public element: any) {}
 
   _getChangedEventName(name: string): string {
diff --git a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.ts b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.ts
index 8880f06..d3452dc 100644
--- a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.ts
@@ -1,5 +1,3 @@
-import {GrAttributeHelper} from '../gr-attribute-helper/gr-attribute-helper';
-
 /**
  * @license
  * Copyright (C) 2018 The Android Open Source Project
@@ -16,25 +14,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {HookApi, PluginApi} from '../gr-plugin-types';
 
-type HookCallback = (el: Element) => void;
-interface HookApi {
-  onAttached(callback: HookCallback): void;
-}
-interface PluginAPI {
-  hook(hookname: string): HookApi;
-  attributeHelper(element: Element): GrAttributeHelper;
-}
-
-/** @constructor */
 export class GrChangeMetadataApi {
-  // TODO(TS): Convert to GrDomHook once converted
   private _hook: HookApi | null;
 
-  // TODO(TS): Convert type to GrPlugin.
-  public plugin: PluginAPI;
+  public plugin: PluginApi;
 
-  constructor(plugin: PluginAPI) {
+  constructor(plugin: PluginApi) {
     this.plugin = plugin;
     this._hook = null;
   }
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.ts b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.ts
index daf7d67..dd76be4 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.ts
@@ -15,25 +15,14 @@
  * limitations under the License.
  */
 import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {HookApi, HookCallback, PluginApi} from '../gr-plugin-types';
 
-type HookCallback = (el: Element) => void;
-interface HookApi {
-  onAttached(callback: HookCallback): void;
-}
-interface PluginAPI {
-  hook(hookname: string): HookApi;
-  getPluginName(): string;
-}
-
-/** @constructor */
 export class GrDomHooksManager {
   private _hooks: Record<string, GrDomHook>;
 
-  // TODO(TS): Convert type to GrPlugin.
-  private _plugin: PluginAPI;
+  private _plugin: PluginApi;
 
-  // TODO(TS): Convert type to GrPlugin.
-  constructor(plugin: PluginAPI) {
+  constructor(plugin: PluginApi) {
     this._plugin = plugin;
     this._hooks = {};
   }
@@ -61,18 +50,8 @@
   }
 }
 
-interface PublicApi {
-  onAttached(callback: HookCallback): PublicApi;
-  onDetached(callback: HookCallback): PublicApi;
-  getAllAttached(): any;
-  getLastAttached(): any;
-  getModuleName(): string;
-}
-
-/** @constructor */
-export class GrDomHook {
-  // TODO(TS): specify type for this
-  private _instances: unknown[] = [];
+export class GrDomHook implements HookApi {
+  private _instances: HTMLElement[] = [];
 
   private _attachCallbacks: HookCallback[] = [];
 
@@ -80,7 +59,7 @@
 
   private _moduleName: string;
 
-  private _lastAttachedPromise: Promise<HookCallback> | null = null;
+  private _lastAttachedPromise: Promise<HTMLElement> | null = null;
 
   constructor(hookName: string, moduleName?: string) {
     if (moduleName) {
@@ -108,7 +87,7 @@
     customElements.define(HookPlaceholder.is, HookPlaceholder);
   }
 
-  handleInstanceDetached(instance: Element) {
+  handleInstanceDetached(instance: HTMLElement) {
     const index = this._instances.indexOf(instance);
     if (index !== -1) {
       this._instances.splice(index, 1);
@@ -116,7 +95,7 @@
     this._detachCallbacks.forEach(callback => callback(instance));
   }
 
-  handleInstanceAttached(instance: Element) {
+  handleInstanceAttached(instance: HTMLElement) {
     this._instances.push(instance);
     this._attachCallbacks.forEach(callback => callback(instance));
   }
@@ -124,27 +103,25 @@
   /**
    * Get instance of last DOM hook element attached into the endpoint.
    * Returns a Promise, that's resolved when attachment is done.
-   *
-   * @return
    */
-  getLastAttached() {
+  getLastAttached(): Promise<HTMLElement> {
     if (this._instances.length) {
       return Promise.resolve(this._instances.slice(-1)[0]);
     }
     if (!this._lastAttachedPromise) {
       let resolve: HookCallback;
-      const promise = new Promise(r => {
+      const promise = new Promise<HTMLElement>(r => {
         resolve = r;
         this._attachCallbacks.push(resolve);
       });
-      this._lastAttachedPromise = promise.then(element => {
+      this._lastAttachedPromise = promise.then((element: HTMLElement) => {
         this._lastAttachedPromise = null;
         const index = this._attachCallbacks.indexOf(resolve);
         if (index !== -1) {
           this._attachCallbacks.splice(index, 1);
         }
         return element;
-      }) as Promise<HookCallback>;
+      });
     }
     return this._lastAttachedPromise;
   }
@@ -181,14 +158,4 @@
   getModuleName() {
     return this._moduleName;
   }
-
-  getPublicAPI(): PublicApi {
-    return {
-      onAttached: this.onAttached.bind(this),
-      onDetached: this.onDetached.bind(this),
-      getAllAttached: this.getAllAttached.bind(this),
-      getLastAttached: this.getLastAttached.bind(this),
-      getModuleName: this.getModuleName.bind(this),
-    };
-  }
 }
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.js b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.js
index c101396..49223b9 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.js
@@ -23,18 +23,8 @@
 const pluginApi = _testOnly_initGerritPluginApi();
 
 suite('gr-dom-hooks tests', () => {
-  const PUBLIC_METHODS =[
-    'onAttached',
-    'onDetached',
-    'getLastAttached',
-    'getAllAttached',
-    'getModuleName',
-  ];
-
   let instance;
-
   let hook;
-  let hookInternal;
 
   setup(() => {
     let plugin;
@@ -46,16 +36,11 @@
   suite('placeholder', () => {
     setup(()=>{
       sinon.stub(GrDomHook.prototype, '_createPlaceholder');
-      hookInternal = instance.getDomHook('foo-bar');
-      hook = hookInternal.getPublicAPI();
-    });
-
-    test('public hook API has only public methods', () => {
-      assert.deepEqual(Object.keys(hook).sort(), PUBLIC_METHODS.sort());
+      hook = instance.getDomHook('foo-bar');
     });
 
     test('registers placeholder class', () => {
-      assert.isTrue(hookInternal._createPlaceholder.calledWithExactly(
+      assert.isTrue(hook._createPlaceholder.calledWithExactly(
           'testplugin-autogenerated-foo-bar'));
     });
 
@@ -68,12 +53,7 @@
 
   suite('custom element', () => {
     setup(() => {
-      hookInternal = instance.getDomHook('foo-bar', 'my-el');
-      hook = hookInternal.getPublicAPI();
-    });
-
-    test('public hook API has only public methods', () => {
-      assert.deepEqual(Object.keys(hook).sort(), PUBLIC_METHODS.sort());
+      hook = instance.getDomHook('foo-bar', 'my-el');
     });
 
     test('getModuleName()', () => {
@@ -89,8 +69,8 @@
         document.createElement(hook.getModuleName()),
         document.createElement(hook.getModuleName()),
       ];
-      hookInternal.handleInstanceAttached(el1);
-      hookInternal.handleInstanceAttached(el2);
+      hook.handleInstanceAttached(el1);
+      hook.handleInstanceAttached(el2);
       assert.isTrue(onAttachedSpy.firstCall.calledWithExactly(el1));
       assert.isTrue(onAttachedSpy.secondCall.calledWithExactly(el2));
     });
@@ -102,9 +82,9 @@
         document.createElement(hook.getModuleName()),
         document.createElement(hook.getModuleName()),
       ];
-      hookInternal.handleInstanceDetached(el1);
+      hook.handleInstanceDetached(el1);
       assert.isTrue(onDetachedSpy.firstCall.calledWithExactly(el1));
-      hookInternal.handleInstanceDetached(el2);
+      hook.handleInstanceDetached(el2);
       assert.isTrue(onDetachedSpy.secondCall.calledWithExactly(el2));
     });
 
@@ -115,10 +95,10 @@
       ];
       el1.textContent = 'one';
       el2.textContent = 'two';
-      hookInternal.handleInstanceAttached(el1);
-      hookInternal.handleInstanceAttached(el2);
+      hook.handleInstanceAttached(el1);
+      hook.handleInstanceAttached(el2);
       assert.deepEqual([el1, el2], hook.getAllAttached());
-      hookInternal.handleInstanceDetached(el1);
+      hook.handleInstanceDetached(el1);
       assert.deepEqual([el2], hook.getAllAttached());
     });
 
@@ -131,8 +111,8 @@
       ];
       el1.textContent = 'one';
       el2.textContent = 'two';
-      hookInternal.handleInstanceAttached(el1);
-      hookInternal.handleInstanceAttached(el2);
+      hook.handleInstanceAttached(el1);
+      hook.handleInstanceAttached(el2);
       const afterAttachedPromise = hook.getLastAttached().then(
           el => assert.strictEqual(el2, el));
       return Promise.all([
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-types.ts b/polygerrit-ui/app/elements/plugins/gr-plugin-types.ts
new file mode 100644
index 0000000..7ac670b
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-types.ts
@@ -0,0 +1,111 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {GrAttributeHelper} from './gr-attribute-helper/gr-attribute-helper';
+import {GrPluginRestApi} from '../shared/gr-js-api-interface/gr-plugin-rest-api';
+import {GrEventHelper} from './gr-event-helper/gr-event-helper';
+import {GrPopupInterface} from './gr-popup-interface/gr-popup-interface';
+import {GrPluginActionContext} from '../shared/gr-js-api-interface/gr-plugin-action-context';
+import {ConfigInfo} from '../../types/common';
+
+interface GerritElementExtensions {
+  content?: HTMLElement & {hidden?: boolean};
+  change?: unknown;
+  revision?: unknown;
+  token?: string;
+  repoName?: string;
+  config?: ConfigInfo;
+}
+export type HookCallback = (el: HTMLElement & GerritElementExtensions) => void;
+
+export interface HookApi {
+  onAttached(callback: HookCallback): HookApi;
+  onDetached(callback: HookCallback): HookApi;
+  getAllAttached(): HTMLElement[];
+  getLastAttached(): Promise<HTMLElement>;
+  getModuleName(): string;
+}
+
+export enum TargetElement {
+  CHANGE_ACTIONS = 'changeactions',
+  REPLY_DIALOG = 'replydialog',
+}
+
+// Note: for new events, naming convention should be: `a-b`
+export enum EventType {
+  HISTORY = 'history',
+  LABEL_CHANGE = 'labelchange',
+  SHOW_CHANGE = 'showchange',
+  SUBMIT_CHANGE = 'submitchange',
+  SHOW_REVISION_ACTIONS = 'show-revision-actions',
+  COMMIT_MSG_EDIT = 'commitmsgedit',
+  COMMENT = 'comment',
+  REVERT = 'revert',
+  REVERT_SUBMISSION = 'revert_submission',
+  POST_REVERT = 'postrevert',
+  ANNOTATE_DIFF = 'annotatediff',
+  ADMIN_MENU_LINKS = 'admin-menu-links',
+  HIGHLIGHTJS_LOADED = 'highlightjs-loaded',
+}
+
+export interface RegisterOptions {
+  slot?: string;
+  replace: unknown;
+}
+
+export interface PanelInfo {
+  body: Element;
+  p: {[key: string]: any};
+  onUnload: () => void;
+}
+
+export interface SettingsInfo {
+  body: Element;
+  token?: string;
+  onUnload: () => void;
+  setTitle: () => void;
+  setWindowTitle: () => void;
+  show: () => void;
+}
+
+export interface PluginApi {
+  _url?: URL;
+  deprecated: PluginDeprecatedApi;
+  hook(endpointName: string, opt_options?: RegisterOptions): HookApi;
+  getPluginName(): string;
+  on(eventName: string, target: any): void;
+  attributeHelper(element: Element): GrAttributeHelper;
+  restApi(): GrPluginRestApi;
+  eventHelper(element: Node): GrEventHelper;
+}
+
+export interface PluginDeprecatedApi {
+  _loadedGwt(): void;
+  install: () => void;
+  popup(element: Node): GrPopupInterface;
+  onAction(
+    type: string,
+    action: string,
+    callback: (ctx: GrPluginActionContext) => void
+  ): void;
+  panel(extensionpoint: string, callback: (panel: PanelInfo) => void): void;
+  screen(pattern: string, callback: (settings: SettingsInfo) => void): void;
+  settingsScreen(
+    path: string,
+    menu: string,
+    callback: (settings: SettingsInfo) => void
+  ): void;
+}
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.ts b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.ts
index faa5d1e..d45c263 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.ts
@@ -17,17 +17,10 @@
 import './gr-plugin-popup';
 import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom';
 import {GrPluginPopup} from './gr-plugin-popup';
-
-// TODO(TS): replace with Plugin API once its migrated to ts
-interface HookApi {
-  getLastAttached(): Promise<Node>;
-}
-interface PluginAPI {
-  hook(hookname: string): HookApi;
-}
+import {PluginApi} from '../gr-plugin-types';
 
 interface CustomPolymerPluginEl extends HTMLElement {
-  plugin: PluginAPI;
+  plugin: PluginApi;
 }
 
 /**
@@ -42,14 +35,14 @@
   private _popup: GrPluginPopup | null = null;
 
   constructor(
-    readonly plugin: PluginAPI,
+    readonly plugin: PluginApi,
     private _moduleName: string | null = null
   ) {}
 
   _getElement() {
     // TODO(TS): maybe consider removing this if no one is using
     // anything other than native methods on the return
-    return dom(this._popup);
+    return (dom(this._popup) as unknown) as HTMLElement;
   }
 
   /**
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.js
index e59d35c..75bd306 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.js
@@ -21,6 +21,7 @@
 import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {createIronOverlayBackdropStyleEl} from '../../../test/test-utils';
 
 class GrUserTestPopupElement extends PolymerElement {
   static get is() { return 'gr-user-test-popup'; }
@@ -39,8 +40,10 @@
   let container;
   let instance;
   let plugin;
+  let ironOverlayBackdropStyleEl;
 
   setup(() => {
+    ironOverlayBackdropStyleEl = createIronOverlayBackdropStyleEl();
     pluginApi.install(p => { plugin = p; }, '0.1',
         'http://test.com/plugins/testplugin/static/test.js');
     container = containerFixture.instantiate();
@@ -51,6 +54,10 @@
     });
   });
 
+  teardown(() => {
+    ironOverlayBackdropStyleEl.remove();
+  });
+
   suite('manual', () => {
     setup(() => {
       instance = new GrPopupInterface(plugin);
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.ts b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.ts
index 294205d..701a560 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.ts
@@ -16,21 +16,9 @@
  */
 import './gr-plugin-repo-command';
 import {ConfigInfo} from '../../../types/common';
+import {HookApi, PluginApi} from '../gr-plugin-types';
 
-// TODO(TS): replace with Plugin and proper hook once gr-public-js-api migrated
-interface PluginApi {
-  hook(endpointName: string, option?: {replace?: boolean}): HookApi;
-  eventHelper(
-    el: Element
-  ): {
-    on(name: string, callback: EventListener): void;
-  };
-}
-interface HookApi {
-  onAttached<T extends Element>(callback: HookCallback<T>): this;
-}
-type HookCallback<T extends Element> = (el: T) => void;
-type RepoCommandCallback = (repo: string, config: ConfigInfo | null) => boolean;
+type RepoCommandCallback = (repo?: string, config?: ConfigInfo) => boolean;
 
 /**
  * Parameters provided on repo-command endpoint
@@ -60,7 +48,7 @@
       return this._hook;
     }
     this._hook = this._createHook(title);
-    this._hook.onAttached((element: GrRepoCommandEndpointEl) => {
+    this._hook.onAttached(element => {
       if (callback(element.repoName, element.config) === false) {
         element.hidden = true;
       }
@@ -68,7 +56,7 @@
     return this;
   }
 
-  onTap(callback: EventListener) {
+  onTap(callback: (event: Event) => boolean) {
     if (!this._hook) {
       console.warn('Call createCommand first.');
       return this;
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.ts b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.ts
index a8bed8d..c7f1ecd 100644
--- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.ts
@@ -16,18 +16,7 @@
  */
 import '../../settings/gr-settings-view/gr-settings-item';
 import '../../settings/gr-settings-view/gr-settings-menu-item';
-
-// TODO(TS): replace with Plugin once gr-public-js-api migrated
-interface PluginApi {
-  getPluginName(): string;
-  hook(endpointName: string, option?: {replace?: boolean}): HookApi;
-}
-
-interface HookApi {
-  onAttached(callback: HookCallback): this;
-}
-
-type HookCallback = (el: Node) => void;
+import {PluginApi} from '../gr-plugin-types';
 
 export class GrSettingsApi {
   private _token: string;
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.ts b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.ts
index 613bde2..821e4bf 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.ts
@@ -16,16 +16,7 @@
  */
 import './gr-custom-plugin-header';
 import {GrCustomPluginHeader} from './gr-custom-plugin-header';
-
-// TODO(TS): replace with Plugin once gr-public-js-api migrated
-interface PluginApi {
-  hook(
-    endpointName: string,
-    option: {replace?: boolean}
-  ): {
-    onAttached(callback: (el: Element) => void): void;
-  };
-}
+import {PluginApi} from '../gr-plugin-types';
 
 /**
  * Defines api for theme, can be used to set header logo and title.
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts
index d75781f..80e09d4 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts
@@ -19,20 +19,7 @@
 import {CoverageRange} from '../../../types/types';
 import {Side} from '../../../constants/constants';
 import {PatchSetNum} from '../../../types/common';
-
-type HookCallback = (el: {content: Element & {hidden?: boolean}}) => void;
-
-// TODO(TS): remove once Plugin api converted to ts
-interface HookApi {
-  onAttached(callback: HookCallback): void;
-  onDetached(callback: HookCallback): void;
-}
-
-// TODO(TS): remove once Plugin api converted to ts
-interface PluginApi {
-  hook(hookName: string): HookApi;
-  on(eventName: string, callback: unknown): void;
-}
+import {PluginApi} from '../../plugins/gr-plugin-types';
 
 type AddLayerFunc = (ctx: GrAnnotationActionsContext) => void;
 
@@ -49,7 +36,7 @@
   side: Side
 ) => void;
 
-type CoverageProvider = (
+export type CoverageProvider = (
   changeNum: number,
   path: string,
   basePatchNum: number,
@@ -147,6 +134,10 @@
     onAttached: (checkboxEl: Element | null) => void
   ) {
     this.plugin.hook('annotation-toggler').onAttached(element => {
+      if (!element.content) {
+        console.error('plugin endpoint without content.');
+        return;
+      }
       if (!element.content.hidden) {
         console.error(
           element.content.id + ' is already enabled. Cannot re-enable.'
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts
index e61a6b5..3a86700 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts
@@ -15,12 +15,12 @@
  * limitations under the License.
  */
 import {
-  ApiElement,
   GrChangeActions,
   ActionType,
   ActionPriority,
-  JsApiService,
 } from '../../../services/services/gr-rest-api/gr-rest-api';
+import {JsApiService} from './gr-js-api-types';
+import {TargetElement} from '../../plugins/gr-plugin-types';
 
 interface Plugin {
   getPluginName(): string;
@@ -63,7 +63,11 @@
       const sharedApiElement = (document.createElement(
         'gr-js-api-interface'
       ) as unknown) as JsApiService;
-      this.setEl(sharedApiElement.getElement(ApiElement.CHANGE_ACTIONS));
+      this.setEl(
+        (sharedApiElement.getElement(
+          TargetElement.CHANGE_ACTIONS
+        ) as unknown) as GrChangeActions
+      );
     }
     return this._el!;
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.ts
index 7a40c68..7069304 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.ts
@@ -15,11 +15,9 @@
  * limitations under the License.
  */
 
-import {
-  ApiElement,
-  GrReplyDialog,
-  JsApiService,
-} from '../../../services/services/gr-rest-api/gr-rest-api';
+import {GrReplyDialog} from '../../../services/services/gr-rest-api/gr-rest-api';
+import {PluginApi, TargetElement} from '../../plugins/gr-plugin-types';
+import {JsApiService} from './gr-js-api-types';
 
 // TODO(TS): maybe move interfaces\types to other files when convertion complete
 interface LabelsChangedDetail {
@@ -30,56 +28,9 @@
   value: string;
 }
 
-interface GerritHtmlElementEventMap {
-  'value-changed': CustomEvent<ValueChangedDetail>;
-  'labels-changed': CustomEvent<LabelsChangedDetail>;
-}
-
-interface GerritHtmlElement extends EventTarget {
-  addEventListener<K extends keyof GerritHtmlElementEventMap>(
-    type: K,
-    listener: (
-      this: GerritHtmlElement,
-      ev: GerritHtmlElementEventMap[K]
-    ) => void,
-    options?: boolean | AddEventListenerOptions
-  ): void;
-  addEventListener(
-    type: string,
-    listener: EventListenerOrEventListenerObject,
-    options?: boolean | AddEventListenerOptions
-  ): void;
-
-  removeEventListener<K extends keyof GerritHtmlElementEventMap>(
-    type: K,
-    listener: (
-      this: GerritHtmlElement,
-      ev: GerritHtmlElementEventMap[K]
-    ) => void,
-    options?: boolean | EventListenerOptions
-  ): void;
-  removeEventListener(
-    type: string,
-    listener: EventListenerOrEventListenerObject,
-    options?: boolean | EventListenerOptions
-  ): void;
-}
-
-type HookCallback = (el: {content: GerritHtmlElement}) => void;
 type ReplyChangedCallback = (text: string) => void;
 type LabelsChangedCallback = (detail: LabelsChangedDetail) => void;
 
-// TODO(TS): remove once Plugin api converted to ts
-interface HookApi {
-  onAttached(callback: HookCallback): void;
-  onDetached(callback: HookCallback): void;
-}
-
-// TODO(TS): remove once Plugin api converted to ts
-interface PluginApi {
-  hook(hookName: string): HookApi;
-}
-
 /**
  * GrChangeReplyInterface, provides a set of handy methods on reply dialog.
  */
@@ -90,7 +41,9 @@
   ) {}
 
   get _el(): GrReplyDialog {
-    return this.sharedApiElement.getElement(ApiElement.REPLY_DIALOG);
+    return (this.sharedApiElement.getElement(
+      TargetElement.REPLY_DIALOG
+    ) as unknown) as GrReplyDialog;
   }
 
   getLabelValue(label: string) {
@@ -107,8 +60,10 @@
 
   addReplyTextChangedCallback(handler: ReplyChangedCallback) {
     const hookApi = this.plugin.hook('reply-text');
-    const registeredHandler = (e: CustomEvent<ValueChangedDetail>) =>
-      handler(e.detail.value);
+    const registeredHandler = (e: Event) => {
+      const ce = e as CustomEvent<ValueChangedDetail>;
+      handler(ce.detail.value);
+    };
     hookApi.onAttached(el => {
       if (!el.content) {
         return;
@@ -125,8 +80,10 @@
 
   addLabelValuesChangedCallback(handler: LabelsChangedCallback) {
     const hookApi = this.plugin.hook('reply-label-scores');
-    const registeredHandler = (e: CustomEvent<LabelsChangedDetail>) =>
-      handler(e.detail);
+    const registeredHandler = (e: Event) => {
+      const ce = e as CustomEvent<LabelsChangedDetail>;
+      handler(ce.detail);
+    };
     hookApi.onAttached(el => {
       if (!el.content) {
         return;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.js
deleted file mode 100644
index ce65755..0000000
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.js
+++ /dev/null
@@ -1,188 +0,0 @@
-/**
- * @license
- * Copyright (C) 2019 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.
- */
-
-/**
- * This defines the Gerrit instance. All methods directly attached to Gerrit
- * should be defined or linked here.
- */
-
-import {pluginLoader} from './gr-plugin-loader.js';
-import {getRestAPI, send} from './gr-api-utils.js';
-import {appContext} from '../../../services/app-context.js';
-
-/**
- * Trigger the preinstalls for bundled plugins.
- * This needs to happen before Gerrit as plugin bundle overrides the Gerrit.
- */
-function flushPreinstalls() {
-  if (window.Gerrit.flushPreinstalls) {
-    window.Gerrit.flushPreinstalls();
-  }
-}
-export const _testOnly_flushPreinstalls = flushPreinstalls;
-
-export function initGerritPluginApi() {
-  window.Gerrit = window.Gerrit || {};
-  flushPreinstalls();
-  initGerritPluginsMethods(window.Gerrit);
-  // Preloaded plugins should be installed after Gerrit.install() is set,
-  // since plugin preloader substitutes Gerrit.install() temporarily.
-  // (Gerrit.install() is set in initGerritPluginsMethods)
-  pluginLoader.installPreloadedPlugins();
-}
-
-export function _testOnly_initGerritPluginApi() {
-  initGerritPluginApi();
-  return window.Gerrit;
-}
-
-export function deprecatedDelete(url, opt_callback) {
-  console.warn('.delete() is deprecated! Use plugin.restApi().delete()');
-  return getRestAPI().send('DELETE', url)
-      .then(response => {
-        if (response.status !== 204) {
-          return response.text().then(text => {
-            if (text) {
-              return Promise.reject(new Error(text));
-            } else {
-              return Promise.reject(new Error(response.status));
-            }
-          });
-        }
-        if (opt_callback) {
-          opt_callback(response);
-        }
-        return response;
-      });
-}
-
-function initGerritPluginsMethods(globalGerritObj) {
-  /**
-   * @deprecated Use plugin.styles().css(rulesStr) instead. Please, consult
-   * the documentation how to replace it accordingly.
-   */
-  globalGerritObj.css = function(rulesStr) {
-    console.warn('Gerrit.css(rulesStr) is deprecated!',
-        'Use plugin.styles().css(rulesStr)');
-    if (!globalGerritObj._customStyleSheet) {
-      const styleEl = document.createElement('style');
-      document.head.appendChild(styleEl);
-      globalGerritObj._customStyleSheet = styleEl.sheet;
-    }
-
-    const name = '__pg_js_api_class_' +
-        globalGerritObj._customStyleSheet.cssRules.length;
-    globalGerritObj._customStyleSheet
-        .insertRule('.' + name + '{' + rulesStr + '}', 0);
-    return name;
-  };
-
-  globalGerritObj.install = function(callback, opt_version, opt_src) {
-    pluginLoader.install(callback, opt_version, opt_src);
-  };
-
-  globalGerritObj.getLoggedIn = function() {
-    console.warn('Gerrit.getLoggedIn() is deprecated! ' +
-        'Use plugin.restApi().getLoggedIn()');
-    return document.createElement('gr-rest-api-interface').getLoggedIn();
-  };
-
-  globalGerritObj.get = function(url, callback) {
-    console.warn('.get() is deprecated! Use plugin.restApi().get()');
-    send('GET', url, callback);
-  };
-
-  globalGerritObj.post = function(url, payload, callback) {
-    console.warn('.post() is deprecated! Use plugin.restApi().post()');
-    send('POST', url, callback, payload);
-  };
-
-  globalGerritObj.put = function(url, payload, callback) {
-    console.warn('.put() is deprecated! Use plugin.restApi().put()');
-    send('PUT', url, callback, payload);
-  };
-
-  globalGerritObj.delete = function(url, opt_callback) {
-    deprecatedDelete(url, opt_callback);
-  };
-
-  globalGerritObj.awaitPluginsLoaded = function() {
-    return pluginLoader.awaitPluginsLoaded();
-  };
-
-  // TODO(taoalpha): consider removing these proxy methods
-  // and using pluginLoader directly
-  globalGerritObj._loadPlugins = function(plugins, opt_option) {
-    pluginLoader.loadPlugins(plugins, opt_option);
-  };
-
-  globalGerritObj._arePluginsLoaded = function() {
-    return pluginLoader.arePluginsLoaded();
-  };
-
-  globalGerritObj._isPluginPreloaded = function(url) {
-    return pluginLoader.isPluginPreloaded(url);
-  };
-
-  globalGerritObj._isPluginEnabled = function(pathOrUrl) {
-    return pluginLoader.isPluginEnabled(pathOrUrl);
-  };
-
-  globalGerritObj._isPluginLoaded = function(pathOrUrl) {
-    return pluginLoader.isPluginLoaded(pathOrUrl);
-  };
-
-  const eventEmitter = appContext.eventEmitter;
-
-  // TODO(taoalpha): List all internal supported event names.
-  // Also convert this to inherited class once we move Gerrit to class.
-  globalGerritObj._eventEmitter = eventEmitter;
-  ['addListener',
-    'dispatch',
-    'emit',
-    'off',
-    'on',
-    'once',
-    'removeAllListeners',
-    'removeListener',
-  ].forEach(method => {
-    /**
-     * Enabling EventEmitter interface on Gerrit.
-     *
-     * This will enable to signal across different parts of js code without relying on DOM,
-     * including core to core, plugin to plugin and also core to plugin.
-     *
-     * @example
-     *
-     * // Emit this event from pluginA
-     * Gerrit.install(pluginA => {
-     *   fetch("some-api").then(() => {
-     *     Gerrit.on("your-special-event", {plugin: pluginA});
-     *   });
-     * });
-     *
-     * // Listen on your-special-event from pluignB
-     * Gerrit.install(pluginB => {
-     *   Gerrit.on("your-special-event", ({plugin}) => {
-     *     // do something, plugin is pluginA
-     *   });
-     * });
-     */
-    globalGerritObj[method] = eventEmitter[method]
-        .bind(eventEmitter);
-  });
-}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
new file mode 100644
index 0000000..e5e6069
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
@@ -0,0 +1,252 @@
+/**
+ * @license
+ * Copyright (C) 2019 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.
+ */
+
+/**
+ * This defines the Gerrit instance. All methods directly attached to Gerrit
+ * should be defined or linked here.
+ */
+import {pluginLoader, PluginOptionMap} from './gr-plugin-loader';
+import {getRestAPI, send} from './gr-api-utils';
+import {appContext} from '../../../services/app-context';
+import {PluginApi} from '../../plugins/gr-plugin-types';
+import {HttpMethod} from '../../../constants/constants';
+import {RequestPayload} from '../../../types/common';
+import {
+  EventCallback,
+  EventEmitterService,
+} from '../../../services/gr-event-interface/gr-event-interface';
+
+interface GerritGlobal extends EventEmitterService {
+  flushPreinstalls?(): void;
+  css(rule: string): string;
+  install(
+    callback: (plugin: PluginApi) => void,
+    opt_version?: string,
+    src?: string
+  ): void;
+  getLoggedIn(): Promise<boolean>;
+  get(url: string, callback?: (response: unknown) => void): void;
+  post(
+    url: string,
+    payload?: RequestPayload,
+    callback?: (response: unknown) => void
+  ): void;
+  put(
+    url: string,
+    payload?: RequestPayload,
+    callback?: (response: unknown) => void
+  ): void;
+  delete(url: string, callback?: (response: unknown) => void): void;
+  isPluginLoaded(pathOrUrl: string): boolean;
+  awaitPluginsLoaded(): Promise<unknown>;
+  _loadPlugins(plugins: string[], opts: PluginOptionMap): void;
+  _arePluginsLoaded(): boolean;
+  _isPluginPreloaded(pathOrUrl: string): boolean;
+  _isPluginEnabled(pathOrUrl: string): boolean;
+  _isPluginLoaded(pathOrUrl: string): boolean;
+  _eventEmitter: EventEmitterService;
+  _customStyleSheet: CSSStyleSheet;
+}
+
+/**
+ * Trigger the preinstalls for bundled plugins.
+ * This needs to happen before Gerrit as plugin bundle overrides the Gerrit.
+ */
+function flushPreinstalls() {
+  const Gerrit = window.Gerrit as GerritGlobal;
+  if (Gerrit.flushPreinstalls) {
+    Gerrit.flushPreinstalls();
+  }
+}
+export const _testOnly_flushPreinstalls = flushPreinstalls;
+
+export function initGerritPluginApi() {
+  window.Gerrit = {};
+  flushPreinstalls();
+  initGerritPluginsMethods(window.Gerrit as GerritGlobal);
+  // Preloaded plugins should be installed after Gerrit.install() is set,
+  // since plugin preloader substitutes Gerrit.install() temporarily.
+  // (Gerrit.install() is set in initGerritPluginsMethods)
+  pluginLoader.installPreloadedPlugins();
+}
+
+export function _testOnly_initGerritPluginApi(): GerritGlobal {
+  initGerritPluginApi();
+  return window.Gerrit as GerritGlobal;
+}
+
+export function deprecatedDelete(
+  url: string,
+  callback?: (response: Response) => void
+) {
+  console.warn('.delete() is deprecated! Use plugin.restApi().delete()');
+  return getRestAPI()
+    .send(HttpMethod.DELETE, url)
+    .then(response => {
+      if (response.status !== 204) {
+        return response.text().then(text => {
+          if (text) {
+            return Promise.reject(new Error(text));
+          } else {
+            return Promise.reject(new Error(`${response.status}`));
+          }
+        });
+      }
+      if (callback) callback(response);
+      return response;
+    });
+}
+
+function initGerritPluginsMethods(globalGerritObj: GerritGlobal) {
+  /**
+   * @deprecated Use plugin.styles().css(rulesStr) instead. Please, consult
+   * the documentation how to replace it accordingly.
+   */
+  globalGerritObj.css = (rulesStr: string) => {
+    console.warn(
+      'Gerrit.css(rulesStr) is deprecated!',
+      'Use plugin.styles().css(rulesStr)'
+    );
+    if (!globalGerritObj._customStyleSheet) {
+      const styleEl = document.createElement('style');
+      document.head.appendChild(styleEl);
+      globalGerritObj._customStyleSheet = styleEl.sheet!;
+    }
+
+    const name = `__pg_js_api_class_${globalGerritObj._customStyleSheet.cssRules.length}`;
+    globalGerritObj._customStyleSheet.insertRule(
+      '.' + name + '{' + rulesStr + '}',
+      0
+    );
+    return name;
+  };
+
+  globalGerritObj.install = (callback, opt_version, opt_src) => {
+    pluginLoader.install(callback, opt_version, opt_src);
+  };
+
+  globalGerritObj.getLoggedIn = () => {
+    console.warn(
+      'Gerrit.getLoggedIn() is deprecated! ' +
+        'Use plugin.restApi().getLoggedIn()'
+    );
+    return document.createElement('gr-rest-api-interface').getLoggedIn();
+  };
+
+  globalGerritObj.get = (
+    url: string,
+    callback?: (response: unknown) => void
+  ) => {
+    console.warn('.get() is deprecated! Use plugin.restApi().get()');
+    send(HttpMethod.GET, url, callback);
+  };
+
+  globalGerritObj.post = (
+    url: string,
+    payload?: RequestPayload,
+    callback?: (response: unknown) => void
+  ) => {
+    console.warn('.post() is deprecated! Use plugin.restApi().post()');
+    send(HttpMethod.POST, url, callback, payload);
+  };
+
+  globalGerritObj.put = (
+    url: string,
+    payload?: RequestPayload,
+    callback?: (response: unknown) => void
+  ) => {
+    console.warn('.put() is deprecated! Use plugin.restApi().put()');
+    send(HttpMethod.PUT, url, callback, payload);
+  };
+
+  globalGerritObj.delete = (
+    url: string,
+    callback?: (response: Response) => void
+  ) => {
+    deprecatedDelete(url, callback);
+  };
+
+  globalGerritObj.awaitPluginsLoaded = () => {
+    return pluginLoader.awaitPluginsLoaded();
+  };
+
+  // TODO(taoalpha): consider removing these proxy methods
+  // and using pluginLoader directly
+  globalGerritObj._loadPlugins = (plugins, opt_option) => {
+    pluginLoader.loadPlugins(plugins, opt_option);
+  };
+
+  globalGerritObj._arePluginsLoaded = () => {
+    return pluginLoader.arePluginsLoaded();
+  };
+
+  globalGerritObj._isPluginPreloaded = url => {
+    return pluginLoader.isPluginPreloaded(url);
+  };
+
+  globalGerritObj._isPluginEnabled = pathOrUrl => {
+    return pluginLoader.isPluginEnabled(pathOrUrl);
+  };
+
+  globalGerritObj._isPluginLoaded = pathOrUrl => {
+    return pluginLoader.isPluginLoaded(pathOrUrl);
+  };
+
+  const eventEmitter = appContext.eventEmitter;
+
+  // TODO(taoalpha): List all internal supported event names.
+  // Also convert this to inherited class once we move Gerrit to class.
+  globalGerritObj._eventEmitter = eventEmitter;
+  /**
+   * Enabling EventEmitter interface on Gerrit.
+   *
+   * This will enable to signal across different parts of js code without relying on DOM,
+   * including core to core, plugin to plugin and also core to plugin.
+   *
+   * @example
+   *
+   * // Emit this event from pluginA
+   * Gerrit.install(pluginA => {
+   *   fetch("some-api").then(() => {
+   *     Gerrit.on("your-special-event", {plugin: pluginA});
+   *   });
+   * });
+   *
+   * // Listen on your-special-event from pluignB
+   * Gerrit.install(pluginB => {
+   *   Gerrit.on("your-special-event", ({plugin}) => {
+   *     // do something, plugin is pluginA
+   *   });
+   * });
+   */
+  globalGerritObj.addListener = (eventName: string, cb: EventCallback) =>
+    eventEmitter.addListener(eventName, cb);
+  globalGerritObj.dispatch = (eventName: string, detail: any) =>
+    eventEmitter.dispatch(eventName, detail);
+  globalGerritObj.emit = (eventName: string, detail: any) =>
+    eventEmitter.emit(eventName, detail);
+  globalGerritObj.off = (eventName: string, cb: EventCallback) =>
+    eventEmitter.off(eventName, cb);
+  globalGerritObj.on = (eventName: string, cb: EventCallback) =>
+    eventEmitter.on(eventName, cb);
+  globalGerritObj.once = (eventName: string, cb: EventCallback) =>
+    eventEmitter.once(eventName, cb);
+  globalGerritObj.removeAllListeners = (eventName: string) =>
+    eventEmitter.removeAllListeners(eventName);
+  globalGerritObj.removeListener = (eventName: string, cb: EventCallback) =>
+    eventEmitter.removeListener(eventName, cb);
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js
deleted file mode 100644
index 6c785d4..0000000
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js
+++ /dev/null
@@ -1,327 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {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 {pluginLoader} from './gr-plugin-loader.js';
-import {patchNumEquals} from '../../../utils/patch-set-util.js';
-
-// Note: for new events, naming convention should be: `a-b`
-const EventType = {
-  HISTORY: 'history',
-  LABEL_CHANGE: 'labelchange',
-  SHOW_CHANGE: 'showchange',
-  SUBMIT_CHANGE: 'submitchange',
-  SHOW_REVISION_ACTIONS: 'show-revision-actions',
-  COMMIT_MSG_EDIT: 'commitmsgedit',
-  COMMENT: 'comment',
-  REVERT: 'revert',
-  REVERT_SUBMISSION: 'revert_submission',
-  POST_REVERT: 'postrevert',
-  ANNOTATE_DIFF: 'annotatediff',
-  ADMIN_MENU_LINKS: 'admin-menu-links',
-  HIGHLIGHTJS_LOADED: 'highlightjs-loaded',
-};
-
-const Element = {
-  CHANGE_ACTIONS: 'changeactions',
-  REPLY_DIALOG: 'replydialog',
-};
-
-/**
- * @extends PolymerElement
- */
-class GrJsApiInterface extends GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement)) {
-  static get is() { return 'gr-js-api-interface'; }
-
-  constructor() {
-    super();
-    this.Element = Element;
-    this.EventType = EventType;
-  }
-
-  static get properties() {
-    return {
-      _elements: {
-        type: Object,
-        value: {}, // Shared across all instances.
-      },
-      _eventCallbacks: {
-        type: Object,
-        value: {}, // Shared across all instances.
-      },
-    };
-  }
-
-  handleEvent(type, detail) {
-    pluginLoader.awaitPluginsLoaded().then(() => {
-      switch (type) {
-        case EventType.HISTORY:
-          this._handleHistory(detail);
-          break;
-        case EventType.SHOW_CHANGE:
-          this._handleShowChange(detail);
-          break;
-        case EventType.COMMENT:
-          this._handleComment(detail);
-          break;
-        case EventType.LABEL_CHANGE:
-          this._handleLabelChange(detail);
-          break;
-        case EventType.SHOW_REVISION_ACTIONS:
-          this._handleShowRevisionActions(detail);
-          break;
-        case EventType.HIGHLIGHTJS_LOADED:
-          this._handleHighlightjsLoaded(detail);
-          break;
-        default:
-          console.warn('handleEvent called with unsupported event type:',
-              type);
-          break;
-      }
-    });
-  }
-
-  addElement(key, el) {
-    this._elements[key] = el;
-  }
-
-  getElement(key) {
-    return this._elements[key];
-  }
-
-  addEventCallback(eventName, callback) {
-    if (!this._eventCallbacks[eventName]) {
-      this._eventCallbacks[eventName] = [];
-    }
-    this._eventCallbacks[eventName].push(callback);
-  }
-
-  canSubmitChange(change, revision) {
-    const submitCallbacks = this._getEventCallbacks(EventType.SUBMIT_CHANGE);
-    const cancelSubmit = submitCallbacks.some(callback => {
-      try {
-        return callback(change, revision) === false;
-      } catch (err) {
-        console.error(err);
-      }
-      return false;
-    });
-
-    return !cancelSubmit;
-  }
-
-  _removeEventCallbacks() {
-    for (const k in EventType) {
-      if (!EventType.hasOwnProperty(k)) { continue; }
-      this._eventCallbacks[EventType[k]] = [];
-    }
-  }
-
-  _handleHistory(detail) {
-    for (const cb of this._getEventCallbacks(EventType.HISTORY)) {
-      try {
-        cb(detail.path);
-      } catch (err) {
-        console.error(err);
-      }
-    }
-  }
-
-  _handleShowChange(detail) {
-    // Note (issue 8221) Shallow clone the change object and add a mergeable
-    // getter with deprecation warning. This makes the change detail appear as
-    // though SKIP_MERGEABLE was not set, so that plugins that expect it can
-    // still access.
-    //
-    // This clone and getter can be removed after plugins migrate to use
-    // info.mergeable.
-    //
-    // assign on getter with existing property will report error
-    // see Issue: 12286
-    const change = {...detail.change, get mergeable() {
-      console.warn('Accessing change.mergeable from SHOW_CHANGE is ' +
-            'deprecated! Use info.mergeable instead.');
-      return detail.info && detail.info.mergeable;
-    }};
-    const patchNum = detail.patchNum;
-    const info = detail.info;
-
-    let revision;
-    for (const rev of Object.values(change.revisions || {})) {
-      if (patchNumEquals(rev._number, patchNum)) {
-        revision = rev;
-        break;
-      }
-    }
-
-    for (const cb of this._getEventCallbacks(EventType.SHOW_CHANGE)) {
-      try {
-        cb(change, revision, info);
-      } catch (err) {
-        console.error(err);
-      }
-    }
-  }
-
-  /**
-   * @param {!{change: !Object, revisionActions: !Object}} detail
-   */
-  _handleShowRevisionActions(detail) {
-    const registeredCallbacks = this._getEventCallbacks(
-        EventType.SHOW_REVISION_ACTIONS
-    );
-    for (const cb of registeredCallbacks) {
-      try {
-        cb(detail.revisionActions, detail.change);
-      } catch (err) {
-        console.error(err);
-      }
-    }
-  }
-
-  handleCommitMessage(change, msg) {
-    for (const cb of this._getEventCallbacks(EventType.COMMIT_MSG_EDIT)) {
-      try {
-        cb(change, msg);
-      } catch (err) {
-        console.error(err);
-      }
-    }
-  }
-
-  _handleComment(detail) {
-    for (const cb of this._getEventCallbacks(EventType.COMMENT)) {
-      try {
-        cb(detail.node);
-      } catch (err) {
-        console.error(err);
-      }
-    }
-  }
-
-  _handleLabelChange(detail) {
-    for (const cb of this._getEventCallbacks(EventType.LABEL_CHANGE)) {
-      try {
-        cb(detail.change);
-      } catch (err) {
-        console.error(err);
-      }
-    }
-  }
-
-  _handleHighlightjsLoaded(detail) {
-    for (const cb of this._getEventCallbacks(EventType.HIGHLIGHTJS_LOADED)) {
-      try {
-        cb(detail.hljs);
-      } catch (err) {
-        console.error(err);
-      }
-    }
-  }
-
-  modifyRevertMsg(change, revertMsg, origMsg) {
-    for (const cb of this._getEventCallbacks(EventType.REVERT)) {
-      try {
-        revertMsg = cb(change, revertMsg, origMsg);
-      } catch (err) {
-        console.error(err);
-      }
-    }
-    return revertMsg;
-  }
-
-  modifyRevertSubmissionMsg(change, revertSubmissionMsg, origMsg) {
-    for (const cb of this._getEventCallbacks(EventType.REVERT_SUBMISSION)) {
-      try {
-        revertSubmissionMsg = cb(change, revertSubmissionMsg, origMsg);
-      } catch (err) {
-        console.error(err);
-      }
-    }
-    return revertSubmissionMsg;
-  }
-
-  getDiffLayers(path, changeNum, patchNum) {
-    const layers = [];
-    for (const annotationApi of
-      this._getEventCallbacks(EventType.ANNOTATE_DIFF)) {
-      try {
-        const layer = annotationApi.getLayer(path, changeNum, patchNum);
-        layers.push(layer);
-      } catch (err) {
-        console.error(err);
-      }
-    }
-    return layers;
-  }
-
-  disposeDiffLayers(path) {
-    for (const annotationApi of
-      this._getEventCallbacks(EventType.ANNOTATE_DIFF)) {
-      try {
-        annotationApi.disposeLayer(path);
-      } catch (err) {
-        console.error(err);
-      }
-    }
-  }
-
-  /**
-   * Retrieves coverage data possibly provided by a plugin.
-   *
-   * Will wait for plugins to be loaded. If multiple plugins offer a coverage
-   * provider, the first one is returned. If no plugin offers a coverage provider,
-   * will resolve to null.
-   *
-   * @return {!Promise<?GrAnnotationActionsInterface>}
-   */
-  getCoverageAnnotationApi() {
-    return pluginLoader.awaitPluginsLoaded()
-        .then(() => this._getEventCallbacks(EventType.ANNOTATE_DIFF)
-            .find(api => api.getCoverageProvider()));
-  }
-
-  getAdminMenuLinks() {
-    const links = [];
-    for (const adminApi of
-      this._getEventCallbacks(EventType.ADMIN_MENU_LINKS)) {
-      links.push(...adminApi.getMenuLinks());
-    }
-    return links;
-  }
-
-  getLabelValuesPostRevert(change) {
-    let labels = {};
-    for (const cb of this._getEventCallbacks(EventType.POST_REVERT)) {
-      try {
-        labels = cb(change);
-      } catch (err) {
-        console.error(err);
-      }
-    }
-    return labels;
-  }
-
-  _getEventCallbacks(type) {
-    return this._eventCallbacks[type] || [];
-  }
-}
-
-customElements.define(GrJsApiInterface.is, GrJsApiInterface);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
new file mode 100644
index 0000000..4973594
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
@@ -0,0 +1,316 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {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 {pluginLoader} from './gr-plugin-loader';
+import {patchNumEquals} from '../../../utils/patch-set-util';
+import {customElement} from '@polymer/decorators';
+import {ChangeInfo, RevisionInfo} from '../../../types/common';
+import {
+  CoverageProvider,
+  GrAnnotationActionsInterface,
+} from './gr-annotation-actions-js-api';
+import {GrAdminApi} from '../../plugins/gr-admin-api/gr-admin-api';
+import {
+  JsApiService,
+  EventCallback,
+  ShowChangeDetail,
+  ShowRevisionActionsDetail,
+} from './gr-js-api-types';
+import {EventType, TargetElement} from '../../plugins/gr-plugin-types';
+
+const elements: {[key: string]: HTMLElement} = {};
+const eventCallbacks: {[key: string]: EventCallback[]} = {};
+
+@customElement('gr-js-api-interface')
+export class GrJsApiInterface
+  extends GestureEventListeners(LegacyElementMixin(PolymerElement))
+  implements JsApiService {
+  handleEvent(type: EventType, detail: any) {
+    pluginLoader.awaitPluginsLoaded().then(() => {
+      switch (type) {
+        case EventType.HISTORY:
+          this._handleHistory(detail);
+          break;
+        case EventType.SHOW_CHANGE:
+          this._handleShowChange(detail);
+          break;
+        case EventType.COMMENT:
+          this._handleComment(detail);
+          break;
+        case EventType.LABEL_CHANGE:
+          this._handleLabelChange(detail);
+          break;
+        case EventType.SHOW_REVISION_ACTIONS:
+          this._handleShowRevisionActions(detail);
+          break;
+        case EventType.HIGHLIGHTJS_LOADED:
+          this._handleHighlightjsLoaded(detail);
+          break;
+        default:
+          console.warn('handleEvent called with unsupported event type:', type);
+          break;
+      }
+    });
+  }
+
+  addElement(key: TargetElement, el: HTMLElement) {
+    elements[key] = el;
+  }
+
+  getElement(key: TargetElement) {
+    return elements[key];
+  }
+
+  addEventCallback(eventName: EventType, callback: EventCallback) {
+    if (!eventCallbacks[eventName]) {
+      eventCallbacks[eventName] = [];
+    }
+    eventCallbacks[eventName].push(callback);
+  }
+
+  canSubmitChange(change: ChangeInfo, revision: RevisionInfo) {
+    const submitCallbacks = this._getEventCallbacks(EventType.SUBMIT_CHANGE);
+    const cancelSubmit = submitCallbacks.some(callback => {
+      try {
+        return callback(change, revision) === false;
+      } catch (err) {
+        console.error(err);
+      }
+      return false;
+    });
+
+    return !cancelSubmit;
+  }
+
+  /** For testing only. */
+  _removeEventCallbacks() {
+    for (const type of Object.values(EventType)) {
+      eventCallbacks[type] = [];
+    }
+  }
+
+  // TODO(TS): The HISTORY event and its handler seem unused.
+  _handleHistory(detail: {path: string}) {
+    for (const cb of this._getEventCallbacks(EventType.HISTORY)) {
+      try {
+        cb(detail.path);
+      } catch (err) {
+        console.error(err);
+      }
+    }
+  }
+
+  _handleShowChange(detail: ShowChangeDetail) {
+    // Note (issue 8221) Shallow clone the change object and add a mergeable
+    // getter with deprecation warning. This makes the change detail appear as
+    // though SKIP_MERGEABLE was not set, so that plugins that expect it can
+    // still access.
+    //
+    // This clone and getter can be removed after plugins migrate to use
+    // info.mergeable.
+    //
+    // assign on getter with existing property will report error
+    // see Issue: 12286
+    const change = {
+      ...detail.change,
+      get mergeable() {
+        console.warn(
+          'Accessing change.mergeable from SHOW_CHANGE is ' +
+            'deprecated! Use info.mergeable instead.'
+        );
+        return detail.info && detail.info.mergeable;
+      },
+    };
+    const patchNum = detail.patchNum;
+    const info = detail.info;
+
+    let revision;
+    for (const rev of Object.values(change.revisions || {})) {
+      if (patchNumEquals(rev._number, patchNum)) {
+        revision = rev;
+        break;
+      }
+    }
+
+    for (const cb of this._getEventCallbacks(EventType.SHOW_CHANGE)) {
+      try {
+        cb(change, revision, info);
+      } catch (err) {
+        console.error(err);
+      }
+    }
+  }
+
+  _handleShowRevisionActions(detail: ShowRevisionActionsDetail) {
+    const registeredCallbacks = this._getEventCallbacks(
+      EventType.SHOW_REVISION_ACTIONS
+    );
+    for (const cb of registeredCallbacks) {
+      try {
+        cb(detail.revisionActions, detail.change);
+      } catch (err) {
+        console.error(err);
+      }
+    }
+  }
+
+  handleCommitMessage(change: ChangeInfo, msg: string) {
+    for (const cb of this._getEventCallbacks(EventType.COMMIT_MSG_EDIT)) {
+      try {
+        cb(change, msg);
+      } catch (err) {
+        console.error(err);
+      }
+    }
+  }
+
+  // TODO(TS): The COMMENT event and its handler seem unused.
+  _handleComment(detail: {node: Node}) {
+    for (const cb of this._getEventCallbacks(EventType.COMMENT)) {
+      try {
+        cb(detail.node);
+      } catch (err) {
+        console.error(err);
+      }
+    }
+  }
+
+  _handleLabelChange(detail: {change: ChangeInfo}) {
+    for (const cb of this._getEventCallbacks(EventType.LABEL_CHANGE)) {
+      try {
+        cb(detail.change);
+      } catch (err) {
+        console.error(err);
+      }
+    }
+  }
+
+  _handleHighlightjsLoaded(detail: {hljs: unknown}) {
+    for (const cb of this._getEventCallbacks(EventType.HIGHLIGHTJS_LOADED)) {
+      try {
+        cb(detail.hljs);
+      } catch (err) {
+        console.error(err);
+      }
+    }
+  }
+
+  modifyRevertMsg(change: ChangeInfo, revertMsg: string, origMsg: string) {
+    for (const cb of this._getEventCallbacks(EventType.REVERT)) {
+      try {
+        revertMsg = cb(change, revertMsg, origMsg) as string;
+      } catch (err) {
+        console.error(err);
+      }
+    }
+    return revertMsg;
+  }
+
+  modifyRevertSubmissionMsg(
+    change: ChangeInfo,
+    revertSubmissionMsg: string,
+    origMsg: string
+  ) {
+    for (const cb of this._getEventCallbacks(EventType.REVERT_SUBMISSION)) {
+      try {
+        revertSubmissionMsg = cb(
+          change,
+          revertSubmissionMsg,
+          origMsg
+        ) as string;
+      } catch (err) {
+        console.error(err);
+      }
+    }
+    return revertSubmissionMsg;
+  }
+
+  getDiffLayers(path: string, changeNum: number, patchNum: number) {
+    const layers = [];
+    for (const cb of this._getEventCallbacks(EventType.ANNOTATE_DIFF)) {
+      const annotationApi = (cb as unknown) as GrAnnotationActionsInterface;
+      try {
+        const layer = annotationApi.getLayer(path, changeNum, patchNum);
+        layers.push(layer);
+      } catch (err) {
+        console.error(err);
+      }
+    }
+    return layers;
+  }
+
+  disposeDiffLayers(path: string) {
+    for (const cb of this._getEventCallbacks(EventType.ANNOTATE_DIFF)) {
+      try {
+        const annotationApi = (cb as unknown) as GrAnnotationActionsInterface;
+        annotationApi.disposeLayer(path);
+      } catch (err) {
+        console.error(err);
+      }
+    }
+  }
+
+  /**
+   * Retrieves coverage data possibly provided by a plugin.
+   *
+   * Will wait for plugins to be loaded. If multiple plugins offer a coverage
+   * provider, the first one is returned. If no plugin offers a coverage provider,
+   * will resolve to null.
+   */
+  getCoverageAnnotationApi(): Promise<CoverageProvider | undefined> {
+    return pluginLoader.awaitPluginsLoaded().then(
+      () =>
+        this._getEventCallbacks(EventType.ANNOTATE_DIFF).find(cb => {
+          const annotationApi = (cb as unknown) as GrAnnotationActionsInterface;
+          return annotationApi.getCoverageProvider();
+        }) as CoverageProvider | undefined
+    );
+  }
+
+  getAdminMenuLinks() {
+    const links = [];
+    for (const cb of this._getEventCallbacks(EventType.ADMIN_MENU_LINKS)) {
+      const adminApi = (cb as unknown) as GrAdminApi;
+      links.push(...adminApi.getMenuLinks());
+    }
+    return links;
+  }
+
+  getLabelValuesPostRevert(change: ChangeInfo) {
+    let labels = {};
+    for (const cb of this._getEventCallbacks(EventType.POST_REVERT)) {
+      try {
+        labels = cb(change);
+      } catch (err) {
+        console.error(err);
+      }
+    }
+    return labels;
+  }
+
+  _getEventCallbacks(type: EventType) {
+    return eventCallbacks[type] || [];
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-js-api-interface': JsApiService & Element;
+  }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
deleted file mode 100644
index 6f0ade9..0000000
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import '../gr-rest-api-interface/gr-rest-api-interface.js';
-import './gr-js-api-interface-element.js';
-import './gr-public-js-api.js';
-import './gr-gerrit.js';
-
-/*
-  Note: the order matters as files depend on each other.
-  1. gr-api-utils will be used in multiple files below.
-  2. gr-gerrit depends on gr-plugin-loader, gr-public-js-api and
-    also gr-plugin-endpoints
-  3. gr-public-js-api depends on gr-plugin-rest-api
-*/
-/*
-  FIXME(polymer-modulizer): the above comments were extracted
-  from HTML and may be out of place here. Review them and
-  then delete this comment!
-*/
-
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.ts
new file mode 100644
index 0000000..b9a6ff4
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.ts
@@ -0,0 +1,20 @@
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../gr-rest-api-interface/gr-rest-api-interface';
+import './gr-js-api-interface-element';
+import './gr-public-js-api';
+import './gr-gerrit';
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
index 2a11f62..272a13b 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
@@ -19,6 +19,7 @@
 import './gr-js-api-interface.js';
 import {GrPopupInterface} from '../../plugins/gr-popup-interface/gr-popup-interface.js';
 import {GrSettingsApi} from '../../plugins/gr-settings-api/gr-settings-api.js';
+import {EventType} from '../../plugins/gr-plugin-types.js';
 import {GrPluginActionContext} from './gr-plugin-action-context.js';
 import {PLUGIN_LOADING_TIMEOUT_MS} from './gr-api-utils.js';
 import {pluginLoader} from './gr-plugin-loader.js';
@@ -183,13 +184,13 @@
   });
 
   test('history event', done => {
-    plugin.on(element.EventType.HISTORY, throwErrFn);
-    plugin.on(element.EventType.HISTORY, path => {
+    plugin.on(EventType.HISTORY, throwErrFn);
+    plugin.on(EventType.HISTORY, path => {
       assert.equal(path, '/path/to/awesomesauce');
       assert.isTrue(errorStub.calledOnce);
       done();
     });
-    element.handleEvent(element.EventType.HISTORY,
+    element.handleEvent(EventType.HISTORY,
         {path: '/path/to/awesomesauce'});
   });
 
@@ -199,15 +200,15 @@
       revisions: {def: {_number: 2}, abc: {_number: 1}},
     };
     const expectedChange = {mergeable: false, ...testChange};
-    plugin.on(element.EventType.SHOW_CHANGE, throwErrFn);
-    plugin.on(element.EventType.SHOW_CHANGE, (change, revision, info) => {
+    plugin.on(EventType.SHOW_CHANGE, throwErrFn);
+    plugin.on(EventType.SHOW_CHANGE, (change, revision, info) => {
       assert.deepEqual(change, expectedChange);
       assert.deepEqual(revision, testChange.revisions.abc);
       assert.deepEqual(info, {mergeable: false});
       assert.isTrue(errorStub.calledOnce);
       done();
     });
-    element.handleEvent(element.EventType.SHOW_CHANGE,
+    element.handleEvent(EventType.SHOW_CHANGE,
         {change: testChange, patchNum: 1, info: {mergeable: false}});
   });
 
@@ -216,14 +217,14 @@
       _number: 42,
       revisions: {def: {_number: 2}, abc: {_number: 1}},
     };
-    plugin.on(element.EventType.SHOW_REVISION_ACTIONS, throwErrFn);
-    plugin.on(element.EventType.SHOW_REVISION_ACTIONS, (actions, change) => {
+    plugin.on(EventType.SHOW_REVISION_ACTIONS, throwErrFn);
+    plugin.on(EventType.SHOW_REVISION_ACTIONS, (actions, change) => {
       assert.deepEqual(change, testChange);
       assert.deepEqual(actions, {test: {}});
       assert.isTrue(errorStub.calledOnce);
       done();
     });
-    element.handleEvent(element.EventType.SHOW_REVISION_ACTIONS,
+    element.handleEvent(EventType.SHOW_REVISION_ACTIONS,
         {change: testChange, revisionActions: {test: {}}});
   });
 
@@ -234,8 +235,8 @@
     };
     const spy = sinon.spy();
     pluginLoader.loadPlugins(['plugins/test.html']);
-    plugin.on(element.EventType.SHOW_CHANGE, spy);
-    element.handleEvent(element.EventType.SHOW_CHANGE,
+    plugin.on(EventType.SHOW_CHANGE, spy);
+    element.handleEvent(EventType.SHOW_CHANGE,
         {change: testChange, patchNum: 1});
     assert.isFalse(spy.called);
 
@@ -250,13 +251,13 @@
 
   test('comment event', done => {
     const testCommentNode = {foo: 'bar'};
-    plugin.on(element.EventType.COMMENT, throwErrFn);
-    plugin.on(element.EventType.COMMENT, commentNode => {
+    plugin.on(EventType.COMMENT, throwErrFn);
+    plugin.on(EventType.COMMENT, commentNode => {
       assert.deepEqual(commentNode, testCommentNode);
       assert.isTrue(errorStub.calledOnce);
       done();
     });
-    element.handleEvent(element.EventType.COMMENT, {node: testCommentNode});
+    element.handleEvent(EventType.COMMENT, {node: testCommentNode});
   });
 
   test('revert event', () => {
@@ -267,13 +268,13 @@
     assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'), 'test');
     assert.equal(errorStub.callCount, 0);
 
-    plugin.on(element.EventType.REVERT, throwErrFn);
-    plugin.on(element.EventType.REVERT, appendToRevertMsg);
+    plugin.on(EventType.REVERT, throwErrFn);
+    plugin.on(EventType.REVERT, appendToRevertMsg);
     assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'),
         'test\n> origTest\ninfo');
     assert.isTrue(errorStub.calledOnce);
 
-    plugin.on(element.EventType.REVERT, appendToRevertMsg);
+    plugin.on(EventType.REVERT, appendToRevertMsg);
     assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'),
         'test\n> origTest\ninfo\n> origTest\ninfo');
     assert.isTrue(errorStub.calledTwice);
@@ -287,8 +288,8 @@
     assert.deepEqual(element.getLabelValuesPostRevert(null), {});
     assert.equal(errorStub.callCount, 0);
 
-    plugin.on(element.EventType.POST_REVERT, throwErrFn);
-    plugin.on(element.EventType.POST_REVERT, getLabels);
+    plugin.on(EventType.POST_REVERT, throwErrFn);
+    plugin.on(EventType.POST_REVERT, getLabels);
     assert.deepEqual(
         element.getLabelValuesPostRevert(null), {'Code-Review': 1});
     assert.isTrue(errorStub.calledOnce);
@@ -296,8 +297,8 @@
 
   test('commitmsgedit event', done => {
     const testMsg = 'Test CL commit message';
-    plugin.on(element.EventType.COMMIT_MSG_EDIT, throwErrFn);
-    plugin.on(element.EventType.COMMIT_MSG_EDIT, (change, msg) => {
+    plugin.on(EventType.COMMIT_MSG_EDIT, throwErrFn);
+    plugin.on(EventType.COMMIT_MSG_EDIT, (change, msg) => {
       assert.deepEqual(msg, testMsg);
       assert.isTrue(errorStub.calledOnce);
       done();
@@ -307,35 +308,35 @@
 
   test('labelchange event', done => {
     const testChange = {_number: 42};
-    plugin.on(element.EventType.LABEL_CHANGE, throwErrFn);
-    plugin.on(element.EventType.LABEL_CHANGE, change => {
+    plugin.on(EventType.LABEL_CHANGE, throwErrFn);
+    plugin.on(EventType.LABEL_CHANGE, change => {
       assert.deepEqual(change, testChange);
       assert.isTrue(errorStub.calledOnce);
       done();
     });
-    element.handleEvent(element.EventType.LABEL_CHANGE, {change: testChange});
+    element.handleEvent(EventType.LABEL_CHANGE, {change: testChange});
   });
 
   test('submitchange', () => {
-    plugin.on(element.EventType.SUBMIT_CHANGE, throwErrFn);
-    plugin.on(element.EventType.SUBMIT_CHANGE, () => true);
+    plugin.on(EventType.SUBMIT_CHANGE, throwErrFn);
+    plugin.on(EventType.SUBMIT_CHANGE, () => true);
     assert.isTrue(element.canSubmitChange());
     assert.isTrue(errorStub.calledOnce);
-    plugin.on(element.EventType.SUBMIT_CHANGE, () => false);
-    plugin.on(element.EventType.SUBMIT_CHANGE, () => true);
+    plugin.on(EventType.SUBMIT_CHANGE, () => false);
+    plugin.on(EventType.SUBMIT_CHANGE, () => true);
     assert.isFalse(element.canSubmitChange());
     assert.isTrue(errorStub.calledTwice);
   });
 
   test('highlightjs-loaded event', done => {
     const testHljs = {_number: 42};
-    plugin.on(element.EventType.HIGHLIGHTJS_LOADED, throwErrFn);
-    plugin.on(element.EventType.HIGHLIGHTJS_LOADED, hljs => {
+    plugin.on(EventType.HIGHLIGHTJS_LOADED, throwErrFn);
+    plugin.on(EventType.HIGHLIGHTJS_LOADED, hljs => {
       assert.deepEqual(hljs, testHljs);
       assert.isTrue(errorStub.calledOnce);
       done();
     });
-    element.handleEvent(element.EventType.HIGHLIGHTJS_LOADED, {hljs: testHljs});
+    element.handleEvent(EventType.HIGHLIGHTJS_LOADED, {hljs: testHljs});
   });
 
   test('getLoggedIn', done => {
@@ -353,6 +354,8 @@
   });
 
   test('deprecated.install', () => {
+    assert.notStrictEqual(plugin.popup, plugin.deprecated.popup);
+    assert.notStrictEqual(plugin.onAction, plugin.deprecated.onAction);
     plugin.deprecated.install();
     assert.strictEqual(plugin.popup, plugin.deprecated.popup);
     assert.strictEqual(plugin.onAction, plugin.deprecated.onAction);
@@ -370,7 +373,7 @@
     assert.deepEqual(result, links);
     assert.isTrue(getCallbacksStub.calledOnce);
     assert.equal(getCallbacksStub.lastCall.args[0],
-        element.EventType.ADMIN_MENU_LINKS);
+        EventType.ADMIN_MENU_LINKS);
   });
 
   suite('test plugin with base url', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
new file mode 100644
index 0000000..4b597a5
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
@@ -0,0 +1,37 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {ActionInfo, ChangeInfo, PatchSetNum} from '../../../types/common';
+import {EventType, TargetElement} from '../../plugins/gr-plugin-types';
+
+export interface ShowChangeDetail {
+  change: ChangeInfo;
+  patchNum: PatchSetNum;
+  info: {mergeable: boolean};
+}
+
+export interface ShowRevisionActionsDetail {
+  change: ChangeInfo;
+  revisionActions: {[key: string]: ActionInfo};
+}
+
+export type EventCallback = (...args: any[]) => any;
+
+export interface JsApiService {
+  getElement(key: TargetElement): HTMLElement;
+  addEventCallback(eventName: EventType, callback: EventCallback): void;
+  // TODO(TS): Add more methods when needed for the TS conversion.
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts
index 38e11d6..34ecd9e 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts
@@ -15,19 +15,13 @@
  * limitations under the License.
  */
 
-import {GrPluginRestApi} from './gr-plugin-rest-api';
-import {GrEventHelper} from '../../plugins/gr-event-helper/gr-event-helper';
-import {RevisionInfo, ChangeInfo, RequestPayload} from '../../../types/common';
-import {HttpMethod} from '../../../constants/constants';
-
-interface PluginApi {
-  restApi(): GrPluginRestApi;
-  deprecated: PluginDeprecatedApi;
-  eventHelper(element: Node): GrEventHelper;
-}
-interface PluginDeprecatedApi {
-  popup(element: Node): GrPopupInterface;
-}
+import {
+  RevisionInfo,
+  ChangeInfo,
+  RequestPayload,
+  ActionInfo,
+} from '../../../types/common';
+import {PluginApi} from '../../plugins/gr-plugin-types';
 
 interface GrPopupInterface {
   close(): void;
@@ -37,18 +31,12 @@
   onclick: (event: Event) => boolean;
 }
 
-interface ActionInterface {
-  method: HttpMethod;
-  __url: string;
-  __key: string;
-}
-
 export class GrPluginActionContext {
   private _popups: GrPopupInterface[] = [];
 
   constructor(
     public readonly plugin: PluginApi,
-    public readonly action: ActionInterface,
+    public readonly action: ActionInfo,
     public readonly change: ChangeInfo,
     public readonly revision: RevisionInfo
   ) {}
@@ -115,6 +103,7 @@
   }
 
   call(payload: RequestPayload, onSuccess: (result: unknown) => void) {
+    if (!this.action.method) return;
     if (!this.action.__url) {
       console.warn(`Unable to ${this.action.method} to ${this.action.__key}!`);
       return;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts
index db6d14a..2aabf34 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts
@@ -16,17 +16,17 @@
  */
 
 import {importHref} from '../../../scripts/import-href';
+import {HookApi, PluginApi} from '../../plugins/gr-plugin-types';
+import {notUndefined} from '../../../types/types';
 
 type Callback = (value: any) => void;
 
 interface ModuleInfo {
   moduleName: string;
-  // TODO(TS): Convert type to GrPlugin.
-  plugin: any;
-  pluginUrl: URL;
+  plugin: PluginApi;
+  pluginUrl?: URL;
   type?: string;
-  // TODO(TS): Convert type to GrDomHook.
-  domHook: any;
+  domHook?: HookApi;
   slot?: string;
 }
 
@@ -36,8 +36,7 @@
   slot?: string;
   type?: string;
   moduleName?: string;
-  // TODO(TS): Convert type to GrDomHook.
-  domHook?: any;
+  domHook?: HookApi;
 }
 
 export class GrPluginEndpoints {
@@ -71,7 +70,7 @@
     }
   }
 
-  _getOrCreateModuleInfo(plugin: any, opts: Options): ModuleInfo {
+  _getOrCreateModuleInfo(plugin: PluginApi, opts: Options): ModuleInfo {
     const {endpoint, slot, type, moduleName, domHook} = opts;
     const existingModule = this._endpoints
       .get(endpoint!)!
@@ -105,7 +104,7 @@
    * 'change-list-header'. These plugins are then fetched by prefix to determine
    * which endpoints to dynamically add to the page.
    */
-  registerModule(plugin: any, opts: Options) {
+  registerModule(plugin: PluginApi, opts: Options) {
     const endpoint = opts.endpoint!;
     const dynamicEndpoint = opts.dynamicEndpoint;
     if (dynamicEndpoint) {
@@ -173,7 +172,9 @@
     if (!modulesData.length) {
       return [];
     }
-    return Array.from(new Set(modulesData.map(m => m.pluginUrl)));
+    return Array.from(new Set(modulesData.map(m => m.pluginUrl))).filter(
+      notUndefined
+    );
   }
 
   importUrl(pluginUrl: URL) {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
similarity index 61%
rename from polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js
rename to polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
index 6b8f73e..31254ce 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
@@ -1,5 +1,3 @@
-import {appContext} from '../../../services/app-context.js';
-
 /**
  * @license
  * Copyright (C) 2019 The Android Open Source Project
@@ -16,41 +14,58 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {importHref} from '../../../scripts/import-href.js';
+import {appContext} from '../../../services/app-context';
+import {importHref} from '../../../scripts/import-href';
 import {
   PLUGIN_LOADING_TIMEOUT_MS,
   PRELOADED_PROTOCOL,
   getPluginNameFromUrl,
-} from './gr-api-utils.js';
-import {Plugin} from './gr-public-js-api.js';
-import {getBaseUrl} from '../../../utils/url-util.js';
-import {getPluginEndpoints} from './gr-plugin-endpoints.js';
+} from './gr-api-utils';
+import {Plugin} from './gr-public-js-api';
+import {getBaseUrl} from '../../../utils/url-util';
+import {getPluginEndpoints} from './gr-plugin-endpoints';
+import {PluginApi} from '../../plugins/gr-plugin-types';
+import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
+import {hasOwnProperty} from '../../../utils/common-util';
 
-/**
- * @enum {string}
- */
-const PluginState = {
-  /**
-   * State that indicates the plugin is pending to be loaded.
-   */
-  PENDING: 'PENDING',
+enum PluginState {
+  /** State that indicates the plugin is pending to be loaded. */
+  PENDING = 'PENDING',
+  /** State that indicates the plugin is already loaded. */
+  LOADED = 'LOADED',
+  /** State that indicates the plugin failed to load. */
+  LOAD_FAILED = 'LOAD_FAILED',
+}
 
-  /**
-   * State that indicates the plugin is already loaded.
-   */
-  LOADED: 'LOADED',
+interface PluginObject {
+  name: string;
+  url: string;
+  state: PluginState;
+  plugin: PluginApi | null;
+}
 
-  /**
-   * State that indicates the plugin is already loaded.
-   */
-  PRE_LOADED: 'PRE_LOADED',
+interface PluginOption {
+  sync?: boolean;
+}
 
-  /**
-   * State that indicates the plugin failed to load.
-   */
-  LOAD_FAILED: 'LOAD_FAILED',
+export interface PluginOptionMap {
+  [path: string]: PluginOption;
+}
+
+type GerritScriptElement = HTMLScriptElement & {
+  __importElement: HTMLScriptElement;
 };
 
+type PluginCallback = (plugin: PluginApi) => void;
+
+interface PluginCallbackMap {
+  [name: string]: PluginCallback;
+}
+
+interface GerritGlobal {
+  _preloadedPlugins?: PluginCallbackMap;
+}
+
 // Prefix for any unrecognized plugin urls.
 // Url should match following patterns:
 // /plugins/PLUGINNAME/static/SCRIPTNAME.(html|js)
@@ -71,20 +86,17 @@
  * Check plugin status and if all plugins loaded.
  */
 export class PluginLoader {
-  constructor() {
-    this._pluginListLoaded = false;
+  _pluginListLoaded = false;
 
-    /** @type {Map<string,PluginLoader.PluginObject>} */
-    this._plugins = new Map();
+  _plugins = new Map<string, PluginObject>();
 
-    this._reporting = null;
+  _reporting: ReportingService | null = null;
 
-    // Promise that resolves when all plugins loaded
-    this._loadingPromise = null;
+  // Promise that resolves when all plugins loaded
+  _loadingPromise: Promise<void> | null = null;
 
-    // Resolver to resolve _loadingPromise once all plugins loaded
-    this._loadingResolver = null;
-  }
+  // Resolver to resolve _loadingPromise once all plugins loaded
+  _loadingResolver: (() => void) | null = null;
 
   _getReporting() {
     if (!this._reporting) {
@@ -95,22 +107,15 @@
 
   /**
    * Use the plugin name or use the full url if not recognized.
-   *
-   * @see gr-api-utils#getPluginNameFromUrl
-   * @param {string|URL} url
    */
-  _getPluginKeyFromUrl(url) {
-    return getPluginNameFromUrl(url) ||
-      `${UNKNOWN_PLUGIN_PREFIX}${url}`;
+  _getPluginKeyFromUrl(url: string) {
+    return getPluginNameFromUrl(url) || `${UNKNOWN_PLUGIN_PREFIX}${url}`;
   }
 
   /**
    * Load multiple plugins with certain options.
-   *
-   * @param {Array<string>} plugins
-   * @param {Object<string, PluginLoader.PluginOption>} opts
    */
-  loadPlugins(plugins = [], opts = {}) {
+  loadPlugins(plugins: string[] = [], opts: PluginOptionMap = {}) {
     this._pluginListLoaded = true;
 
     plugins.forEach(path => {
@@ -143,7 +148,7 @@
     });
   }
 
-  _isPathEndsWith(url, suffix) {
+  _isPathEndsWith(url: string | URL, suffix: string) {
     if (!(url instanceof URL)) {
       try {
         url = new URL(url);
@@ -166,19 +171,29 @@
     return installedPlugins;
   }
 
-  install(callback, opt_version, opt_src) {
+  install(
+    callback: (plugin: PluginApi) => void,
+    version?: string,
+    src?: string
+  ) {
     // HTML import polyfill adds __importElement pointing to the import tag.
-    const script = document.currentScript &&
-        (document.currentScript.__importElement || document.currentScript);
-    let src = opt_src || (script && script.src);
-    if (!src || src.startsWith('data:')) {
+    const gerritScript = document.currentScript as GerritScriptElement | null;
+    const script = gerritScript?.__importElement ?? gerritScript;
+    if (!src && script && script.src) {
+      src = script.src;
+    }
+    if ((!src || src.startsWith('data:')) && script && script.baseURI) {
       src = script && script.baseURI;
     }
-
-    if (opt_version && opt_version !== API_VERSION) {
-      this._failToLoad(`Plugin ${src} install error: only version ` +
-          API_VERSION + ' is supported in PolyGerrit. ' + opt_version +
-          ' was given.', src);
+    if (!src) {
+      this._failToLoad('Failed to determine src.');
+      return;
+    }
+    if (version && version !== API_VERSION) {
+      this._failToLoad(
+        `Plugin ${src} install error: only version ${API_VERSION} is supported in PolyGerrit. ${version} was given.`,
+        src
+      );
       return;
     }
 
@@ -231,21 +246,23 @@
     return `Timeout when loading plugins: ${pendingPlugins.join(',')}`;
   }
 
-  _failToLoad(message, pluginUrl) {
+  _failToLoad(message: string, pluginUrl?: string) {
     // Show an alert with the error
-    document.dispatchEvent(new CustomEvent('show-alert', {
-      detail: {
-        message: `Plugin install error: ${message} from ${pluginUrl}`,
-      },
-    }));
-    this._updatePluginState(pluginUrl, PluginState.LOAD_FAILED);
+    document.dispatchEvent(
+      new CustomEvent('show-alert', {
+        detail: {
+          message: `Plugin install error: ${message} from ${pluginUrl}`,
+        },
+      })
+    );
+    if (pluginUrl) this._updatePluginState(pluginUrl, PluginState.LOAD_FAILED);
     this._checkIfCompleted();
   }
 
-  _updatePluginState(pluginUrl, state) {
+  _updatePluginState(pluginUrl: string, state: PluginState): PluginObject {
     const key = this._getPluginKeyFromUrl(pluginUrl);
     if (this._plugins.has(key)) {
-      this._plugins.get(key).state = state;
+      this._plugins.get(key)!.state = state;
     } else {
       // Plugin is not recorded for some reason.
       console.info(`Plugin loaded separately: ${pluginUrl}`);
@@ -256,10 +273,10 @@
         plugin: null,
       });
     }
-    return this._plugins.get(key);
+    return this._plugins.get(key)!;
   }
 
-  _pluginInstalled(url, plugin) {
+  _pluginInstalled(url: string, plugin: PluginApi) {
     const pluginObj = this._updatePluginState(url, PluginState.LOADED);
     pluginObj.plugin = plugin;
     this._getReporting().pluginLoaded(plugin.getPluginName() || url);
@@ -268,20 +285,22 @@
   }
 
   installPreloadedPlugins() {
-    if (!window.Gerrit || !window.Gerrit._preloadedPlugins) { return; }
-    const Gerrit = window.Gerrit;
-    for (const name in Gerrit._preloadedPlugins) {
-      if (!Gerrit._preloadedPlugins.hasOwnProperty(name)) { continue; }
+    const Gerrit = window.Gerrit as GerritGlobal;
+    if (!Gerrit || !Gerrit._preloadedPlugins) {
+      return;
+    }
+    for (const name of Object.keys(Gerrit._preloadedPlugins)) {
       const callback = Gerrit._preloadedPlugins[name];
       this.install(callback, API_VERSION, PRELOADED_PROTOCOL + name);
     }
   }
 
-  isPluginPreloaded(pathOrUrl) {
+  isPluginPreloaded(pathOrUrl: string) {
     const url = this._urlFor(pathOrUrl);
     const name = getPluginNameFromUrl(url);
-    if (name && window.Gerrit._preloadedPlugins) {
-      return window.Gerrit._preloadedPlugins.hasOwnProperty(name);
+    const Gerrit = window.Gerrit as GerritGlobal;
+    if (name && Gerrit._preloadedPlugins) {
+      return hasOwnProperty(Gerrit._preloadedPlugins, name);
     } else {
       return false;
     }
@@ -289,10 +308,8 @@
 
   /**
    * Checks if given plugin path/url is enabled or not.
-   *
-   * @param {string} pathOrUrl
    */
-  isPluginEnabled(pathOrUrl) {
+  isPluginEnabled(pathOrUrl: string) {
     const url = this._urlFor(pathOrUrl);
     if (this.isPluginPreloaded(url)) return true;
     const key = this._getPluginKeyFromUrl(url);
@@ -301,54 +318,48 @@
 
   /**
    * Returns the plugin object with a given url.
-   *
-   * @param {string} pathOrUrl
    */
-  getPlugin(pathOrUrl) {
-    const key = this._getPluginKeyFromUrl(this._urlFor(pathOrUrl));
+  getPlugin(pathOrUrl: string) {
+    const url = this._urlFor(pathOrUrl);
+    const key = this._getPluginKeyFromUrl(url);
     return this._plugins.get(key);
   }
 
   /**
    * Checks if given plugin path/url is loaded or not.
-   *
-   * @param {string} pathOrUrl
    */
-  isPluginLoaded(pathOrUrl) {
+  isPluginLoaded(pathOrUrl: string): boolean {
     const url = this._urlFor(pathOrUrl);
     const key = this._getPluginKeyFromUrl(url);
-    return this._plugins.has(key) ?
-      this._plugins.get(key).state === PluginState.LOADED :
-      false;
+    return this._plugins.has(key)
+      ? this._plugins.get(key)!.state === PluginState.LOADED
+      : false;
   }
 
-  _importHtmlPlugin(pluginUrl, opts = {}) {
+  _importHtmlPlugin(pluginUrl: string, opts: PluginOption = {}) {
     const urlWithAP = this._urlFor(pluginUrl, window.ASSETS_PATH);
     const urlWithoutAP = this._urlFor(pluginUrl);
-    let onerror = null;
+    let onerror = undefined;
     if (urlWithAP !== urlWithoutAP) {
       onerror = () => this._loadHtmlPlugin(urlWithoutAP, opts.sync);
     }
     this._loadHtmlPlugin(urlWithAP, opts.sync, onerror);
   }
 
-  _loadHtmlPlugin(url, sync, onerror) {
+  _loadHtmlPlugin(url: string, sync?: boolean, onerror?: (e: Event) => void) {
     if (!onerror) {
       onerror = () => {
         this._failToLoad(`${url} import error`, url);
       };
     }
 
-    importHref(
-        url, () => {},
-        onerror,
-        !sync);
+    importHref(url, () => {}, onerror, !sync);
   }
 
-  _loadJsPlugin(pluginUrl) {
+  _loadJsPlugin(pluginUrl: string) {
     const urlWithAP = this._urlFor(pluginUrl, window.ASSETS_PATH);
     const urlWithoutAP = this._urlFor(pluginUrl);
-    let onerror = null;
+    let onerror = undefined;
     if (urlWithAP !== urlWithoutAP) {
       onerror = () => this._createScriptTag(urlWithoutAP);
     }
@@ -356,7 +367,7 @@
     this._createScriptTag(urlWithAP, onerror);
   }
 
-  _createScriptTag(url, onerror) {
+  _createScriptTag(url: string, onerror?: OnErrorEventHandler) {
     if (!onerror) {
       onerror = () => this._failToLoad(`${url} load error`, url);
     }
@@ -372,23 +383,24 @@
     return document.body.appendChild(el);
   }
 
-  _urlFor(pathOrUrl, assetsPath) {
-    if (!pathOrUrl) {
-      return pathOrUrl;
-    }
-
+  _urlFor(pathOrUrl: string, assetsPath?: string): string {
     // theme is per host, should always load from assetsPath
-    const isThemeFile = pathOrUrl.endsWith('static/gerrit-theme.html') ||
+    const isThemeFile =
+      pathOrUrl.endsWith('static/gerrit-theme.html') ||
       pathOrUrl.endsWith('static/gerrit-theme.js');
     const shouldTryLoadFromAssetsPathFirst = !isThemeFile && assetsPath;
-    if (pathOrUrl.startsWith(PRELOADED_PROTOCOL) ||
-        pathOrUrl.startsWith('http')) {
+    if (
+      pathOrUrl.startsWith(PRELOADED_PROTOCOL) ||
+      pathOrUrl.startsWith('http')
+    ) {
       // Plugins are loaded from another domain or preloaded.
-      if (pathOrUrl.includes(location.host) &&
-        shouldTryLoadFromAssetsPathFirst) {
+      if (
+        pathOrUrl.includes(location.host) &&
+        shouldTryLoadFromAssetsPathFirst &&
+        assetsPath
+      ) {
         // if is loading from host server, try replace with cdn when assetsPath provided
-        return pathOrUrl
-            .replace(location.origin, assetsPath);
+        return pathOrUrl.replace(location.origin, assetsPath);
       }
       return pathOrUrl;
     }
@@ -397,7 +409,7 @@
       pathOrUrl = '/' + pathOrUrl;
     }
 
-    if (shouldTryLoadFromAssetsPathFirst) {
+    if (shouldTryLoadFromAssetsPathFirst && assetsPath) {
       return assetsPath + pathOrUrl;
     }
 
@@ -412,39 +424,25 @@
       return Promise.resolve();
     }
     if (!this._loadingPromise) {
-      let timerId;
-      this._loadingPromise =
-        Promise.race([
-          new Promise(resolve => this._loadingResolver = resolve),
-          new Promise((_, reject) => timerId = setTimeout(
-              () => {
-                reject(new Error(this._timeout()));
-              }, PLUGIN_LOADING_TIMEOUT_MS)),
-        ]).finally(() => {
-          if (timerId) clearTimeout(timerId);
-        });
+      // TODO(TS): Should be a number, but TS thinks that is must be some weird
+      // NodeJS.Timeout object.
+      let timerId: any;
+      this._loadingPromise = Promise.race([
+        new Promise(resolve => (this._loadingResolver = resolve)),
+        new Promise(
+          (_, reject) =>
+            (timerId = setTimeout(() => {
+              reject(new Error(this._timeout()));
+            }, PLUGIN_LOADING_TIMEOUT_MS))
+        ),
+      ]).finally(() => {
+        if (timerId) clearTimeout(timerId);
+      }) as Promise<void>;
     }
     return this._loadingPromise;
   }
 }
 
-/**
- * @typedef {{
- *            name:string,
- *            url:string,
- *            state:PluginState,
- *            plugin:Object
- *          }}
- */
-PluginLoader.PluginObject;
-
-/**
- * @typedef {{
- *            sync:boolean,
- *          }}
- */
-PluginLoader.PluginOption;
-
 // TODO(dmfilippov): Convert to service and add to appContext
 export let pluginLoader = new PluginLoader();
 export function _testOnly_resetPluginLoader() {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
deleted file mode 100644
index 6c3ccc9..0000000
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ /dev/null
@@ -1,446 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {getBaseUrl} from '../../../utils/url-util.js';
-import {getSharedApiEl} from '../../../utils/dom-util.js';
-import {GrAttributeHelper} from '../../plugins/gr-attribute-helper/gr-attribute-helper.js';
-import {GrChangeActionsInterface} from './gr-change-actions-js-api.js';
-import {GrChangeReplyInterface} from './gr-change-reply-js-api.js';
-import {GrDomHooksManager} from '../../plugins/gr-dom-hooks/gr-dom-hooks.js';
-import {GrThemeApi} from '../../plugins/gr-theme-api/gr-theme-api.js';
-import {GrPopupInterface} from '../../plugins/gr-popup-interface/gr-popup-interface.js';
-import {GrAdminApi} from '../../plugins/gr-admin-api/gr-admin-api.js';
-import {GrAnnotationActionsInterface} from './gr-annotation-actions-js-api.js';
-import {GrChangeMetadataApi} from '../../plugins/gr-change-metadata-api/gr-change-metadata-api.js';
-import {GrEventHelper} from '../../plugins/gr-event-helper/gr-event-helper.js';
-import {GrPluginRestApi} from './gr-plugin-rest-api.js';
-import {GrRepoApi} from '../../plugins/gr-repo-api/gr-repo-api.js';
-import {GrSettingsApi} from '../../plugins/gr-settings-api/gr-settings-api.js';
-import {GrStylesApi} from '../../plugins/gr-styles-api/gr-styles-api.js';
-import {GrPluginActionContext} from './gr-plugin-action-context.js';
-import {getPluginEndpoints} from './gr-plugin-endpoints.js';
-
-import {
-  PRELOADED_PROTOCOL,
-  getPluginNameFromUrl,
-  send,
-} from './gr-api-utils.js';
-import {GrReporintJsApi} from './gr-reporting-js-api.js';
-
-const PANEL_ENDPOINTS_MAPPING = {
-  CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK: 'change-view-integration',
-  CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK: 'change-metadata-item',
-};
-
-/**
- * Plugin-provided custom components can affect content in extension
- * points using one of following methods:
- * - DECORATE: custom component is set with `content` attribute and may
- *   decorate (e.g. style) DOM element.
- * - REPLACE: contents of extension point are replaced with the custom
- *   component.
- * - STYLE: custom component is a shared styles module that is inserted
- *   into the extension point.
- */
-const EndpointType = {
-  DECORATE: 'decorate',
-  REPLACE: 'replace',
-  STYLE: 'style',
-};
-
-export class Plugin {
-  constructor(opt_url) {
-    this._domHooks = new GrDomHooksManager(this);
-
-    if (!opt_url) {
-      console.warn(
-          'Plugin not being loaded from /plugins base path.',
-          'Unable to determine name.'
-      );
-      return this;
-    }
-    this.deprecated = {
-      _loadedGwt: deprecatedAPI._loadedGwt.bind(this),
-      install: deprecatedAPI.install.bind(this),
-      onAction: deprecatedAPI.onAction.bind(this),
-      panel: deprecatedAPI.panel.bind(this),
-      popup: deprecatedAPI.popup.bind(this),
-      screen: deprecatedAPI.screen.bind(this),
-      settingsScreen: deprecatedAPI.settingsScreen.bind(this),
-    };
-
-    this._url = new URL(opt_url);
-    this._name = getPluginNameFromUrl(this._url);
-    this.sharedApiElement = getSharedApiEl();
-  }
-
-  getPluginName() {
-    return this._name;
-  }
-
-  registerStyleModule(endpoint, moduleName) {
-    getPluginEndpoints().registerModule(this, {
-      endpoint,
-      type: EndpointType.STYLE,
-      moduleName,
-    });
-  }
-
-  /**
-   * Registers an endpoint for the plugin.
-   */
-  registerCustomComponent(endpointName, opt_moduleName, opt_options) {
-    return this._registerCustomComponent(
-        endpointName,
-        opt_moduleName,
-        opt_options
-    );
-  }
-
-  /**
-   * Registers a dynamic endpoint for the plugin.
-   *
-   * Dynamic plugins are registered by specific prefix, such as
-   * 'change-list-header'.
-   */
-  registerDynamicCustomComponent(endpointName, opt_moduleName, opt_options) {
-    const fullEndpointName = `${endpointName}-${this.getPluginName()}`;
-    return this._registerCustomComponent(
-        fullEndpointName,
-        opt_moduleName,
-        opt_options,
-        endpointName
-    );
-  }
-
-  _registerCustomComponent(
-      endpoint,
-      opt_moduleName,
-      opt_options,
-      dynamicEndpoint
-  ) {
-    const type =
-      opt_options && opt_options.replace
-        ? EndpointType.REPLACE
-        : EndpointType.DECORATE;
-    const slot = (opt_options && opt_options.slot) || '';
-    const domHook = this._domHooks.getDomHook(endpoint, opt_moduleName);
-    const moduleName = opt_moduleName || domHook.getModuleName();
-    getPluginEndpoints().registerModule(this, {
-      slot,
-      endpoint,
-      type,
-      moduleName,
-      domHook,
-      dynamicEndpoint,
-    });
-    return domHook.getPublicAPI();
-  }
-
-  /**
-   * Returns instance of DOM hook API for endpoint. Creates a placeholder
-   * element for the first call.
-   */
-  hook(endpointName, opt_options) {
-    return this.registerCustomComponent(endpointName, undefined, opt_options);
-  }
-
-  getServerInfo() {
-    return document.createElement('gr-rest-api-interface').getConfig();
-  }
-
-  on(eventName, callback) {
-    this.sharedApiElement.addEventCallback(eventName, callback);
-  }
-
-  url(opt_path) {
-    const relPath = '/plugins/' + this._name + (opt_path || '/');
-    const sameOriginPath = window.location.origin + `${getBaseUrl()}${relPath}`;
-    if (window.location.origin === this._url.origin) {
-      // Plugin loaded from the same origin as gr-app, getBaseUrl in effect.
-      return sameOriginPath;
-    } else if (this._url.protocol === PRELOADED_PROTOCOL) {
-      // Plugin is preloaded, load plugin with ASSETS_PATH or location.origin
-      return window.ASSETS_PATH
-        ? `${window.ASSETS_PATH}${relPath}`
-        : sameOriginPath;
-    } else {
-      // Plugin loaded from assets bundle, expect assets placed along with it.
-      return this._url.href.split('/plugins/' + this._name)[0] + relPath;
-    }
-  }
-
-  screenUrl(opt_screenName) {
-    const origin = location.origin;
-    const base = getBaseUrl();
-    const tokenPart = opt_screenName ? '/' + opt_screenName : '';
-    return `${origin}${base}/x/${this.getPluginName()}${tokenPart}`;
-  }
-
-  _send(method, url, opt_callback, opt_payload) {
-    return send(method, this.url(url), opt_callback, opt_payload);
-  }
-
-  get(url, opt_callback) {
-    console.warn('.get() is deprecated! Use .restApi().get()');
-    return this._send('GET', url, opt_callback);
-  }
-
-  post(url, payload, opt_callback) {
-    console.warn('.post() is deprecated! Use .restApi().post()');
-    return this._send('POST', url, opt_callback, payload);
-  }
-
-  put(url, payload, opt_callback) {
-    console.warn('.put() is deprecated! Use .restApi().put()');
-    return this._send('PUT', url, opt_callback, payload);
-  }
-
-  delete(url, opt_callback) {
-    console.warn('.delete() is deprecated! Use plugin.restApi().delete()');
-    return this.restApi()
-        .delete(this.url(url))
-        .then(res => {
-          if (opt_callback) {
-            opt_callback(res);
-          }
-          return res;
-        });
-  }
-
-  annotationApi() {
-    return new GrAnnotationActionsInterface(this);
-  }
-
-  changeActions() {
-    return new GrChangeActionsInterface(
-        this,
-        this.sharedApiElement.getElement(
-            this.sharedApiElement.Element.CHANGE_ACTIONS
-        )
-    );
-  }
-
-  changeReply() {
-    return new GrChangeReplyInterface(this, this.sharedApiElement);
-  }
-
-  reporting() {
-    return new GrReporintJsApi(this);
-  }
-
-  theme() {
-    return new GrThemeApi(this);
-  }
-
-  project() {
-    return new GrRepoApi(this);
-  }
-
-  changeMetadata() {
-    return new GrChangeMetadataApi(this);
-  }
-
-  admin() {
-    return new GrAdminApi(this);
-  }
-
-  settings() {
-    return new GrSettingsApi(this);
-  }
-
-  styles() {
-    return new GrStylesApi();
-  }
-
-  /**
-   * To make REST requests for plugin-provided endpoints, use
-   *
-   * @example
-   * const pluginRestApi = plugin.restApi(plugin.url());
-   *
-   * @param {string=} opt_prefix url for subsequent .get(), .post() etc requests.
-   */
-  restApi(opt_prefix) {
-    return new GrPluginRestApi(opt_prefix);
-  }
-
-  attributeHelper(element) {
-    return new GrAttributeHelper(element);
-  }
-
-  eventHelper(element) {
-    return new GrEventHelper(element);
-  }
-
-  popup(moduleName) {
-    if (typeof moduleName !== 'string') {
-      console.error('.popup(element) deprecated, use .popup(moduleName)!');
-      return;
-    }
-    const api = new GrPopupInterface(this, moduleName);
-    return api.open();
-  }
-
-  panel() {
-    console.error(
-        '.panel() is deprecated! ' + 'Use registerCustomComponent() instead.'
-    );
-  }
-
-  settingsScreen() {
-    console.error(
-        '.settingsScreen() is deprecated! ' + 'Use .settings() instead.'
-    );
-  }
-
-  screen(screenName, opt_moduleName) {
-    if (opt_moduleName && typeof opt_moduleName !== 'string') {
-      console.error(
-          '.screen(pattern, callback) deprecated, use ' +
-          '.screen(screenName, opt_moduleName)!'
-      );
-      return;
-    }
-    return this.registerCustomComponent(
-        this._getScreenName(screenName),
-        opt_moduleName
-    );
-  }
-
-  _getScreenName(screenName) {
-    return `${this.getPluginName()}-screen-${screenName}`;
-  }
-}
-
-// TODO: should be removed soon after all core plugins moved away from it.
-const deprecatedAPI = {
-  _loadedGwt: () => {},
-
-  install() {
-    console.info('Installing deprecated APIs is deprecated!');
-    for (const method in this.deprecated) {
-      if (method === 'install') continue;
-      this[method] = this.deprecated[method];
-    }
-  },
-
-  popup(el) {
-    console.warn(
-        'plugin.deprecated.popup() is deprecated, '
-        + 'use plugin.popup() insted!'
-    );
-    if (!el) {
-      throw new Error('Popup contents not found');
-    }
-    const api = new GrPopupInterface(this);
-    api.open().then(api => api._getElement().appendChild(el));
-    return api;
-  },
-
-  onAction(type, action, callback) {
-    console.warn(
-        'plugin.deprecated.onAction() is deprecated,' +
-        ' use plugin.changeActions() instead!'
-    );
-    if (type !== 'change' && type !== 'revision') {
-      console.warn(`${type} actions are not supported.`);
-      return;
-    }
-    this.on('showchange', (change, revision) => {
-      const details = this.changeActions().getActionDetails(action);
-      if (!details) {
-        console.warn(
-            `${this.getPluginName()} onAction error: ${action} not found!`
-        );
-        return;
-      }
-      this.changeActions().addTapListener(details.__key, () => {
-        callback(new GrPluginActionContext(this, details, change, revision));
-      });
-    });
-  },
-
-  screen(pattern, callback) {
-    console.warn(
-        'plugin.deprecated.screen is deprecated,'
-        + ' use plugin.screen instead!'
-    );
-    if (pattern instanceof RegExp) {
-      console.error(
-          'deprecated.screen() does not support RegExp. ' +
-          'Please use strings for patterns.'
-      );
-      return;
-    }
-    this.hook(this._getScreenName(pattern)).onAttached(el => {
-      el.style.display = 'none';
-      callback({
-        body: el,
-        token: el.token,
-        onUnload: () => {},
-        setTitle: () => {},
-        setWindowTitle: () => {},
-        show: () => {
-          el.style.display = 'initial';
-        },
-      });
-    });
-  },
-
-  settingsScreen(path, menu, callback) {
-    console.warn('.settingsScreen() is deprecated! Use .settings() instead.');
-    const hook = this.settings().title(menu)
-        .token(path)
-        .module('div')
-        .build();
-    hook.onAttached(el => {
-      el.style.display = 'none';
-      const body = el.querySelector('div');
-      callback({
-        body,
-        onUnload: () => {},
-        setTitle: () => {},
-        setWindowTitle: () => {},
-        show: () => {
-          el.style.display = 'initial';
-        },
-      });
-    });
-  },
-
-  panel(extensionpoint, callback) {
-    console.warn(
-        '.panel() is deprecated! ' + 'Use registerCustomComponent() instead.'
-    );
-    const endpoint = PANEL_ENDPOINTS_MAPPING[extensionpoint];
-    if (!endpoint) {
-      console.warn(`.panel ${extensionpoint} not supported!`);
-      return;
-    }
-    this.hook(endpoint).onAttached(el =>
-      callback({
-        body: el,
-        p: {
-          CHANGE_INFO: el.change,
-          REVISION_INFO: el.revision,
-        },
-        onUnload: () => {},
-      })
-    );
-  },
-};
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
new file mode 100644
index 0000000..c6b3485
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
@@ -0,0 +1,506 @@
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {getBaseUrl} from '../../../utils/url-util';
+import {getSharedApiEl} from '../../../utils/dom-util';
+import {GrAttributeHelper} from '../../plugins/gr-attribute-helper/gr-attribute-helper';
+import {GrChangeActionsInterface} from './gr-change-actions-js-api';
+import {GrChangeReplyInterface} from './gr-change-reply-js-api';
+import {GrDomHooksManager} from '../../plugins/gr-dom-hooks/gr-dom-hooks';
+import {GrThemeApi} from '../../plugins/gr-theme-api/gr-theme-api';
+import {GrPopupInterface} from '../../plugins/gr-popup-interface/gr-popup-interface';
+import {GrAdminApi} from '../../plugins/gr-admin-api/gr-admin-api';
+import {GrAnnotationActionsInterface} from './gr-annotation-actions-js-api';
+import {GrChangeMetadataApi} from '../../plugins/gr-change-metadata-api/gr-change-metadata-api';
+import {GrEventHelper} from '../../plugins/gr-event-helper/gr-event-helper';
+import {GrPluginRestApi} from './gr-plugin-rest-api';
+import {GrRepoApi} from '../../plugins/gr-repo-api/gr-repo-api';
+import {GrSettingsApi} from '../../plugins/gr-settings-api/gr-settings-api';
+import {GrStylesApi} from '../../plugins/gr-styles-api/gr-styles-api';
+import {GrPluginActionContext} from './gr-plugin-action-context';
+import {getPluginEndpoints} from './gr-plugin-endpoints';
+
+import {PRELOADED_PROTOCOL, getPluginNameFromUrl, send} from './gr-api-utils';
+import {GrReporintJsApi} from './gr-reporting-js-api';
+import {
+  EventType,
+  HookApi,
+  PanelInfo,
+  PluginApi,
+  PluginDeprecatedApi,
+  RegisterOptions,
+  SettingsInfo,
+  TargetElement,
+} from '../../plugins/gr-plugin-types';
+import {ActionInfo, RequestPayload} from '../../../types/common';
+import {HttpMethod} from '../../../constants/constants';
+import {JsApiService} from './gr-js-api-types';
+import {GrChangeActions} from '../../../services/services/gr-rest-api/gr-rest-api';
+
+const PANEL_ENDPOINTS_MAPPING = {
+  CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK: 'change-view-integration',
+  CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK: 'change-metadata-item',
+};
+
+/**
+ * Plugin-provided custom components can affect content in extension
+ * points using one of following methods:
+ * - DECORATE: custom component is set with `content` attribute and may
+ *   decorate (e.g. style) DOM element.
+ * - REPLACE: contents of extension point are replaced with the custom
+ *   component.
+ * - STYLE: custom component is a shared styles module that is inserted
+ *   into the extension point.
+ */
+enum EndpointType {
+  DECORATE = 'decorate',
+  REPLACE = 'replace',
+  STYLE = 'style',
+}
+
+const PLUGIN_NAME_NOT_SET = 'NULL';
+
+export type SendCallback = (response: unknown) => void;
+
+export class Plugin implements PluginApi {
+  readonly deprecated: PluginDeprecatedApi;
+
+  readonly _url?: URL;
+
+  private _domHooks: GrDomHooksManager;
+
+  private readonly _name: string = PLUGIN_NAME_NOT_SET;
+
+  // TODO(TS): Change type to GrJsApiInterface
+  private readonly sharedApiElement: JsApiService;
+
+  constructor(url?: string) {
+    this.deprecated = {
+      _loadedGwt: () => {},
+      install: () => this.deprecatedInstall(),
+      onAction: (
+        type: string,
+        action: string,
+        callback: (ctx: GrPluginActionContext) => void
+      ) => this.deprecatedOnAction(type, action, callback),
+      panel: (extensionpoint: string, callback: (panel: PanelInfo) => void) =>
+        this.deprecatedPanel(extensionpoint, callback),
+      popup: (el: Element) => this.deprecatedPopup(el),
+      screen: (pattern: string, callback: (settings: SettingsInfo) => void) =>
+        this.deprecatedScreen(pattern, callback),
+      settingsScreen: (
+        path: string,
+        menu: string,
+        callback: (settings: SettingsInfo) => void
+      ) => this.deprecatedSettingsScreen(path, menu, callback),
+    };
+    this.sharedApiElement = getSharedApiEl();
+    this._domHooks = new GrDomHooksManager(this);
+
+    if (!url) {
+      console.warn(
+        'Plugin not being loaded from /plugins base path.',
+        'Unable to determine name.'
+      );
+      return this;
+    }
+
+    this._url = new URL(url);
+    this._name = getPluginNameFromUrl(this._url) ?? 'NULL';
+  }
+
+  getPluginName() {
+    return this._name;
+  }
+
+  registerStyleModule(endpoint: string, moduleName: string) {
+    getPluginEndpoints().registerModule(this, {
+      endpoint,
+      type: EndpointType.STYLE,
+      moduleName,
+    });
+  }
+
+  /**
+   * Registers an endpoint for the plugin.
+   */
+  registerCustomComponent(
+    endpointName: string,
+    moduleName?: string,
+    options?: RegisterOptions
+  ): HookApi {
+    return this._registerCustomComponent(endpointName, moduleName, options);
+  }
+
+  /**
+   * Registers a dynamic endpoint for the plugin.
+   *
+   * Dynamic plugins are registered by specific prefix, such as
+   * 'change-list-header'.
+   */
+  registerDynamicCustomComponent(
+    endpointName: string,
+    moduleName?: string,
+    options?: RegisterOptions
+  ): HookApi {
+    const fullEndpointName = `${endpointName}-${this.getPluginName()}`;
+    return this._registerCustomComponent(
+      fullEndpointName,
+      moduleName,
+      options,
+      endpointName
+    );
+  }
+
+  _registerCustomComponent(
+    endpoint: string,
+    moduleName?: string,
+    options?: RegisterOptions,
+    dynamicEndpoint?: string
+  ): HookApi {
+    const type =
+      options && options.replace ? EndpointType.REPLACE : EndpointType.DECORATE;
+    const slot = (options && options.slot) || '';
+    const domHook = this._domHooks.getDomHook(endpoint, moduleName);
+    moduleName = moduleName || domHook.getModuleName();
+    getPluginEndpoints().registerModule(this, {
+      slot,
+      endpoint,
+      type,
+      moduleName,
+      domHook,
+      dynamicEndpoint,
+    });
+    return domHook;
+  }
+
+  /**
+   * Returns instance of DOM hook API for endpoint. Creates a placeholder
+   * element for the first call.
+   */
+  hook(endpointName: string, options?: RegisterOptions) {
+    return this.registerCustomComponent(endpointName, undefined, options);
+  }
+
+  getServerInfo() {
+    return document.createElement('gr-rest-api-interface').getConfig();
+  }
+
+  on(eventName: EventType, callback: (...args: any[]) => any) {
+    this.sharedApiElement.addEventCallback(eventName, callback);
+  }
+
+  url(path?: string) {
+    if (!this._url) throw new Error('plugin url not set');
+    const relPath = '/plugins/' + this._name + (path || '/');
+    const sameOriginPath = window.location.origin + `${getBaseUrl()}${relPath}`;
+    if (window.location.origin === this._url.origin) {
+      // Plugin loaded from the same origin as gr-app, getBaseUrl in effect.
+      return sameOriginPath;
+    } else if (this._url.protocol === PRELOADED_PROTOCOL) {
+      // Plugin is preloaded, load plugin with ASSETS_PATH or location.origin
+      return window.ASSETS_PATH
+        ? `${window.ASSETS_PATH}${relPath}`
+        : sameOriginPath;
+    } else {
+      // Plugin loaded from assets bundle, expect assets placed along with it.
+      return this._url.href.split('/plugins/' + this._name)[0] + relPath;
+    }
+  }
+
+  screenUrl(screenName?: string) {
+    const origin = location.origin;
+    const base = getBaseUrl();
+    const tokenPart = screenName ? '/' + screenName : '';
+    return `${origin}${base}/x/${this.getPluginName()}${tokenPart}`;
+  }
+
+  _send(
+    method: HttpMethod,
+    url: string,
+    callback?: SendCallback,
+    payload?: RequestPayload
+  ) {
+    return send(method, this.url(url), callback, payload);
+  }
+
+  get(url: string, callback?: SendCallback) {
+    console.warn('.get() is deprecated! Use .restApi().get()');
+    return this._send(HttpMethod.GET, url, callback);
+  }
+
+  post(url: string, payload: RequestPayload, callback?: SendCallback) {
+    console.warn('.post() is deprecated! Use .restApi().post()');
+    return this._send(HttpMethod.POST, url, callback, payload);
+  }
+
+  put(url: string, payload: RequestPayload, callback?: SendCallback) {
+    console.warn('.put() is deprecated! Use .restApi().put()');
+    return this._send(HttpMethod.PUT, url, callback, payload);
+  }
+
+  delete(url: string, callback?: SendCallback) {
+    console.warn('.delete() is deprecated! Use plugin.restApi().delete()');
+    return this.restApi()
+      .delete(this.url(url))
+      .then(res => {
+        if (callback) callback(res);
+        return res;
+      });
+  }
+
+  annotationApi() {
+    return new GrAnnotationActionsInterface(this);
+  }
+
+  changeActions() {
+    return new GrChangeActionsInterface(
+      this,
+      (this.sharedApiElement.getElement(
+        TargetElement.CHANGE_ACTIONS
+      ) as unknown) as GrChangeActions
+    );
+  }
+
+  changeReply() {
+    return new GrChangeReplyInterface(this, this.sharedApiElement);
+  }
+
+  reporting() {
+    return new GrReporintJsApi(this);
+  }
+
+  theme() {
+    return new GrThemeApi(this);
+  }
+
+  project() {
+    return new GrRepoApi(this);
+  }
+
+  changeMetadata() {
+    return new GrChangeMetadataApi(this);
+  }
+
+  admin() {
+    return new GrAdminApi(this);
+  }
+
+  settings() {
+    return new GrSettingsApi(this);
+  }
+
+  styles() {
+    return new GrStylesApi();
+  }
+
+  /**
+   * To make REST requests for plugin-provided endpoints, use
+   *
+   * @example
+   * const pluginRestApi = plugin.restApi(plugin.url());
+   * @param prefix url for subsequent .get(), .post() etc requests.
+   */
+  restApi(prefix?: string) {
+    return new GrPluginRestApi(prefix);
+  }
+
+  attributeHelper(element: HTMLElement) {
+    return new GrAttributeHelper(element);
+  }
+
+  eventHelper(element: HTMLElement) {
+    return new GrEventHelper(element);
+  }
+
+  popup(moduleName: string) {
+    if (typeof moduleName !== 'string') {
+      console.error('.popup(element) deprecated, use .popup(moduleName)!');
+      return;
+    }
+    const api = new GrPopupInterface(this, moduleName);
+    return api.open();
+  }
+
+  panel() {
+    console.error(
+      '.panel() is deprecated! ' + 'Use registerCustomComponent() instead.'
+    );
+  }
+
+  settingsScreen() {
+    console.error(
+      '.settingsScreen() is deprecated! ' + 'Use .settings() instead.'
+    );
+  }
+
+  screen(screenName: string, moduleName?: string) {
+    if (moduleName && typeof moduleName !== 'string') {
+      console.error(
+        '.screen(pattern, callback) deprecated, use ' +
+          '.screen(screenName, moduleName)!'
+      );
+      return;
+    }
+    return this.registerCustomComponent(
+      this._getScreenName(screenName),
+      moduleName
+    );
+  }
+
+  _getScreenName(screenName: string) {
+    return `${this.getPluginName()}-screen-${screenName}`;
+  }
+
+  // !!! DEPRECATED !!!
+  // All methods below are deprecated!
+  // TODO: should be removed soon after all core plugins moved away from it.
+
+  deprecatedInstall() {
+    console.info('Installing deprecated APIs is deprecated!');
+    const deprecatedThis = (this as unknown) as PluginDeprecatedApi;
+    deprecatedThis._loadedGwt = this.deprecated._loadedGwt;
+    deprecatedThis.onAction = this.deprecated.onAction;
+    deprecatedThis.panel = this.deprecated.panel;
+    deprecatedThis.popup = this.deprecated.popup;
+    deprecatedThis.screen = this.deprecated.screen;
+    deprecatedThis.settingsScreen = this.deprecated.settingsScreen;
+  }
+
+  deprecatedPopup(el: Element): GrPopupInterface {
+    console.warn(
+      'plugin.deprecated.popup() is deprecated, ' + 'use plugin.popup() insted!'
+    );
+    if (!el) {
+      throw new Error('Popup contents not found');
+    }
+    const api = new GrPopupInterface(this);
+    api.open().then(api => {
+      const popupEl = api._getElement();
+      if (!popupEl) {
+        throw new Error('Popup element not found');
+      }
+      popupEl.appendChild(el);
+    });
+    return api;
+  }
+
+  deprecatedOnAction(
+    type: string,
+    action: string,
+    callback: (ctx: GrPluginActionContext) => void
+  ) {
+    console.warn(
+      'plugin.deprecated.onAction() is deprecated,' +
+        ' use plugin.changeActions() instead!'
+    );
+    if (type !== 'change' && type !== 'revision') {
+      console.warn(`${type} actions are not supported.`);
+      return;
+    }
+    this.on(EventType.SHOW_CHANGE, (change, revision) => {
+      const details: ActionInfo = this.changeActions().getActionDetails(action);
+      if (!details) {
+        console.warn(
+          `${this.getPluginName()} onAction error: ${action} not found!`
+        );
+        return;
+      }
+      if (!details.__key) {
+        console.warn(
+          `${this.getPluginName()} onAction error: ${action} has no key!`
+        );
+        return;
+      }
+      this.changeActions().addTapListener(details.__key, () => {
+        callback(new GrPluginActionContext(this, details, change, revision));
+      });
+    });
+  }
+
+  deprecatedScreen(
+    pattern: string,
+    callback: (settings: SettingsInfo) => void
+  ) {
+    console.warn(
+      'plugin.deprecated.screen is deprecated,' + ' use plugin.screen instead!'
+    );
+    this.hook(this._getScreenName(pattern)).onAttached(el => {
+      el.style.display = 'none';
+      callback({
+        body: el,
+        token: el.token,
+        onUnload: () => {},
+        setTitle: () => {},
+        setWindowTitle: () => {},
+        show: () => {
+          el.style.display = 'initial';
+        },
+      });
+    });
+  }
+
+  deprecatedSettingsScreen(
+    path: string,
+    menu: string,
+    callback: (settings: SettingsInfo) => void
+  ) {
+    console.warn('.settingsScreen() is deprecated! Use .settings() instead.');
+    const hook = this.settings().title(menu).token(path).module('div').build();
+    hook.onAttached(el => {
+      el.style.display = 'none';
+      const body = el.querySelector('div');
+      if (!body) return;
+      callback({
+        body,
+        onUnload: () => {},
+        setTitle: () => {},
+        setWindowTitle: () => {},
+        show: () => {
+          el.style.display = 'initial';
+        },
+      });
+    });
+  }
+
+  deprecatedPanel(
+    extensionpoint: string,
+    callback: (panel: PanelInfo) => void
+  ) {
+    console.warn(
+      '.panel() is deprecated! ' + 'Use registerCustomComponent() instead.'
+    );
+    let endpoint;
+    for (const [key, value] of Object.entries(PANEL_ENDPOINTS_MAPPING)) {
+      if (key === extensionpoint) endpoint = value;
+    }
+    if (!endpoint) {
+      console.warn(`.panel ${extensionpoint} not supported!`);
+      return;
+    }
+    this.hook(endpoint).onAttached(el =>
+      callback({
+        body: el,
+        p: {
+          CHANGE_INFO: el.change,
+          REVISION_INFO: el.revision,
+        },
+        onUnload: () => {},
+      })
+    );
+  }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
index 2d1d3ac..f6bb0e3 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
@@ -19,6 +19,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-lib-loader_html.js';
+import {EventType} from '../../plugins/gr-plugin-types.js';
 
 // preloaded in PolyGerritIndexHtml.soy
 const HLJS_PATH = 'bower_components/highlightjs/highlight.min.js';
@@ -79,7 +80,7 @@
   _onHLJSLibLoaded() {
     const lib = this._getHighlightLib();
     this._hljsState.loading = false;
-    this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.HIGHLIGHTJS_LOADED, {
+    this.$.jsAPI.handleEvent(EventType.HIGHLIGHTJS_LOADED, {
       hljs: lib,
     });
     for (const cb of this._hljsState.callbacks) {
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth_test.js b/polygerrit-ui/app/services/gr-auth/gr-auth_test.js
index 432518c..80938ad 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth_test.js
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_test.js
@@ -144,8 +144,7 @@
         assert.equal(auth.status, Auth.STATUS.AUTHED);
         clock.tick(1000 * 10000);
         fakeFetch.returns(Promise.resolve({status: 403}));
-        const emitStub = sinon.stub();
-        appContext.eventEmitter.emit = emitStub;
+        const emitStub = sinon.stub(appContext.eventEmitter, 'emit');
         auth.authCheck().then(authed2 => {
           assert.isFalse(authed2);
           assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
@@ -162,8 +161,7 @@
         assert.equal(auth.status, Auth.STATUS.AUTHED);
         clock.tick(1000 * 10000);
         fakeFetch.returns(Promise.reject(new Error('random error')));
-        const emitStub = sinon.stub();
-        appContext.eventEmitter.emit = emitStub;
+        const emitStub = sinon.stub(appContext.eventEmitter, 'emit');
         auth.authCheck().then(authed2 => {
           assert.isFalse(authed2);
           assert.isTrue(emitStub.called);
@@ -180,8 +178,7 @@
         assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
         clock.tick(1000 * 10000);
         fakeFetch.returns(Promise.resolve({status: 204}));
-        const emitStub = sinon.stub();
-        appContext.eventEmitter.emit = emitStub;
+        const emitStub = sinon.stub(appContext.eventEmitter, 'emit');
         auth.authCheck().then(authed2 => {
           assert.isTrue(authed2);
           assert.isFalse(emitStub.called);
@@ -198,8 +195,7 @@
         assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
         clock.tick(1000 * 10000);
         fakeFetch.returns(Promise.reject(new Error('random error')));
-        const emitStub = sinon.stub();
-        appContext.eventEmitter.emit = emitStub;
+        const emitStub = sinon.stub(appContext.eventEmitter, 'emit');
         auth.authCheck().then(authed2 => {
           assert.isFalse(authed2);
           assert.isFalse(emitStub.called);
diff --git a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.js b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.js
index 6d0ab7b..32590e0 100644
--- a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.js
+++ b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.js
@@ -18,60 +18,58 @@
 import '../../test/common-test-setup-karma.js';
 import '../../elements/shared/gr-js-api-interface/gr-js-api-interface.js';
 import {EventEmitter} from './gr-event-interface_impl.js';
-import {_testOnly_initGerritPluginApi} from '../../elements/shared/gr-js-api-interface/gr-gerrit.js';
 
 const basicFixture = fixtureFromElement('gr-js-api-interface');
 
-const pluginApi = _testOnly_initGerritPluginApi();
-
 suite('gr-event-interface tests', () => {
+  let gerrit;
   setup(() => {
-
+    gerrit = window.Gerrit;
   });
 
   suite('test on Gerrit', () => {
     setup(() => {
       basicFixture.instantiate();
-      pluginApi.removeAllListeners();
+      gerrit.removeAllListeners();
     });
 
     test('communicate between plugin and Gerrit', done => {
       const eventName = 'test-plugin-event';
       let p;
-      pluginApi.on(eventName, e => {
+      gerrit.on(eventName, e => {
         assert.equal(e.value, 'test');
         assert.equal(e.plugin, p);
         done();
       });
-      pluginApi.install(plugin => {
+      gerrit.install(plugin => {
         p = plugin;
-        pluginApi.emit(eventName, {value: 'test', plugin});
+        gerrit.emit(eventName, {value: 'test', plugin});
       }, '0.1',
       'http://test.com/plugins/testplugin/static/test.js');
     });
 
     test('listen on events from core', done => {
       const eventName = 'test-plugin-event';
-      pluginApi.on(eventName, e => {
+      gerrit.on(eventName, e => {
         assert.equal(e.value, 'test');
         done();
       });
 
-      pluginApi.emit(eventName, {value: 'test'});
+      gerrit.emit(eventName, {value: 'test'});
     });
 
     test('communicate across plugins', done => {
       const eventName = 'test-plugin-event';
-      pluginApi.install(plugin => {
-        pluginApi.on(eventName, e => {
+      gerrit.install(plugin => {
+        gerrit.on(eventName, e => {
           assert.equal(e.plugin.getPluginName(), 'testB');
           done();
         });
       }, '0.1',
       'http://test.com/plugins/testA/static/testA.js');
 
-      pluginApi.install(plugin => {
-        pluginApi.emit(eventName, {plugin});
+      gerrit.install(plugin => {
+        gerrit.emit(eventName, {plugin});
       }, '0.1',
       'http://test.com/plugins/testB/static/testB.js');
     });
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 9b86ae9..de96db3 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
@@ -81,11 +81,6 @@
 export type ErrorCallback = (response?: Response | null, err?: Error) => void;
 export type CancelConditionCallback = () => boolean;
 
-export enum ApiElement {
-  CHANGE_ACTIONS = 'changeactions',
-  REPLY_DIALOG = 'replydialog',
-}
-
 // TODO(TS): remove when GrReplyDialog converted to typescript
 export interface GrReplyDialog {
   getLabelValue(label: string): string;
@@ -130,17 +125,6 @@
   getActionDetails(actionName: string): ActionInfo;
 }
 
-export interface RestApiTagNameMap {
-  [ApiElement.REPLY_DIALOG]: GrReplyDialog;
-  [ApiElement.CHANGE_ACTIONS]: GrChangeActions;
-}
-
-export interface JsApiService {
-  getElement<K extends keyof RestApiTagNameMap>(
-    elementKey: K
-  ): RestApiTagNameMap[K];
-}
-
 export interface GetDiffCommentsOutput {
   baseComments: CommentInfo[];
   comments: CommentInfo[];
diff --git a/polygerrit-ui/app/test/common-test-setup.js b/polygerrit-ui/app/test/common-test-setup.js
index 19465a3..004daf7 100644
--- a/polygerrit-ui/app/test/common-test-setup.js
+++ b/polygerrit-ui/app/test/common-test-setup.js
@@ -18,10 +18,10 @@
 // TODO(dmfilippov): remove bundled-polymer.js imports when the following issue
 // https://github.com/Polymer/polymer-resin/issues/9 is resolved.
 import '../scripts/bundled-polymer.js';
-import './test-app-context-init.js';
 import 'polymer-resin/standalone/polymer-resin.js';
 import '@polymer/iron-test-helpers/iron-test-helpers.js';
 import './test-router.js';
+import {_testOnlyInitAppContext} from './test-app-context-init';
 import {_testOnly_resetPluginLoader} from '../elements/shared/gr-js-api-interface/gr-plugin-loader.js';
 import {_testOnlyResetRestApi} from '../elements/shared/gr-js-api-interface/gr-plugin-rest-api.js';
 import {_testOnlyResetGrRestApiSharedObjects} from '../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -30,6 +30,7 @@
 import {_testOnly_getShortcutManagerInstance} from '../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 import sinon from 'sinon/pkg/sinon-esm.js';
 import {safeTypesBridge} from '../utils/safe-types-util.js';
+import {_testOnly_initGerritPluginApi} from '../elements/shared/gr-js-api-interface/gr-gerrit.js';
 window.sinon = sinon;
 
 security.polymer_resin.install({
@@ -65,6 +66,8 @@
   // The following calls is nessecary to avoid influence of previously executed
   // tests.
   TestKeyboardShortcutBinder.push();
+  _testOnlyInitAppContext();
+  _testOnly_initGerritPluginApi();
   const mgr = _testOnly_getShortcutManagerInstance();
   assert.equal(mgr.activeHosts.size, 0);
   assert.equal(mgr.listeners.size, 0);
diff --git a/polygerrit-ui/app/test/test-app-context-init.js b/polygerrit-ui/app/test/test-app-context-init.js
index 88232cd3..68e68f0 100644
--- a/polygerrit-ui/app/test/test-app-context-init.js
+++ b/polygerrit-ui/app/test/test-app-context-init.js
@@ -20,13 +20,15 @@
 import {grReportingMock} from '../services/gr-reporting/gr-reporting_mock.js';
 import {appContext} from '../services/app-context.js';
 
-initAppContext();
+export function _testOnlyInitAppContext() {
+  initAppContext();
 
-function setMock(serviceName, setupMock) {
-  Object.defineProperty(appContext, serviceName, {
-    get() {
-      return setupMock;
-    },
-  });
+  function setMock(serviceName, setupMock) {
+    Object.defineProperty(appContext, serviceName, {
+      get() {
+        return setupMock;
+      },
+    });
+  }
+  setMock('reportingService', grReportingMock);
 }
-setMock('reportingService', grReportingMock);
diff --git a/polygerrit-ui/app/test/test-utils.js b/polygerrit-ui/app/test/test-utils.js
index e1eadef..12a10d6 100644
--- a/polygerrit-ui/app/test/test-utils.js
+++ b/polygerrit-ui/app/test/test-utils.js
@@ -123,3 +123,17 @@
   }
   return change;
 }
+
+/**
+ * Forcing an opacity of 0 onto the ironOverlayBackdrop is required, because
+ * otherwise the backdrop stays around in the DOM for too long waiting for
+ * an animation to finish. This could be considered to be moved to a
+ * common-test-setup file.
+ */
+export function createIronOverlayBackdropStyleEl() {
+  const ironOverlayBackdropStyleEl = document.createElement('style');
+  document.head.appendChild(ironOverlayBackdropStyleEl);
+  ironOverlayBackdropStyleEl.sheet.insertRule(
+      'body { --iron-overlay-backdrop-opacity: 0; }');
+  return ironOverlayBackdropStyleEl;
+}
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index dcbfcca..f453c4a 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -367,6 +367,8 @@
  * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#action-info
  */
 export interface ActionInfo {
+  __key?: string;
+  __url?: string;
   method?: HttpMethod; // Most actions use POST, PUT or DELETE to cause state changes.
   label?: string; // Short title to display to a user describing the action
   title?: string; // Longer text to display describing the action
diff --git a/polygerrit-ui/app/types/types.ts b/polygerrit-ui/app/types/types.ts
index b54c67a..ece3d3a 100644
--- a/polygerrit-ui/app/types/types.ts
+++ b/polygerrit-ui/app/types/types.ts
@@ -16,6 +16,10 @@
  */
 import {Side} from '../constants/constants';
 
+export function notUndefined<T>(x: T | undefined): x is T {
+  return x !== undefined;
+}
+
 export interface CoverageRange {
   type: CoverageType;
   side: Side;
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index 0def6ad..ae6d616 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -17,6 +17,7 @@
 
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
 import {EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
+import {JsApiService} from '../elements/shared/gr-js-api-interface/gr-js-api-types';
 
 /**
  * Event emitted from polymer elements.
@@ -236,18 +237,19 @@
 }
 
 // shared API element
-// TODO: once gr-js-api-interface moved to ts
-// use GrJsApiInterface instead
-let _sharedApiEl: Element;
+// TODO: Make this a proper service singleton. Move into AppContext?
+let _sharedApiEl: JsApiService;
 
 /**
  * Retrieves the shared API element.
  * We want to keep a single instance of API element instead of
  * creating multiple elements.
  */
-export function getSharedApiEl() {
+export function getSharedApiEl(): JsApiService {
   if (!_sharedApiEl) {
-    _sharedApiEl = document.createElement('gr-js-api-interface');
+    _sharedApiEl = (document.createElement(
+      'gr-js-api-interface'
+    ) as unknown) as JsApiService;
   }
   return _sharedApiEl;
 }