Convert gr-repo-list to lit

Change-Id: I160d92d65b81e516b6b2bf250a268b273f5bebb3
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts
index a444067..8e50b5f 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts
@@ -14,24 +14,27 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../styles/gr-table-styles';
-import '../../../styles/shared-styles';
 import '../../shared/gr-dialog/gr-dialog';
 import '../../shared/gr-list-view/gr-list-view';
 import '../../shared/gr-overlay/gr-overlay';
 import '../gr-create-repo-dialog/gr-create-repo-dialog';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-repo-list_html';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {customElement, property, observe, computed} from '@polymer/decorators';
 import {AppElementAdminParams} from '../../gr-app-types';
 import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
-import {RepoName, ProjectInfoWithName} from '../../../types/common';
+import {
+  RepoName,
+  ProjectInfoWithName,
+  WebLinkInfo,
+} from '../../../types/common';
 import {GrCreateRepoDialog} from '../gr-create-repo-dialog/gr-create-repo-dialog';
 import {ProjectState, SHOWN_ITEMS_COUNT} from '../../../constants/constants';
 import {fireTitleChange} from '../../../utils/event-util';
 import {appContext} from '../../../services/app-context';
 import {encodeURL, getBaseUrl} from '../../../utils/url-util';
+import {tableStyles} from '../../../styles/gr-table-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, css, html} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -39,154 +42,259 @@
   }
 }
 
-export interface GrRepoList {
-  $: {
-    createOverlay: GrOverlay;
-    createNewModal: GrCreateRepoDialog;
-  };
-}
-
 @customElement('gr-repo-list')
-export class GrRepoList extends PolymerElement {
-  static get template() {
-    return htmlTemplate;
-  }
+export class GrRepoList extends LitElement {
+  @query('#createOverlay')
+  createOverlay?: GrOverlay;
+
+  @query('#createNewModal')
+  createNewModal?: GrCreateRepoDialog;
 
   @property({type: Object})
   params?: AppElementAdminParams;
 
-  @property({type: Number})
-  _offset = 0;
+  @state() offset = 0;
 
-  @property({type: String})
-  readonly _path = '/admin/repos';
+  @state() newRepoName = false;
 
-  @property({type: Boolean})
-  _newRepoName = false;
+  @state() createNewCapability = false;
 
-  @property({type: Boolean})
-  _createNewCapability = false;
+  @state() repos: ProjectInfoWithName[] = [];
 
-  @property({type: Array})
-  _repos: ProjectInfoWithName[] = [];
+  @state() reposPerPage = 25;
 
-  @property({type: Number})
-  _reposPerPage = 25;
+  @state() loading = true;
 
-  @property({type: Boolean})
-  _loading = true;
+  @state() filter = '';
 
-  @property({type: String})
-  _filter = '';
-
-  @computed('_repos')
-  get _shownRepos() {
-    return this._repos.slice(0, SHOWN_ITEMS_COUNT);
-  }
+  @state() readonly path = '/admin/repos';
 
   private readonly restApiService = appContext.restApiService;
 
-  override connectedCallback() {
+  override async connectedCallback() {
     super.connectedCallback();
-    this._getCreateRepoCapability();
+    await this.getCreateRepoCapability();
     fireTitleChange(this, 'Repos');
-    this._maybeOpenCreateOverlay(this.params);
+    this.maybeOpenCreateOverlay(this.params);
   }
 
-  @observe('params')
-  _paramsChanged(params: AppElementAdminParams) {
-    this._loading = true;
-    this._filter = params?.filter ?? '';
-    this._offset = Number(params?.offset ?? 0);
+  static override get styles() {
+    return [
+      tableStyles,
+      sharedStyles,
+      css`
+        .genericList tr td:last-of-type {
+          text-align: left;
+        }
+        .genericList tr th:last-of-type {
+          text-align: left;
+        }
+        .readOnly {
+          text-align: center;
+        }
+        .changesLink,
+        .name,
+        .repositoryBrowser,
+        .readOnly {
+          white-space: nowrap;
+        }
+      `,
+    ];
+  }
 
-    return this._getRepos(this._filter, this._reposPerPage, this._offset);
+  override render() {
+    return html`
+      <gr-list-view
+        .createNew=${this.createNewCapability}
+        .filter=${this.filter}
+        .itemsPerPage=${this.reposPerPage}
+        .items=${this.repos}
+        .loading=${this.loading}
+        .offset=${this.offset}
+        .path=${this.path}
+        @create-clicked=${this.handleCreateClicked}
+      >
+        <table id="list" class="genericList">
+          <tbody>
+            <tr class="headerRow">
+              <th class="name topHeader">Repository Name</th>
+              <th class="repositoryBrowser topHeader">Repository Browser</th>
+              <th class="changesLink topHeader">Changes</th>
+              <th class="topHeader readOnly">Read only</th>
+              <th class="description topHeader">Repository Description</th>
+            </tr>
+            <tr
+              id="loading"
+              class="loadingMsg ${this.computeLoadingClass(this.loading)}"
+            >
+              <td>Loading...</td>
+            </tr>
+          </tbody>
+          <tbody class="${this.computeLoadingClass(this.loading)}">
+            ${this.renderRepoList()}
+          </tbody>
+        </table>
+      </gr-list-view>
+      <gr-overlay id="createOverlay" with-backdrop>
+        <gr-dialog
+          id="createDialog"
+          class="confirmDialog"
+          ?disabled=${!this.newRepoName}
+          confirm-label="Create"
+          @confirm=${this.handleCreateRepo}
+          @cancel=${this.handleCloseCreate}
+        >
+          <div class="header" slot="header">Create Repository</div>
+          <div class="main" slot="main">
+            <gr-create-repo-dialog
+              id="createNewModal"
+              @new-repo-name=${this.handleNewRepoName}
+            ></gr-create-repo-dialog>
+          </div>
+        </gr-dialog>
+      </gr-overlay>
+    `;
+  }
+
+  private renderRepoList() {
+    const shownRepos = this.repos.slice(0, SHOWN_ITEMS_COUNT);
+    return shownRepos.map(item => this.renderRepo(item));
+  }
+
+  private renderRepo(item: ProjectInfoWithName) {
+    return html`
+      <tr class="table">
+        <td class="name">
+          <a href="${this.computeRepoUrl(item.name)}">${item.name}</a>
+        </td>
+        <td class="repositoryBrowser">${this.renderWebLinks(item)}</td>
+        <td class="changesLink">
+          <a href="${this.computeChangesLink(item.name)}">view all</a>
+        </td>
+        <td class="readOnly">
+          ${item.state === ProjectState.READ_ONLY ? 'Y' : ''}
+        </td>
+        <td class="description">${item.description}</td>
+      </tr>
+    `;
+  }
+
+  private renderWebLinks(links: ProjectInfoWithName) {
+    const webLinks = links.web_links ? links.web_links : [];
+    return webLinks.map(link => this.renderWebLink(link));
+  }
+
+  private renderWebLink(link: WebLinkInfo) {
+    return html`
+      <a href="${link.url}" class="webLink" rel="noopener" target="_blank">
+        ${link.name}
+      </a>
+    `;
+  }
+
+  override willUpdate(changedProperties: PropertyValues) {
+    if (changedProperties.has('params')) {
+      this._paramsChanged();
+    }
+  }
+
+  async _paramsChanged() {
+    const params = this.params;
+    this.loading = true;
+    this.filter = params?.filter ?? '';
+    this.offset = Number(params?.offset ?? 0);
+
+    return await this.getRepos();
   }
 
   /**
-   * Opens the create overlay if the route has a hash 'create'
+   * Opens the create overlay if the route has a hash 'create'.
+   *
+   * private but used in test
    */
-  _maybeOpenCreateOverlay(params?: AppElementAdminParams) {
+  maybeOpenCreateOverlay(params?: AppElementAdminParams) {
     if (params?.openCreateModal) {
-      this.$.createOverlay.open();
+      this.createOverlay?.open();
     }
   }
 
-  _computeRepoUrl(name: string) {
-    return getBaseUrl() + this._path + '/' + encodeURL(name, true);
+  private computeRepoUrl(name: string) {
+    return `${getBaseUrl()}${this.path}/${encodeURL(name, true)}`;
   }
 
-  _computeChangesLink(name: string) {
+  private computeChangesLink(name: string) {
     return GerritNav.getUrlForProjectChanges(name as RepoName);
   }
 
-  _getCreateRepoCapability() {
-    return this.restApiService.getAccount().then(account => {
-      if (!account) {
-        return;
-      }
-      return this.restApiService
-        .getAccountCapabilities(['createProject'])
-        .then(capabilities => {
-          if (capabilities?.createProject) {
-            this._createNewCapability = true;
-          }
-        });
-    });
-  }
+  private async getCreateRepoCapability() {
+    const account = await this.restApiService.getAccount();
 
-  _getRepos(filter: string, reposPerPage: number, offset?: number) {
-    this._repos = [];
-    return this.restApiService
-      .getRepos(filter, reposPerPage, offset)
-      .then(repos => {
-        // Late response.
-        if (filter !== this._filter || !repos) {
-          return;
-        }
-        this._repos = repos.filter(repo =>
-          repo.name.toLowerCase().includes(filter.toLowerCase())
-        );
-        this._loading = false;
-      });
-  }
+    if (!account) return;
 
-  _refreshReposList() {
-    this.restApiService.invalidateReposCache();
-    return this._getRepos(this._filter, this._reposPerPage, this._offset);
-  }
-
-  async _handleCreateRepo() {
-    await this.$.createNewModal.handleCreateRepo();
-    this._refreshReposList();
-  }
-
-  _handleCloseCreate() {
-    this.$.createOverlay.close();
-  }
-
-  _handleCreateClicked() {
-    this.$.createOverlay.open().then(() => {
-      this.$.createNewModal.focus();
-    });
-  }
-
-  _readOnly(repo: ProjectInfoWithName) {
-    return repo.state === ProjectState.READ_ONLY ? 'Y' : '';
-  }
-
-  _computeWeblink(repo: ProjectInfoWithName) {
-    if (!repo.web_links) {
-      return '';
+    const accountCapabilities =
+      await this.restApiService.getAccountCapabilities(['createProject']);
+    if (accountCapabilities?.createProject) {
+      this.createNewCapability = true;
     }
-    const webLinks = repo.web_links;
-    return webLinks.length ? webLinks : null;
+
+    return account;
   }
 
+  /* private but used in test */
+  async getRepos() {
+    this.repos = [];
+
+    // We save the filter before getting the repos
+    // and then we check the value hasn't changed aftwards.
+    const filter = this.filter;
+
+    const repos = await this.restApiService.getRepos(
+      this.filter,
+      this.reposPerPage,
+      this.offset
+    );
+
+    // Late response.
+    if (filter !== this.filter || !repos) return;
+
+    this.repos = repos.filter(repo =>
+      repo.name.toLowerCase().includes(filter.toLowerCase())
+    );
+    this.loading = false;
+
+    return repos;
+  }
+
+  private async refreshReposList() {
+    this.restApiService.invalidateReposCache();
+    return await this.getRepos();
+  }
+
+  /* private but used in test */
+  async handleCreateRepo() {
+    await this.createNewModal?.handleCreateRepo();
+    await this.refreshReposList();
+  }
+
+  /* private but used in test */
+  handleCloseCreate() {
+    this.createOverlay?.close();
+  }
+
+  /* private but used in test */
+  handleCreateClicked() {
+    this.createOverlay?.open().then(() => {
+      this.createNewModal?.focus();
+    });
+  }
+
+  /* private but used in test */
   computeLoadingClass(loading: boolean) {
     return loading ? 'loading' : '';
   }
 
-  _handleNewRepoName() {
-    this._newRepoName = this.$.createNewModal.nameChanged;
+  private handleNewRepoName() {
+    if (!this.createNewModal) return;
+    this.newRepoName = this.createNewModal.nameChanged;
   }
 }
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.ts b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.ts
deleted file mode 100644
index d65c499..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
-  <style include="shared-styles">
-    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
-  </style>
-  <style include="gr-table-styles">
-    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
-  </style>
-  <style>
-    .genericList tr td:last-of-type {
-      text-align: left;
-    }
-    .genericList tr th:last-of-type {
-      text-align: left;
-    }
-    .readOnly {
-      text-align: center;
-    }
-    .changesLink,
-    .name,
-    .repositoryBrowser,
-    .readOnly {
-      white-space: nowrap;
-    }
-  </style>
-  <gr-list-view
-    create-new="[[_createNewCapability]]"
-    filter="[[_filter]]"
-    items-per-page="[[_reposPerPage]]"
-    items="[[_repos]]"
-    loading="[[_loading]]"
-    offset="[[_offset]]"
-    on-create-clicked="_handleCreateClicked"
-    path="[[_path]]"
-  >
-    <table id="list" class="genericList">
-      <tbody>
-        <tr class="headerRow">
-          <th class="name topHeader">Repository Name</th>
-          <th class="repositoryBrowser topHeader">Repository Browser</th>
-          <th class="changesLink topHeader">Changes</th>
-          <th class="topHeader readOnly">Read only</th>
-          <th class="description topHeader">Repository Description</th>
-        </tr>
-        <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
-          <td>Loading...</td>
-        </tr>
-      </tbody>
-      <tbody class$="[[computeLoadingClass(_loading)]]">
-        <template is="dom-repeat" items="[[_shownRepos]]">
-          <tr class="table">
-            <td class="name">
-              <a href$="[[_computeRepoUrl(item.name)]]">[[item.name]]</a>
-            </td>
-            <td class="repositoryBrowser">
-              <template
-                is="dom-repeat"
-                items="[[_computeWeblink(item)]]"
-                as="link"
-              >
-                <a
-                  href$="[[link.url]]"
-                  class="webLink"
-                  rel="noopener"
-                  target="_blank"
-                >
-                  [[link.name]]
-                </a>
-              </template>
-            </td>
-            <td class="changesLink">
-              <a href$="[[_computeChangesLink(item.name)]]">view all</a>
-            </td>
-            <td class="readOnly">[[_readOnly(item)]]</td>
-            <td class="description">[[item.description]]</td>
-          </tr>
-        </template>
-      </tbody>
-    </table>
-  </gr-list-view>
-  <gr-overlay id="createOverlay" with-backdrop="">
-    <gr-dialog
-      id="createDialog"
-      class="confirmDialog"
-      disabled="[[!_newRepoName]]"
-      confirm-label="Create"
-      on-confirm="_handleCreateRepo"
-      on-cancel="_handleCloseCreate"
-    >
-      <div class="header" slot="header">Create Repository</div>
-      <div class="main" slot="main">
-        <gr-create-repo-dialog
-          id="createNewModal"
-          on-new-repo-name="_handleNewRepoName"
-        ></gr-create-repo-dialog>
-      </div>
-    </gr-dialog>
-  </gr-overlay>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.ts
index 7f7b0cf..142a838 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.ts
@@ -19,14 +19,18 @@
 import './gr-repo-list';
 import {GrRepoList} from './gr-repo-list';
 import {page} from '../../../utils/page-wrapper-utils';
-import {queryAndAssert, stubRestApi} from '../../../test/test-utils';
+import {
+  mockPromise,
+  queryAndAssert,
+  stubRestApi,
+} from '../../../test/test-utils';
 import {
   UrlEncodedRepoName,
   ProjectInfoWithName,
   RepoName,
 } from '../../../types/common';
 import {AppElementAdminParams} from '../../gr-app-types';
-import {ProjectState} from '../../../constants/constants';
+import {ProjectState, SHOWN_ITEMS_COUNT} from '../../../constants/constants';
 import {GerritView} from '../../../services/router/router-model';
 import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
@@ -60,47 +64,46 @@
   let element: GrRepoList;
   let repos: ProjectInfoWithName[];
 
-  const value: AppElementAdminParams = {view: GerritView.ADMIN, adminView: ''};
-
-  setup(() => {
+  setup(async () => {
     sinon.stub(page, 'show');
     element = basicFixture.instantiate();
+    await element.updateComplete;
   });
 
   suite('list with repos', () => {
     setup(async () => {
       repos = createRepoList('test', 26);
       stubRestApi('getRepos').returns(Promise.resolve(repos));
-      await element._paramsChanged(value);
-      await flush();
+      await element._paramsChanged();
+      await element.updateComplete;
     });
 
     test('test for test repo in the list', async () => {
-      await flush();
-      assert.equal(element._repos[0].id, 'test0');
-      assert.equal(element._repos[1].id, 'test1');
-      assert.equal(element._repos[2].id, 'test2');
+      await element.updateComplete;
+      assert.equal(element.repos[0].id, 'test0');
+      assert.equal(element.repos[1].id, 'test1');
+      assert.equal(element.repos[2].id, 'test2');
     });
 
-    test('_shownRepos', () => {
-      assert.equal(element._shownRepos.length, 25);
+    test('shownRepos', () => {
+      assert.equal(element.repos.slice(0, SHOWN_ITEMS_COUNT).length, 25);
     });
 
-    test('_maybeOpenCreateOverlay', () => {
+    test('maybeOpenCreateOverlay', () => {
       const overlayOpen = sinon.stub(
         queryAndAssert<GrOverlay>(element, '#createOverlay'),
         'open'
       );
-      element._maybeOpenCreateOverlay();
+      element.maybeOpenCreateOverlay();
       assert.isFalse(overlayOpen.called);
-      element._maybeOpenCreateOverlay(undefined);
+      element.maybeOpenCreateOverlay(undefined);
       assert.isFalse(overlayOpen.called);
       const params: AppElementAdminParams = {
         view: GerritView.ADMIN,
         adminView: '',
         openCreateModal: true,
       };
-      element._maybeOpenCreateOverlay(params);
+      element.maybeOpenCreateOverlay(params);
       assert.isTrue(overlayOpen.called);
     });
   });
@@ -109,12 +112,12 @@
     setup(async () => {
       repos = createRepoList('test', 25);
       stubRestApi('getRepos').returns(Promise.resolve(repos));
-      await element._paramsChanged(value);
-      await flush();
+      await element._paramsChanged();
+      await element.updateComplete;
     });
 
-    test('_shownRepos', () => {
-      assert.equal(element._shownRepos.length, 25);
+    test('shownRepos', () => {
+      assert.equal(element.repos.slice(0, SHOWN_ITEMS_COUNT).length, 25);
     });
   });
 
@@ -129,41 +132,51 @@
     test('_paramsChanged', async () => {
       const repoStub = stubRestApi('getRepos');
       repoStub.returns(Promise.resolve(repos));
-      const value: AppElementAdminParams = {
+      element.params = {
         view: GerritView.ADMIN,
         adminView: '',
         filter: 'test',
         offset: 25,
-      };
-      await element._paramsChanged(value);
+      } as AppElementAdminParams;
+      await element._paramsChanged();
       assert.isTrue(repoStub.lastCall.calledWithExactly('test', 25, 25));
     });
 
     test('latest repos requested are always set', async () => {
       const repoStub = stubRestApi('getRepos');
-      repoStub.withArgs('test', 25).returns(Promise.resolve(repos));
-      repoStub.withArgs('filter', 25).returns(Promise.resolve(reposFiltered));
-      element._filter = 'test';
+      const promise = mockPromise<ProjectInfoWithName[]>();
+      repoStub.withArgs('filter', 25).returns(promise);
 
-      // Repos are not set because the element._filter differs.
-      await element._getRepos('filter', 25, 0);
-      assert.deepEqual(element._repos, []);
+      element.filter = 'test';
+      element.reposPerPage = 25;
+      element.offset = 0;
+
+      // Repos are not set because the element.filter differs.
+      const p = element.getRepos();
+      element.filter = 'filter';
+      promise.resolve(reposFiltered);
+      await p;
+      assert.deepEqual(element.repos, []);
     });
 
     test('filter is case insensitive', async () => {
       const repoStub = stubRestApi('getRepos');
       const repos = [createRepo('aSDf', 0)];
       repoStub.withArgs('asdf', 25).returns(Promise.resolve(repos));
-      element._filter = 'asdf';
-      await element._getRepos('asdf', 25, 0);
-      assert.equal(element._repos.length, 1);
+
+      element.filter = 'asdf';
+      element.reposPerPage = 25;
+      element.offset = 0;
+
+      await element.getRepos();
+      assert.equal(element.repos.length, 1);
     });
   });
 
   suite('loading', () => {
-    test('correct contents are displayed', () => {
-      assert.isTrue(element._loading);
-      assert.equal(element.computeLoadingClass(element._loading), 'loading');
+    test('correct contents are displayed', async () => {
+      assert.isTrue(element.loading);
+      assert.equal(element.computeLoadingClass(element.loading), 'loading');
       assert.equal(
         getComputedStyle(
           queryAndAssert<HTMLTableRowElement>(element, '#loading')
@@ -171,11 +184,11 @@
         'block'
       );
 
-      element._loading = false;
-      element._repos = createRepoList('test', 25);
+      element.loading = false;
+      element.repos = createRepoList('test', 25);
 
-      flush();
-      assert.equal(element.computeLoadingClass(element._loading), '');
+      await element.updateComplete;
+      assert.equal(element.computeLoadingClass(element.loading), '');
       assert.equal(
         getComputedStyle(
           queryAndAssert<HTMLTableRowElement>(element, '#loading')
@@ -186,11 +199,9 @@
   });
 
   suite('create new', () => {
-    test('_handleCreateClicked called when create-click fired', () => {
-      const handleCreateClickedStub = sinon.stub(
-        element,
-        '_handleCreateClicked'
-      );
+    test('handleCreateClicked called when create-clicked fired', () => {
+      const handleCreateClickedStub = sinon.stub();
+      element.addEventListener('create-clicked', handleCreateClickedStub);
       queryAndAssert<GrListView>(element, 'gr-list-view').dispatchEvent(
         new CustomEvent('create-clicked', {
           composed: true,
@@ -200,31 +211,33 @@
       assert.isTrue(handleCreateClickedStub.called);
     });
 
-    test('_handleCreateClicked opens modal', () => {
+    test('handleCreateClicked opens modal', () => {
       const openStub = sinon
         .stub(queryAndAssert<GrOverlay>(element, '#createOverlay'), 'open')
         .returns(Promise.resolve());
-      element._handleCreateClicked();
+      element.handleCreateClicked();
       assert.isTrue(openStub.called);
     });
 
-    test('_handleCreateRepo called when confirm fired', () => {
-      const handleCreateRepoStub = sinon.stub(element, '_handleCreateRepo');
+    test('handleCreateRepo called when confirm fired', () => {
+      const handleCreateRepoStub = sinon.stub();
+      element.addEventListener('confirm', handleCreateRepoStub);
       queryAndAssert<GrDialog>(element, '#createDialog').dispatchEvent(
         new CustomEvent('confirm', {
           composed: true,
-          bubbles: true,
+          bubbles: false,
         })
       );
       assert.isTrue(handleCreateRepoStub.called);
     });
 
-    test('_handleCloseCreate called when cancel fired', () => {
-      const handleCloseCreateStub = sinon.stub(element, '_handleCloseCreate');
+    test('handleCloseCreate called when cancel fired', () => {
+      const handleCloseCreateStub = sinon.stub();
+      element.addEventListener('cancel', handleCloseCreateStub);
       queryAndAssert<GrDialog>(element, '#createDialog').dispatchEvent(
         new CustomEvent('cancel', {
           composed: true,
-          bubbles: true,
+          bubbles: false,
         })
       );
       assert.isTrue(handleCloseCreateStub.called);
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 3ac7c7b..4406a73 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -206,6 +206,7 @@
   UrlEncodedRepoName,
   UserConfigInfo,
   VotingRangeInfo,
+  WebLinkInfo,
   isDetailedLabelInfo,
   isQuickLabelInfo,
 };