Convert gr-admin-group-list to lit

Change-Id: I0c2cd217a04383ac862ebc862d258dd02eaf4c05
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
index 83063a1..f3f1139 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
@@ -15,16 +15,11 @@
  * 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-group-dialog/gr-create-group-dialog';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-admin-group-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 {GroupId, GroupInfo, GroupName} from '../../../types/common';
@@ -32,6 +27,11 @@
 import {fireTitleChange} from '../../../utils/event-util';
 import {appContext} from '../../../services/app-context';
 import {SHOWN_ITEMS_COUNT} from '../../../constants/constants';
+import {tableStyles} from '../../../styles/gr-table-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, html} from 'lit';
+import {customElement, query, property, state} from 'lit/decorators';
+import {assertIsDefined} from '../../../utils/common-util';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -39,18 +39,13 @@
   }
 }
 
-export interface GrAdminGroupList {
-  $: {
-    createOverlay: GrOverlay;
-    createNewModal: GrCreateGroupDialog;
-  };
-}
-
 @customElement('gr-admin-group-list')
-export class GrAdminGroupList extends PolymerElement {
-  static get template() {
-    return htmlTemplate;
-  }
+export class GrAdminGroupList extends LitElement {
+  readonly path = '/admin/groups';
+
+  @query('#createOverlay') private createOverlay?: GrOverlay;
+
+  @query('#createNewModal') private createNewModal?: GrCreateGroupDialog;
 
   @property({type: Object})
   params?: AppElementAdminParams;
@@ -58,135 +53,200 @@
   /**
    * Offset of currently visible query results.
    */
-  @property({type: Number})
-  _offset = 0;
+  @state() private offset = 0;
 
-  @property({type: String})
-  readonly _path = '/admin/groups';
+  @state() private hasNewGroupName = false;
 
-  @property({type: Boolean})
-  _hasNewGroupName = false;
+  @state() private createNewCapability = false;
 
-  @property({type: Boolean})
-  _createNewCapability = false;
+  // private but used in test
+  @state() groups: GroupInfo[] = [];
 
-  @property({type: Array})
-  _groups: GroupInfo[] = [];
+  @state() private groupsPerPage = 25;
 
-  /**
-   * Because  we request one more than the groupsPerPage, _shownGroups
-   * may be one less than _groups.
-   * */
-  @computed('_groups')
-  get _shownGroups() {
-    return this._groups.slice(0, SHOWN_ITEMS_COUNT);
-  }
+  // private but used in test
+  @state() loading = true;
 
-  @property({type: Number})
-  _groupsPerPage = 25;
-
-  @property({type: Boolean})
-  _loading = true;
-
-  @property({type: String})
-  _filter = '';
+  @state() private filter = '';
 
   private readonly restApiService = appContext.restApiService;
 
   override connectedCallback() {
     super.connectedCallback();
-    this._getCreateGroupCapability();
+    this.getCreateGroupCapability();
     fireTitleChange(this, 'Groups');
-    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];
+  }
 
-    return this._getGroups(this._filter, this._groupsPerPage, this._offset);
+  override render() {
+    return html`
+      <gr-list-view
+        .createNew=${this.createNewCapability}
+        .filter=${this.filter}
+        .items=${this.groups}
+        .itemsPerPage=${this.groupsPerPage}
+        .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">Group Name</th>
+              <th class="description topHeader">Group Description</th>
+              <th class="visibleToAll topHeader">Visible To All</th>
+            </tr>
+            <tr
+              id="loading"
+              class="loadingMsg ${this.loading ? 'loading' : ''}"
+            >
+              <td>Loading...</td>
+            </tr>
+          </tbody>
+          <tbody class=${this.loading ? 'loading' : ''}>
+            ${this.groups
+              .slice(0, SHOWN_ITEMS_COUNT)
+              .map(group => this.renderGroupList(group))}
+          </tbody>
+        </table>
+      </gr-list-view>
+      <gr-overlay id="createOverlay" with-backdrop>
+        <gr-dialog
+          id="createDialog"
+          class="confirmDialog"
+          ?disabled=${!this.hasNewGroupName}
+          confirm-label="Create"
+          confirm-on-enter
+          @confirm=${() => this.handleCreateGroup()}
+          @cancel=${() => this.handleCloseCreate()}
+        >
+          <div class="header" slot="header">Create Group</div>
+          <div class="main" slot="main">
+            <gr-create-group-dialog
+              id="createNewModal"
+              @has-new-group-name=${this.handleHasNewGroupName}
+            ></gr-create-group-dialog>
+          </div>
+        </gr-dialog>
+      </gr-overlay>
+    `;
+  }
+
+  private renderGroupList(group: GroupInfo) {
+    return html`
+      <tr class="table">
+        <td class="name">
+          <a href=${this.computeGroupUrl(group.id)}>${group.name}</a>
+        </td>
+        <td class="description">${group.description}</td>
+        <td class="visibleToAll">
+          ${group.options?.visible_to_all === true ? 'Y' : 'N'}
+        </td>
+      </tr>
+    `;
+  }
+
+  override willUpdate(changedProperties: PropertyValues) {
+    if (changedProperties.has('params')) {
+      this.paramsChanged();
+    }
+  }
+
+  // private but used in test
+  paramsChanged() {
+    this.filter = this.params?.filter ?? '';
+    this.offset = Number(this.params?.offset ?? 0);
+    this.maybeOpenCreateOverlay(this.params);
+
+    return this.getGroups(this.filter, this.groupsPerPage, this.offset);
   }
 
   /**
    * 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();
+      assertIsDefined(this.createOverlay, 'createOverlay');
+      this.createOverlay.open();
     }
   }
 
   /**
    * Generates groups link (/admin/groups/<uuid>)
+   *
+   * private but used in test
    */
-  _computeGroupUrl(id: string) {
+  computeGroupUrl(id: string) {
     return GerritNav.getUrlForGroup(decodeURIComponent(id) as GroupId);
   }
 
-  _getCreateGroupCapability() {
+  private getCreateGroupCapability() {
     return this.restApiService.getAccount().then(account => {
-      if (!account) {
-        return;
-      }
+      if (!account) return;
       return this.restApiService
         .getAccountCapabilities(['createGroup'])
         .then(capabilities => {
           if (capabilities?.createGroup) {
-            this._createNewCapability = true;
+            this.createNewCapability = true;
           }
         });
     });
   }
 
-  _getGroups(filter: string, groupsPerPage: number, offset?: number) {
-    this._groups = [];
+  private getGroups(filter: string, groupsPerPage: number, offset?: number) {
+    this.groups = [];
+    this.loading = true;
     return this.restApiService
       .getGroups(filter, groupsPerPage, offset)
       .then(groups => {
-        if (!groups) {
-          return;
-        }
-        this._groups = Object.keys(groups).map(key => {
+        if (!groups) return;
+        this.groups = Object.keys(groups).map(key => {
           const group = groups[key];
           group.name = key as GroupName;
           return group;
         });
-        this._loading = false;
+      })
+      .finally(() => {
+        this.loading = false;
       });
   }
 
-  _refreshGroupsList() {
+  private refreshGroupsList() {
     this.restApiService.invalidateGroupsCache();
-    return this._getGroups(this._filter, this._groupsPerPage, this._offset);
+    return this.getGroups(this.filter, this.groupsPerPage, this.offset);
   }
 
-  _handleCreateGroup() {
-    this.$.createNewModal.handleCreateGroup().then(() => {
-      this._refreshGroupsList();
+  // private but used in test
+  handleCreateGroup() {
+    assertIsDefined(this.createNewModal, 'createNewModal');
+    this.createNewModal.handleCreateGroup().then(() => {
+      this.refreshGroupsList();
     });
   }
 
-  _handleCloseCreate() {
-    this.$.createOverlay.close();
+  // private but used in test
+  handleCloseCreate() {
+    assertIsDefined(this.createOverlay, 'createOverlay');
+    this.createOverlay.close();
   }
 
-  _handleCreateClicked() {
-    this.$.createOverlay.open().then(() => {
-      this.$.createNewModal.focus();
+  // private but used in test
+  handleCreateClicked() {
+    assertIsDefined(this.createOverlay, 'createOverlay');
+    this.createOverlay.open().then(() => {
+      assertIsDefined(this.createNewModal, 'createNewModal');
+      this.createNewModal.focus();
     });
   }
 
-  _visibleToAll(item: GroupInfo) {
-    return item.options?.visible_to_all === true ? 'Y' : 'N';
-  }
-
-  computeLoadingClass(loading: boolean) {
-    return loading ? 'loading' : '';
-  }
-
-  _handleHasNewGroupName() {
-    this._hasNewGroupName = !!this.$.createNewModal.name;
+  private handleHasNewGroupName() {
+    assertIsDefined(this.createNewModal, 'createNewModal');
+    this.hasNewGroupName = !!this.createNewModal.name;
   }
 }
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.ts b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.ts
deleted file mode 100644
index fdde399..0000000
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.ts
+++ /dev/null
@@ -1,79 +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>
-  <gr-list-view
-    create-new="[[_createNewCapability]]"
-    filter="[[_filter]]"
-    items="[[_groups]]"
-    items-per-page="[[_groupsPerPage]]"
-    loading="[[_loading]]"
-    offset="[[_offset]]"
-    on-create-clicked="_handleCreateClicked"
-    path="[[_path]]"
-  >
-    <table id="list" class="genericList">
-      <tbody>
-        <tr class="headerRow">
-          <th class="name topHeader">Group Name</th>
-          <th class="description topHeader">Group Description</th>
-          <th class="visibleToAll topHeader">Visible To All</th>
-        </tr>
-        <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
-          <td>Loading...</td>
-        </tr>
-      </tbody>
-      <tbody class$="[[computeLoadingClass(_loading)]]">
-        <template is="dom-repeat" items="[[_shownGroups]]">
-          <tr class="table">
-            <td class="name">
-              <a href$="[[_computeGroupUrl(item.id)]]">[[item.name]]</a>
-            </td>
-            <td class="description">[[item.description]]</td>
-            <td class="visibleToAll">[[_visibleToAll(item)]]</td>
-          </tr>
-        </template>
-      </tbody>
-    </table>
-  </gr-list-view>
-  <gr-overlay id="createOverlay" with-backdrop="">
-    <gr-dialog
-      id="createDialog"
-      class="confirmDialog"
-      disabled="[[!_hasNewGroupName]]"
-      confirm-label="Create"
-      confirm-on-enter=""
-      on-confirm="_handleCreateGroup"
-      on-cancel="_handleCloseCreate"
-    >
-      <div class="header" slot="header">Create Group</div>
-      <div class="main" slot="main">
-        <gr-create-group-dialog
-          id="createNewModal"
-          on-has-new-group-name="_handleHasNewGroupName"
-        ></gr-create-group-dialog>
-      </div>
-    </gr-dialog>
-  </gr-overlay>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.ts b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.ts
index 83669c9..709a0b7 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.ts
@@ -30,6 +30,7 @@
 import {GrListView} from '../../shared/gr-list-view/gr-list-view';
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
 import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
+import {SHOWN_ITEMS_COUNT} from '../../../constants/constants';
 
 const basicFixture = fixtureFromElement('gr-admin-group-list');
 
@@ -70,11 +71,12 @@
 
   const value: AppElementAdminParams = {view: GerritView.ADMIN, adminView: ''};
 
-  setup(() => {
+  setup(async () => {
     element = basicFixture.instantiate();
+    await element.updateComplete;
   });
 
-  test('_computeGroupUrl', () => {
+  test('computeGroupUrl', () => {
     let urlStub = sinon
       .stub(GerritNav, 'getUrlForGroup')
       .callsFake(
@@ -83,7 +85,7 @@
 
     let group = 'e2cd66f88a2db4d391ac068a92d987effbe872f5';
     assert.equal(
-      element._computeGroupUrl(group),
+      element.computeGroupUrl(group),
       '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5'
     );
 
@@ -94,7 +96,7 @@
       .callsFake(() => '/admin/groups/user/test');
 
     group = 'user%2Ftest';
-    assert.equal(element._computeGroupUrl(group), '/admin/groups/user/test');
+    assert.equal(element.computeGroupUrl(group), '/admin/groups/user/test');
 
     urlStub.restore();
   });
@@ -103,30 +105,31 @@
     setup(async () => {
       groups = createGroupObjectList('test', 26);
       stubRestApi('getGroups').returns(Promise.resolve(groups));
-      element._paramsChanged(value);
-      await flush();
+      element.params = value;
+      element.paramsChanged();
+      await element.updateComplete;
     });
 
     test('test for test group in the list', () => {
-      assert.equal(element._groups[1].name, 'test1' as GroupName);
-      assert.equal(element._groups[1].options!.visible_to_all, false);
+      assert.equal(element.groups[1].name, 'test1' as GroupName);
+      assert.equal(element.groups[1].options!.visible_to_all, false);
     });
 
-    test('_shownGroups', () => {
-      assert.equal(element._shownGroups.length, 25);
+    test('groups', () => {
+      assert.equal(element.groups.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);
       value.openCreateModal = true;
-      element._maybeOpenCreateOverlay(value);
+      element.maybeOpenCreateOverlay(value);
       assert.isTrue(overlayOpen.called);
     });
   });
@@ -135,41 +138,41 @@
     setup(async () => {
       groups = createGroupObjectList('test', 25);
       stubRestApi('getGroups').returns(Promise.resolve(groups));
-      await element._paramsChanged(value);
-      await flush();
+      element.params = value;
+      await element.paramsChanged();
+      await element.updateComplete;
     });
 
-    test('_shownGroups', () => {
-      assert.equal(element._shownGroups.length, 25);
+    test('groups', () => {
+      assert.equal(element.groups.slice(0, SHOWN_ITEMS_COUNT).length, 25);
     });
   });
 
   suite('filter', () => {
-    test('_paramsChanged', async () => {
+    test('paramsChanged', async () => {
       const getGroupsStub = stubRestApi('getGroups');
       getGroupsStub.returns(Promise.resolve(groups));
       value.filter = 'test';
       value.offset = 25;
-      await element._paramsChanged(value);
+      element.params = value;
+      await element.paramsChanged();
       assert.isTrue(getGroupsStub.lastCall.calledWithExactly('test', 25, 25));
     });
   });
 
   suite('loading', async () => {
     test('correct contents are displayed', async () => {
-      assert.isTrue(element._loading);
-      assert.equal(element.computeLoadingClass(element._loading), 'loading');
+      assert.isTrue(element.loading);
       assert.equal(
         getComputedStyle(queryAndAssert<HTMLTableElement>(element, '#loading'))
           .display,
         'block'
       );
 
-      element._loading = false;
-      element._groups = createGroupList('test', 25);
+      element.loading = false;
+      element.groups = createGroupList('test', 25);
 
-      await flush();
-      assert.equal(element.computeLoadingClass(element._loading), '');
+      await element.updateComplete;
       assert.equal(
         getComputedStyle(queryAndAssert<HTMLTableElement>(element, '#loading'))
           .display,
@@ -179,10 +182,10 @@
   });
 
   suite('create new', () => {
-    test('_handleCreateClicked called when create-click fired', () => {
+    test('handleCreateClicked called when create-click fired', () => {
       const handleCreateClickedStub = sinon.stub(
         element,
-        '_handleCreateClicked'
+        'handleCreateClicked'
       );
       queryAndAssert<GrListView>(element, 'gr-list-view').dispatchEvent(
         new CustomEvent('create-clicked', {
@@ -193,16 +196,16 @@
       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('_handleCreateGroup called when confirm fired', () => {
-      const handleCreateGroupStub = sinon.stub(element, '_handleCreateGroup');
+    test('handleCreateGroup called when confirm fired', () => {
+      const handleCreateGroupStub = sinon.stub(element, 'handleCreateGroup');
       queryAndAssert<GrDialog>(element, '#createDialog').dispatchEvent(
         new CustomEvent('confirm', {
           composed: true,
@@ -212,8 +215,8 @@
       assert.isTrue(handleCreateGroupStub.called);
     });
 
-    test('_handleCloseCreate called when cancel fired', () => {
-      const handleCloseCreateStub = sinon.stub(element, '_handleCloseCreate');
+    test('handleCloseCreate called when cancel fired', () => {
+      const handleCloseCreateStub = sinon.stub(element, 'handleCloseCreate');
       queryAndAssert<GrDialog>(element, '#createDialog').dispatchEvent(
         new CustomEvent('cancel', {
           composed: true,