Convert gr-create-change-dialog to lit

I tested this on my local gerrit dev site and made sure it worked.

Change-Id: Iaba1d3b1001f1833cd337ebbc225224142e4aa14
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
index 15f6f4b..bd9bc7a 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
@@ -20,98 +20,77 @@
 import '../../shared/gr-autocomplete/gr-autocomplete';
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-select/gr-select';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-create-change-dialog_html';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {customElement, property, observe} from '@polymer/decorators';
 import {
   RepoName,
   BranchName,
   ChangeId,
-  ConfigInfo,
   InheritedBooleanInfo,
 } from '../../../types/common';
 import {InheritedBooleanInfoConfiguredValue} from '../../../constants/constants';
-import {GrTypedAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
-import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
 import {appContext} from '../../../services/app-context';
 import {Subject} from 'rxjs';
-import {
-  repoConfig$,
-  serverConfig$,
-} from '../../../services/config/config-model';
+import {serverConfig$} from '../../../services/config/config-model';
 import {takeUntil} from 'rxjs/operators';
-import {IronInputElement} from '@polymer/iron-input/iron-input';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, css, html} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
+import {BindValueChangeEvent} from '../../../types/events';
+import {fireEvent} from '../../../utils/event-util';
 
 const SUGGESTIONS_LIMIT = 15;
 const REF_PREFIX = 'refs/heads/';
 
-export interface GrCreateChangeDialog {
-  $: {
-    privateChangeCheckBox: HTMLInputElement;
-    branchInput: GrTypedAutocomplete<BranchName>;
-    tagNameInput: IronInputElement;
-    messageInput: IronAutogrowTextareaElement;
-  };
-}
-@customElement('gr-create-change-dialog')
-export class GrCreateChangeDialog extends PolymerElement {
-  static get template() {
-    return htmlTemplate;
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-create-change-dialog': GrCreateChangeDialog;
   }
+}
+
+@customElement('gr-create-change-dialog')
+export class GrCreateChangeDialog extends LitElement {
+  // private but used in test
+  @query('#privateChangeCheckBox') privateChangeCheckBox!: HTMLInputElement;
 
   @property({type: String})
   repoName?: RepoName;
 
-  @property({type: String})
-  branch = '' as BranchName;
+  // private but used in test
+  @state() branch = '' as BranchName;
 
-  @property({type: Object})
-  _repoConfig?: ConfigInfo;
+  // private but used in test
+  @state() subject = '';
 
-  @property({type: String})
-  subject = '';
+  // private but used in test
+  @state() topic?: string;
 
-  @property({type: String})
-  topic?: string;
+  @state() private baseChange?: ChangeId;
 
-  @property({type: Object})
-  _query?: (input: string) => Promise<{name: BranchName}[]>;
-
-  @property({type: String})
-  baseChange?: ChangeId;
-
-  @property({type: String})
-  baseCommit?: string;
+  @state() private baseCommit?: string;
 
   @property({type: Object})
   privateByDefault?: InheritedBooleanInfo;
 
-  @property({type: Boolean, notify: true})
-  canCreate = false;
+  @state() private privateChangesEnabled = false;
 
-  @property({type: Boolean})
-  _privateChangesEnabled = false;
+  private readonly query: (input: string) => Promise<{name: BranchName}[]>;
 
-  restApiService = appContext.restApiService;
+  private readonly restApiService = appContext.restApiService;
 
   disconnected$ = new Subject();
 
   constructor() {
     super();
-    this._query = (input: string) => this._getRepoBranchesSuggestions(input);
+    this.query = (input: string) => this.getRepoBranchesSuggestions(input);
   }
 
   override connectedCallback() {
     super.connectedCallback();
     if (!this.repoName) return;
 
-    repoConfig$.pipe(takeUntil(this.disconnected$)).subscribe(config => {
-      this.privateByDefault = config?.private_by_default;
-    });
-
     serverConfig$.pipe(takeUntil(this.disconnected$)).subscribe(config => {
-      this._privateChangesEnabled =
+      this.privateChangesEnabled =
         config?.change?.disable_private_changes ?? false;
     });
   }
@@ -121,20 +100,136 @@
     super.disconnectedCallback();
   }
 
-  _computeBranchClass(baseChange?: ChangeId) {
-    return baseChange ? 'hide' : '';
+  static override get styles() {
+    return [
+      formStyles,
+      sharedStyles,
+      css`
+        input:not([type='checkbox']),
+        gr-autocomplete,
+        iron-autogrow-textarea {
+          width: 100%;
+        }
+        .value {
+          width: 32em;
+        }
+        .hide {
+          display: none;
+        }
+        @media only screen and (max-width: 40em) {
+          .value {
+            width: 29em;
+          }
+        }
+      `,
+    ];
   }
 
-  @observe('branch', 'subject')
-  _allowCreate(branch: BranchName, subject: string) {
-    this.canCreate = !!branch && !!subject;
+  override render() {
+    return html`
+      <div class="gr-form-styles">
+        <section class=${this.baseChange ? 'hide' : ''}>
+          <span class="title">Select branch for new change</span>
+          <span class="value">
+            <gr-autocomplete
+              id="branchInput"
+              .text=${this.branch}
+              .query=${this.query}
+              placeholder="Destination branch"
+              @text-changed=${(e: CustomEvent) => {
+                this.branch = e.detail.value;
+              }}
+            >
+            </gr-autocomplete>
+          </span>
+        </section>
+        <section class=${this.baseChange ? 'hide' : ''}>
+          <span class="title">Provide base commit sha1 for change</span>
+          <span class="value">
+            <iron-input
+              maxlength="40"
+              placeholder="(optional)"
+              .bindValue=${this.baseCommit}
+              @bind-value-changed=${(e: BindValueChangeEvent) => {
+                this.baseCommit = e.detail.value;
+              }}
+            >
+              <input
+                id="baseCommitInput"
+                maxlength="40"
+                placeholder="(optional)"
+              />
+            </iron-input>
+          </span>
+        </section>
+        <section>
+          <span class="title">Enter topic for new change</span>
+          <span class="value">
+            <iron-input
+              maxlength="1024"
+              placeholder="(optional)"
+              .bindValue=${this.topic}
+              @bind-value-changed=${(e: BindValueChangeEvent) => {
+                this.topic = e.detail.value;
+              }}
+            >
+              <input
+                id="tagNameInput"
+                maxlength="1024"
+                placeholder="(optional)"
+              />
+            </iron-input>
+          </span>
+        </section>
+        <section id="description">
+          <span class="title">Description</span>
+          <span class="value">
+            <iron-autogrow-textarea
+              id="messageInput"
+              class="message"
+              autocomplete="on"
+              rows="4"
+              maxRows="15"
+              .bindValue=${this.subject}
+              placeholder="Insert the description of the change."
+              @bind-value-changed=${(e: BindValueChangeEvent) => {
+                this.subject = e.detail.value;
+              }}
+            >
+            </iron-autogrow-textarea>
+          </span>
+        </section>
+        <section class=${this.privateChangesEnabled ? 'hide' : ''}>
+          <label class="title" for="privateChangeCheckBox"
+            >Private change</label
+          >
+          <span class="value">
+            <input
+              type="checkbox"
+              id="privateChangeCheckBox"
+              ?checked=${this.formatPrivateByDefaultBoolean()}
+            />
+          </span>
+        </section>
+      </div>
+    `;
+  }
+
+  override willUpdate(changedProperties: PropertyValues) {
+    if (changedProperties.has('branch') || changedProperties.has('subject')) {
+      this.allowCreate();
+    }
+  }
+
+  private allowCreate() {
+    fireEvent(this, 'can-create-change');
   }
 
   handleCreateChange(): Promise<void> {
     if (!this.repoName || !this.branch || !this.subject) {
       return Promise.resolve();
     }
-    const isPrivate = this.$.privateChangeCheckBox.checked;
+    const isPrivate = this.privateChangeCheckBox.checked;
     const isWip = true;
     return this.restApiService
       .createChange(
@@ -148,14 +243,13 @@
         this.baseCommit || undefined
       )
       .then(changeCreated => {
-        if (!changeCreated) {
-          return;
-        }
+        if (!changeCreated) return;
         GerritNav.navigateToChange(changeCreated);
       });
   }
 
-  _getRepoBranchesSuggestions(input: string) {
+  // private but used in test
+  getRepoBranchesSuggestions(input: string) {
     if (!this.repoName) {
       return Promise.reject(new Error('missing repo name'));
     }
@@ -178,34 +272,19 @@
       });
   }
 
-  _formatBooleanString(config?: InheritedBooleanInfo) {
-    if (
-      config &&
-      config.configured_value === InheritedBooleanInfoConfiguredValue.TRUE
-    ) {
-      return true;
-    } else if (
-      config &&
-      config.configured_value === InheritedBooleanInfoConfiguredValue.FALSE
-    ) {
-      return false;
-    } else if (
-      config &&
-      config.configured_value === InheritedBooleanInfoConfiguredValue.INHERITED
-    ) {
-      return !!(config && config.inherited_value);
-    } else {
-      return false;
+  // private but used in test
+  formatPrivateByDefaultBoolean() {
+    const config = this.privateByDefault;
+    if (config === undefined) return false;
+    switch (config.configured_value) {
+      case InheritedBooleanInfoConfiguredValue.TRUE:
+        return true;
+      case InheritedBooleanInfoConfiguredValue.FALSE:
+        return false;
+      case InheritedBooleanInfoConfiguredValue.INHERITED:
+        return !!config.inherited_value;
+      default:
+        return false;
     }
   }
-
-  _computePrivateSectionClass(config: boolean) {
-    return config ? 'hide' : '';
-  }
-}
-
-declare global {
-  interface HTMLElementTagNameMap {
-    'gr-create-change-dialog': GrCreateChangeDialog;
-  }
 }
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.ts b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.ts
deleted file mode 100644
index 47f3818..0000000
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_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-form-styles">
-    input:not([type='checkbox']),
-    gr-autocomplete,
-    iron-autogrow-textarea {
-      width: 100%;
-    }
-    .value {
-      width: 32em;
-    }
-    .hide {
-      display: none;
-    }
-    @media only screen and (max-width: 40em) {
-      .value {
-        width: 29em;
-      }
-    }
-  </style>
-  <div class="gr-form-styles">
-    <section class$="[[_computeBranchClass(baseChange)]]">
-      <span class="title">Select branch for new change</span>
-      <span class="value">
-        <gr-autocomplete
-          id="branchInput"
-          text="{{branch}}"
-          query="[[_query]]"
-          placeholder="Destination branch"
-        >
-        </gr-autocomplete>
-      </span>
-    </section>
-    <section class$="[[_computeBranchClass(baseChange)]]">
-      <span class="title">Provide base commit sha1 for change</span>
-      <span class="value">
-        <iron-input
-          maxlength="40"
-          placeholder="(optional)"
-          bind-value="{{baseCommit}}"
-        >
-          <input
-            is="iron-input"
-            id="baseCommitInput"
-            maxlength="40"
-            placeholder="(optional)"
-            bind-value="{{baseCommit}}"
-          />
-        </iron-input>
-      </span>
-    </section>
-    <section>
-      <span class="title">Enter topic for new change</span>
-      <span class="value">
-        <iron-input
-          maxlength="1024"
-          placeholder="(optional)"
-          bind-value="{{topic}}"
-        >
-          <input
-            is="iron-input"
-            id="tagNameInput"
-            maxlength="1024"
-            placeholder="(optional)"
-            bind-value="{{topic}}"
-          />
-        </iron-input>
-      </span>
-    </section>
-    <section id="description">
-      <span class="title">Description</span>
-      <span class="value">
-        <iron-autogrow-textarea
-          id="messageInput"
-          class="message"
-          autocomplete="on"
-          rows="4"
-          max-rows="15"
-          bind-value="{{subject}}"
-          placeholder="Insert the description of the change."
-        >
-        </iron-autogrow-textarea>
-      </span>
-    </section>
-    <section class$="[[_computePrivateSectionClass(_privateChangesEnabled)]]">
-      <label class="title" for="privateChangeCheckBox">Private change</label>
-      <span class="value">
-        <input
-          type="checkbox"
-          id="privateChangeCheckBox"
-          checked$="[[_formatBooleanString(privateByDefault)]]"
-        />
-      </span>
-    </section>
-  </div>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts
index 9ed5d81..626b03f 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts
@@ -20,19 +20,16 @@
 import {GrCreateChangeDialog} from './gr-create-change-dialog';
 import {BranchName, GitRef, RepoName} from '../../../types/common';
 import {InheritedBooleanInfoConfiguredValue} from '../../../constants/constants';
-import {
-  createChange,
-  createConfig,
-  TEST_CHANGE_ID,
-} from '../../../test/test-data-generators';
-import {stubRestApi} from '../../../test/test-utils';
+import {createChange} from '../../../test/test-data-generators';
+import {queryAndAssert, stubRestApi} from '../../../test/test-utils';
+import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
 
 const basicFixture = fixtureFromElement('gr-create-change-dialog');
 
 suite('gr-create-change-dialog tests', () => {
   let element: GrCreateChangeDialog;
 
-  setup(() => {
+  setup(async () => {
     stubRestApi('getRepoBranches').callsFake((input: string) => {
       if (input.startsWith('test')) {
         return Promise.resolve([
@@ -47,15 +44,8 @@
       }
     });
     element = basicFixture.instantiate();
+    await element.updateComplete;
     element.repoName = 'test-repo' as RepoName;
-    element._repoConfig = {
-      ...createConfig(),
-      private_by_default: {
-        value: false,
-        configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
-        inherited_value: false,
-      },
-    };
   });
 
   test('new change created with default', async () => {
@@ -74,9 +64,13 @@
     element.branch = 'test-branch' as BranchName;
     element.topic = 'test-topic';
     element.subject = 'first change created with polygerrit ui';
-    assert.isFalse(element.$.privateChangeCheckBox.checked);
+    assert.isFalse(element.privateChangeCheckBox.checked);
 
-    element.$.messageInput.bindValue = configInputObj.subject;
+    const messageInput = queryAndAssert<IronAutogrowTextareaElement>(
+      element,
+      '#messageInput'
+    );
+    messageInput.bindValue = configInputObj.subject;
 
     await element.handleCreateChange();
     // Private change
@@ -92,8 +86,8 @@
       inherited_value: false,
       value: true,
     };
-    sinon.stub(element, '_formatBooleanString').callsFake(() => true);
-    flush();
+    sinon.stub(element, 'formatPrivateByDefaultBoolean').callsFake(() => true);
+    await element.updateComplete;
 
     const configInputObj = {
       branch: 'test-branch',
@@ -110,9 +104,13 @@
     element.branch = 'test-branch' as BranchName;
     element.topic = 'test-topic';
     element.subject = 'first change created with polygerrit ui';
-    assert.isTrue(element.$.privateChangeCheckBox.checked);
+    assert.isTrue(element.privateChangeCheckBox.checked);
 
-    element.$.messageInput.bindValue = configInputObj.subject;
+    const messageInput = queryAndAssert<IronAutogrowTextareaElement>(
+      element,
+      '#messageInput'
+    );
+    messageInput.bindValue = configInputObj.subject;
 
     await element.handleCreateChange();
     // Private change
@@ -122,24 +120,14 @@
     assert.isTrue(saveStub.called);
   });
 
-  test('_getRepoBranchesSuggestions empty', async () => {
-    const branches = await element._getRepoBranchesSuggestions('nonexistent');
+  test('getRepoBranchesSuggestions empty', async () => {
+    const branches = await element.getRepoBranchesSuggestions('nonexistent');
     assert.equal(branches.length, 0);
   });
 
-  test('_getRepoBranchesSuggestions non-empty', async () => {
-    const branches = await element._getRepoBranchesSuggestions('test-branch');
+  test('getRepoBranchesSuggestions non-empty', async () => {
+    const branches = await element.getRepoBranchesSuggestions('test-branch');
     assert.equal(branches.length, 1);
     assert.equal(branches[0].name, 'test-branch');
   });
-
-  test('_computeBranchClass', () => {
-    assert.equal(element._computeBranchClass(TEST_CHANGE_ID), 'hide');
-    assert.equal(element._computeBranchClass(undefined), '');
-  });
-
-  test('_computePrivateSectionClass', () => {
-    assert.equal(element._computePrivateSectionClass(true), 'hide');
-    assert.equal(element._computePrivateSectionClass(false), '');
-  });
 });
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
index de43dc6..7145d10 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
@@ -77,7 +77,7 @@
   _repoConfig?: ConfigInfo;
 
   @property({type: Boolean})
-  _canCreate = false;
+  _canCreateChange = false;
 
   @property({type: Boolean})
   _creatingChange = false;
@@ -185,6 +185,12 @@
         this._editingConfig = false;
       });
   }
+
+  _handleCanCreateChange() {
+    this._canCreateChange =
+      !!this.$.createNewChangeModal.branch &&
+      !!this.$.createNewChangeModal.subject;
+  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.ts b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.ts
index 9948f8f..51a26f7 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.ts
@@ -75,7 +75,7 @@
     <gr-dialog
       id="createChangeDialog"
       confirm-label="Create"
-      disabled="[[!_canCreate]]"
+      disabled="[[!_canCreateChange]]"
       on-confirm="_handleCreateChange"
       on-cancel="_handleCloseCreateChange"
     >
@@ -83,8 +83,9 @@
       <div class="main" slot="main">
         <gr-create-change-dialog
           id="createNewChangeModal"
-          can-create="{{_canCreate}}"
           repo-name="[[repo]]"
+          private-by-default="[[_repoConfig.private_by_default]]"
+          on-can-create-change="_handleCanCreateChange"
         ></gr-create-change-dialog>
       </div>
     </gr-dialog>