Merge "Remove tool-tip functionality from gr-button"
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index c8949e6..9ebee9c 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -3698,18 +3698,16 @@
 
     TestRepository<Repo> repo = createProject("repo");
     Change change = insert(repo, newChange(repo));
-    AssigneeInput ain = new AssigneeInput();
-    ain.assignee = user2.toString();
-    gApi.changes().id(change.getId().get()).setAssignee(ain);
+    gApi.changes().id(change.getId().get()).addReviewer(user2.toString());
 
     RequestContext adminContext = requestContext.setContext(newRequestContext(user2));
-    assertQuery("assignee:self", change);
+    assertQuery("reviewer:self", change);
 
     requestContext.setContext(adminContext);
     gApi.accounts().id(user2.get()).setActive(false);
 
     requestContext.setContext(newRequestContext(user2));
-    assertQuery("assignee:self", change);
+    assertQuery("reviewer:self", change);
   }
 
   @Test
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
index 6770c00..d160a28 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
@@ -28,7 +28,6 @@
 import '../../plugins/gr-endpoint-param/gr-endpoint-param';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-change-list-item_html';
-import {ChangeTableMixin} from '../../../mixins/gr-change-table-mixin/gr-change-table-mixin';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {getDisplayName} from '../../../utils/display-name-util';
 import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
@@ -76,11 +75,8 @@
 // How many reviewers should be shown with an account-label?
 const PRIMARY_REVIEWERS_COUNT = 2;
 
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = ChangeTableMixin(PolymerElement);
-
 @customElement('gr-change-list-item')
-export class GrChangeListItem extends base {
+export class GrChangeListItem extends PolymerElement {
   static get template() {
     return htmlTemplate;
   }
@@ -397,6 +393,13 @@
     return change?.attention_set[account._account_id]?.last_update;
   }
 
+  _computeIsColumnHidden(columnToCheck?: string, columnsToDisplay?: string[]) {
+    if (!columnsToDisplay || !columnToCheck) {
+      return false;
+    }
+    return !columnsToDisplay.includes(columnToCheck);
+  }
+
   toggleReviewed() {
     if (!this.change) return;
     const newVal = !this.change?.reviewed;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
index 4557aac..dad451b 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
@@ -127,7 +127,7 @@
   </td>
   <td
     class="cell subject"
-    hidden$="[[isColumnHidden('Subject', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Subject', visibleChangeTableColumns)]]"
   >
     <a
       title$="[[change.subject]]"
@@ -143,7 +143,7 @@
   </td>
   <td
     class="cell status"
-    hidden$="[[isColumnHidden('Status', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Status', visibleChangeTableColumns)]]"
   >
     <template is="dom-repeat" items="[[statuses]]" as="status">
       <div class="comma">,</div>
@@ -155,7 +155,7 @@
   </td>
   <td
     class="cell owner"
-    hidden$="[[isColumnHidden('Owner', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Owner', visibleChangeTableColumns)]]"
   >
     <gr-account-link
       highlightAttention
@@ -165,7 +165,7 @@
   </td>
   <td
     class="cell assignee"
-    hidden$="[[isColumnHidden('Assignee', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Assignee', visibleChangeTableColumns)]]"
   >
     <template is="dom-if" if="[[change.assignee]]">
       <gr-account-link
@@ -179,7 +179,7 @@
   </td>
   <td
     class="cell reviewers"
-    hidden$="[[isColumnHidden('Reviewers', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Reviewers', visibleChangeTableColumns)]]"
   >
     <div>
       <template
@@ -211,7 +211,7 @@
   </td>
   <td
     class="cell comments"
-    hidden$="[[isColumnHidden('Comments', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Comments', visibleChangeTableColumns)]]"
   >
     <iron-icon
       hidden$="[[!change.unresolved_comment_count]]"
@@ -221,7 +221,7 @@
   </td>
   <td
     class="cell repo"
-    hidden$="[[isColumnHidden('Repo', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Repo', visibleChangeTableColumns)]]"
   >
     <a class="fullRepo" href$="[[_computeRepoUrl(change)]]">
       [[_computeRepoDisplay(change, false)]]
@@ -236,7 +236,7 @@
   </td>
   <td
     class="cell branch"
-    hidden$="[[isColumnHidden('Branch', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Branch', visibleChangeTableColumns)]]"
   >
     <a href$="[[_computeRepoBranchURL(change)]]"> [[change.branch]] </a>
     <template is="dom-if" if="[[change.topic]]">
@@ -250,7 +250,7 @@
   </td>
   <td
     class="cell updated"
-    hidden$="[[isColumnHidden('Updated', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Updated', visibleChangeTableColumns)]]"
   >
     <gr-date-formatter
       has-tooltip=""
@@ -259,7 +259,7 @@
   </td>
   <td
     class="cell submitted"
-    hidden$="[[isColumnHidden('Submitted', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Submitted', visibleChangeTableColumns)]]"
   >
     <gr-date-formatter
       has-tooltip=""
@@ -268,7 +268,7 @@
   </td>
   <td
     class="cell waiting"
-    hidden$="[[isColumnHidden('Waiting', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Waiting', visibleChangeTableColumns)]]"
   >
     <gr-date-formatter
       has-tooltip=""
@@ -279,7 +279,7 @@
   </td>
   <td
     class="cell size"
-    hidden$="[[isColumnHidden('Size', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Size', visibleChangeTableColumns)]]"
   >
     <gr-tooltip-content has-tooltip="" title="[[_computeSizeTooltip(change)]]">
       <template is="dom-if" if="[[_changeSize]]">
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
index ac0b929..aa04784 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
@@ -29,6 +29,7 @@
   TopicName,
 } from '../../../types/common';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {columnNames} from '../gr-change-list/gr-change-list';
 import './gr-change-list-item';
 import {GrChangeListItem, LabelCategory} from './gr-change-list-item';
 
@@ -372,7 +373,7 @@
 
     await flush();
 
-    for (const column of element.columnNames) {
+    for (const column of columnNames) {
       const elementClass = '.' + column.toLowerCase();
       assert.isFalse(
         queryAndAssert(element, elementClass).hasAttribute('hidden')
@@ -395,7 +396,7 @@
 
     await flush();
 
-    for (const column of element.columnNames) {
+    for (const column of columnNames) {
       const elementClass = '.' + column.toLowerCase();
       if (column === 'Repo') {
         assert.isTrue(
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
index 40674d4..a2a46e0 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
@@ -24,7 +24,6 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-change-list_html';
 import {appContext} from '../../../services/app-context';
-import {ChangeTableMixin} from '../../../mixins/gr-change-table-mixin/gr-change-table-mixin';
 import {
   KeyboardShortcutMixin,
   Shortcut,
@@ -57,6 +56,19 @@
 const LABEL_PREFIX_INVALID_PROLOG = 'Invalid-Prolog-Rules-Label-Name--';
 const MAX_SHORTCUT_CHARS = 5;
 
+export const columnNames = [
+  'Subject',
+  'Status',
+  'Owner',
+  'Assignee',
+  'Reviewers',
+  'Comments',
+  'Repo',
+  'Branch',
+  'Updated',
+  'Size',
+];
+
 export interface ChangeListSection {
   name?: string;
   query?: string;
@@ -68,7 +80,7 @@
 }
 
 // This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = ChangeTableMixin(KeyboardShortcutMixin(PolymerElement));
+const base = KeyboardShortcutMixin(PolymerElement);
 
 @customElement('gr-change-list')
 export class GrChangeList extends base {
@@ -219,32 +231,44 @@
       return;
     }
 
-    this.changeTableColumns = this.columnNames;
+    this.changeTableColumns = columnNames;
     this.showNumber = false;
-    this.visibleChangeTableColumns = this.getEnabledColumns(
-      this.columnNames,
-      config,
-      this.flagsService.enabledExperiments
+    this.visibleChangeTableColumns = this.changeTableColumns.filter(col =>
+      this._isColumnEnabled(col, config, this.flagsService.enabledExperiments)
     );
-
     if (account && preferences) {
       this.showNumber = !!(
         preferences && preferences.legacycid_in_change_table
       );
       if (preferences.change_table && preferences.change_table.length > 0) {
-        const prefColumns = this.renameProjectToRepoColumn(
-          preferences.change_table
+        const prefColumns = preferences.change_table.map(column =>
+          column === 'Project' ? 'Repo' : column
         );
-        this.visibleChangeTableColumns = this.getEnabledColumns(
-          prefColumns,
-          config,
-          this.flagsService.enabledExperiments
+        this.visibleChangeTableColumns = prefColumns.filter(col =>
+          this._isColumnEnabled(
+            col,
+            config,
+            this.flagsService.enabledExperiments
+          )
         );
       }
     }
   }
 
   /**
+   * Is the column disabled by a server config or experiment? For example the
+   * assignee feature might be disabled and thus the corresponding column is
+   * also disabled.
+   *
+   */
+  _isColumnEnabled(column: string, config: ServerInfo, experiments: string[]) {
+    if (!config || !config.change) return true;
+    if (column === 'Assignee') return !!config.change.enable_assignee;
+    if (column === 'Comments') return experiments.includes('comments-column');
+    return true;
+  }
+
+  /**
    * This methods allows us to customize the columns per section.
    *
    * @param visibleColumns are the columns according to configs and user prefs
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
index 0d2056a..7b226e7 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
@@ -269,7 +269,7 @@
     });
 
     test('all columns visible', () => {
-      for (const column of element.columnNames) {
+      for (const column of element.changeTableColumns) {
         const elementClass = '.' + element._lowerCase(column);
         assert.isFalse(element.shadowRoot
             .querySelector(elementClass).hidden);
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index ced66a9..e278e03 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -52,7 +52,7 @@
 } from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
 import {GrEditConstants} from '../../edit/gr-edit-constants';
 import {pluralize} from '../../../utils/string-util';
-import {windowLocationReload} from '../../../utils/dom-util';
+import {windowLocationReload, querySelectorAll} from '../../../utils/dom-util';
 import {
   GeneratedWebLink,
   GerritNav,
@@ -1173,6 +1173,9 @@
   _paramsChanged(value: AppElementChangeViewParams) {
     if (value.view !== GerritView.CHANGE) {
       this._initialLoadComplete = false;
+      querySelectorAll(this, 'gr-overlay').forEach(overlay =>
+        (overlay as GrOverlay).close()
+      );
       return;
     }
 
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index fa590a3..fd85117 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -16,7 +16,7 @@
  */
 import '../gr-submit-requirement-hovercard/gr-submit-requirement-hovercard';
 import {LitElement, css, html} from 'lit';
-import {customElement, property} from 'lit/decorators';
+import {customElement, property, state} from 'lit/decorators';
 import {ParsedChangeInfo} from '../../../types/types';
 import {
   AccountInfo,
@@ -32,7 +32,14 @@
   iconForStatus,
 } from '../../../utils/label-util';
 import {fontStyles} from '../../../styles/gr-font-styles';
-import {charsOnly} from '../../../utils/string-util';
+import {charsOnly, pluralize} from '../../../utils/string-util';
+import {subscribe} from '../../lit/subscription-controller';
+import {
+  allRunsLatestPatchsetLatestAttempt$,
+  CheckRun,
+} from '../../../services/checks/checks-model';
+import {getResultsOf, hasResultsOf} from '../../../services/checks/checks-util';
+import {Category} from '../../../api/checks';
 
 @customElement('gr-submit-requirements')
 export class GrSubmitRequirements extends LitElement {
@@ -45,6 +52,9 @@
   @property({type: Boolean})
   mutable?: boolean;
 
+  @state()
+  runs: CheckRun[] = [];
+
   static override get styles() {
     return [
       fontStyles,
@@ -95,10 +105,25 @@
         td {
           padding: var(--spacing-s);
         }
+        .votes-cell {
+          display: flex;
+        }
+        .check-error {
+          margin-right: var(--spacing-l);
+        }
+        .check-error iron-icon {
+          color: var(--error-foreground);
+          vertical-align: top;
+        }
       `,
     ];
   }
 
+  constructor() {
+    super();
+    subscribe(this, allRunsLatestPatchsetLatestAttempt$, x => (this.runs = x));
+  }
+
   override render() {
     const submit_requirements = (this.change?.submit_requirements ?? []).filter(
       req => req.status !== SubmitRequirementStatus.NOT_APPLICABLE
@@ -130,7 +155,12 @@
                   .text="${requirement.name}"
                 ></gr-limited-text>
               </td>
-              <td>${this.renderVotes(requirement)}</td>
+              <td>
+                <div class="votes-cell">
+                  ${this.renderVotes(requirement)}
+                  ${this.renderChecks(requirement)}
+                </div>
+              </td>
             </tr>`
           )}
         </tbody>
@@ -199,6 +229,28 @@
     );
   }
 
+  renderChecks(requirement: SubmitRequirementResultInfo) {
+    const requirementLabels = extractAssociatedLabels(requirement);
+    const requirementRuns = this.runs
+      .filter(run => hasResultsOf(run, Category.ERROR))
+      .filter(
+        run => run.labelName && requirementLabels.includes(run.labelName)
+      );
+    const runsCount = requirementRuns.reduce(
+      (sum, run) => sum + getResultsOf(run, Category.ERROR).length,
+      0
+    );
+    if (runsCount > 0) {
+      return html`<span class="check-error"
+        ><iron-icon icon="gr-icons:error"></iron-icon>${pluralize(
+          runsCount,
+          'error'
+        )}</span
+      >`;
+    }
+    return;
+  }
+
   renderTriggerVotes(submitReqs: SubmitRequirementResultInfo[]) {
     const labels = this.change?.labels ?? {};
     const allLabels = Object.keys(labels);
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index c9f07f7..c197599 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -303,9 +303,6 @@
   renderStatusLink() {
     const link = this.run.statusLink;
     if (!link) return;
-    // For COMPLETED we think that the status link are too much clutter.
-    // That could be re-considered.
-    if (this.run.status !== RunStatus.RUNNING) return;
     return html`
       <a href="${link}" target="_blank" @click="${this.onLinkClick}"
         ><iron-icon
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index 4a6f009..8e70eb9 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -177,8 +177,8 @@
 
   CHANGE_ID_QUERY: /^\/id\/(I[0-9a-f]{40})$/,
 
-  // Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>][/].
-  CHANGE_LEGACY: /^\/c\/(\d+)\/?(((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/,
+  // Matches /c/<changeNum>/[*][/].
+  CHANGE_LEGACY: /^\/c\/(\d+)\/(.*)$/,
   CHANGE_NUMBER_LEGACY: /^\/(\d+)\/?/,
 
   // Matches
@@ -210,10 +210,6 @@
   // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>]/<path>,edit[#lineNum]
   DIFF_EDIT: /^\/c\/(.+)\/\+\/(\d+)\/(\d+|edit)\/(.+),edit(#\d+)?$/,
 
-  // Matches non-project-relative
-  // /c/<changeNum>/[<basePatchNum>..]<patchNum>/<path>.
-  DIFF_LEGACY: /^\/c\/(\d+)\/((-?\d+|edit)(\.\.(\d+|edit))?)\/(.+)/,
-
   // Matches diff routes using @\d+ to specify a file name (whether or not
   // the project name is included).
   // eslint-disable-next-line max-len
@@ -287,15 +283,6 @@
 
 type QueryStringItem = [string, string]; // [key, value]
 
-type GenerateUrlLegacyChangeViewParameters = Omit<
-  GenerateUrlChangeViewParameters,
-  'project'
->;
-type GenerateUrlLegacyDiffViewParameters = Omit<
-  GenerateUrlDiffViewParameters,
-  'project'
->;
-
 interface PatchRangeParams {
   patchNum?: PatchSetNum;
   basePatchNum?: BasePatchSetNum;
@@ -679,37 +666,6 @@
   }
 
   /**
-   * Given a set of params without a project, gets the project from the rest
-   * API project lookup and then sets the app params.
-   */
-  _normalizeLegacyRouteParams(
-    params: Readonly<
-      | GenerateUrlLegacyChangeViewParameters
-      | GenerateUrlLegacyDiffViewParameters
-    >
-  ) {
-    if (!params.changeNum) {
-      return Promise.resolve();
-    }
-
-    return this.restApiService
-      .getFromProjectLookup(params.changeNum)
-      .then(project => {
-        // Show a 404 and terminate if the lookup request failed. Attempting
-        // to redirect after failing to get the project loops infinitely.
-        if (!project) {
-          this._show404();
-          return;
-        }
-        const updatedParams:
-          | GenerateUrlChangeViewParameters
-          | GenerateUrlDiffViewParameters = {...params, project};
-        this._normalizePatchRangeParams(updatedParams);
-        this._redirect(this._generateUrl(updatedParams));
-      });
-  }
-
-  /**
    * Normalizes the params object, and determines if the URL needs to be
    * modified to fit the proper schema.
    *
@@ -1100,8 +1056,6 @@
 
     this._mapRoute(RoutePattern.CHANGE_LEGACY, '_handleChangeLegacyRoute');
 
-    this._mapRoute(RoutePattern.DIFF_LEGACY, '_handleDiffLegacyRoute');
-
     this._mapRoute(RoutePattern.AGREEMENTS, '_handleAgreementsRoute', true);
 
     this._mapRoute(
@@ -1666,41 +1620,26 @@
   }
 
   _handleChangeLegacyRoute(ctx: PageContextWithQueryMap) {
-    // Parameter order is based on the regex group number matched.
-    const params: GenerateUrlLegacyChangeViewParameters = {
-      changeNum: Number(ctx.params[0]) as NumericChangeId,
-      basePatchNum: convertToPatchSetNum(ctx.params[3]) as BasePatchSetNum,
-      patchNum: convertToPatchSetNum(ctx.params[5]),
-      view: GerritView.CHANGE,
-      querystring: ctx.querystring,
-    };
-
-    this._normalizeLegacyRouteParams(params);
+    const changeNum = Number(ctx.params[0]) as NumericChangeId;
+    if (!changeNum) {
+      this._show404();
+      return;
+    }
+    this.restApiService.getFromProjectLookup(changeNum).then(project => {
+      // Show a 404 and terminate if the lookup request failed. Attempting
+      // to redirect after failing to get the project loops infinitely.
+      if (!project) {
+        this._show404();
+        return;
+      }
+      this._redirect(`/c/${project}/+/${changeNum}/${ctx.params[1]}`);
+    });
   }
 
   _handleLegacyLinenum(ctx: PageContextWithQueryMap) {
     this._redirect(ctx.path.replace(LEGACY_LINENUM_PATTERN, '#$1'));
   }
 
-  _handleDiffLegacyRoute(ctx: PageContextWithQueryMap) {
-    // Parameter order is based on the regex group number matched.
-    const params: GenerateUrlLegacyDiffViewParameters = {
-      changeNum: Number(ctx.params[0]) as NumericChangeId,
-      basePatchNum: convertToPatchSetNum(ctx.params[2]) as BasePatchSetNum,
-      patchNum: convertToPatchSetNum(ctx.params[4]),
-      path: ctx.params[5],
-      view: GerritView.DIFF,
-    };
-
-    const address = this._parseLineAddress(ctx.hash);
-    if (address) {
-      params.leftSide = address.leftSide;
-      params.lineNum = address.lineNum;
-    }
-
-    this._normalizeLegacyRouteParams(params);
-  }
-
   _handleDiffEditRoute(ctx: PageContextWithQueryMap) {
     // Parameter order is based on the regex group number matched.
     const project = ctx.params[0] as RepoName;
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
index 4c7855b..b91bf0c 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
@@ -192,7 +192,6 @@
       '_handleDiffRoute',
       '_handleDefaultRoute',
       '_handleChangeLegacyRoute',
-      '_handleDiffLegacyRoute',
       '_handleDocumentationRedirectRoute',
       '_handleDocumentationSearchRoute',
       '_handleDocumentationSearchRedirectRoute',
@@ -536,66 +535,6 @@
   });
 
   suite('param normalization', () => {
-    let projectLookupStub;
-    let generateUrlStub;
-
-    setup(() => {
-      projectLookupStub = stubRestApi('getFromProjectLookup');
-      generateUrlStub = sinon.stub(element, '_generateUrl');
-    });
-
-    suite('_normalizeLegacyRouteParams', () => {
-      let rangeStub;
-      let redirectStub;
-      let show404Stub;
-
-      setup(() => {
-        rangeStub = sinon.stub(element, '_normalizePatchRangeParams')
-            .returns(Promise.resolve());
-        redirectStub = sinon.stub(element, '_redirect');
-        show404Stub = sinon.stub(element, '_show404');
-      });
-
-      test('w/o changeNum', () => {
-        projectLookupStub.returns(Promise.resolve('foo/bar'));
-        const params = {};
-        return element._normalizeLegacyRouteParams(params).then(() => {
-          assert.isFalse(generateUrlStub.calledOnce);
-          assert.isFalse(projectLookupStub.called);
-          assert.isFalse(rangeStub.called);
-          assert.isFalse(redirectStub.called);
-          assert.isFalse(show404Stub.called);
-        });
-      });
-
-      test('w/ changeNum', () => {
-        projectLookupStub.returns(Promise.resolve('foo/bar'));
-        const params = {changeNum: 1234};
-
-        return element._normalizeLegacyRouteParams(params).then(() => {
-          assert.isTrue(generateUrlStub.calledOnce);
-          const updatedParams = generateUrlStub.lastCall.args[0];
-          assert.isTrue(projectLookupStub.called);
-          assert.isTrue(rangeStub.called);
-          assert.equal(updatedParams.project, 'foo/bar');
-          assert.isTrue(redirectStub.calledOnce);
-          assert.isFalse(show404Stub.called);
-        });
-      });
-
-      test('halts on project lookup failure', () => {
-        projectLookupStub.returns(Promise.resolve(undefined));
-        const params = {changeNum: 1234};
-        return element._normalizeLegacyRouteParams(params).then(() => {
-          assert.isFalse(generateUrlStub.calledOnce);
-          assert.isTrue(projectLookupStub.called);
-          assert.isFalse(rangeStub.called);
-          assert.isFalse(redirectStub.called);
-          assert.isTrue(show404Stub.calledOnce);
-        });
-      });
-    });
-
     suite('_normalizePatchRangeParams', () => {
       test('range n..n normalizes to n', () => {
         const params = {basePatchNum: 4, patchNum: 4};
@@ -1367,58 +1306,19 @@
         assert.isTrue(redirectStub.calledWithExactly('/c/12345'));
       });
 
-      test('_handleChangeLegacyRoute', () => {
-        const normalizeRouteStub = sinon.stub(element,
-            '_normalizeLegacyRouteParams');
+      test('_handleChangeLegacyRoute', async () => {
+        stubRestApi('getFromProjectLookup').returns(Promise.resolve('project'));
         const ctx = {
           params: [
             1234, // 0 Change number
-            null, // 1 Unused
-            null, // 2 Unused
-            6, // 3 Base patch number
-            null, // 4 Unused
-            9, // 5 Patch number
+            'comment/6789',
           ],
           querystring: '',
         };
         element._handleChangeLegacyRoute(ctx);
-        assert.isTrue(normalizeRouteStub.calledOnce);
-        assert.deepEqual(normalizeRouteStub.lastCall.args[0], {
-          changeNum: 1234,
-          basePatchNum: 6,
-          patchNum: 9,
-          view: GerritView.CHANGE,
-          querystring: '',
-        });
-      });
-
-      test('_handleDiffLegacyRoute', () => {
-        const normalizeRouteStub = sinon.stub(element,
-            '_normalizeLegacyRouteParams');
-        const ctx = {
-          params: [
-            1234, // 0 Change number
-            null, // 1 Unused
-            3, // 2 Base patch number
-            null, // 3 Unused
-            8, // 4 Patch number
-            'foo/bar', // 5 Diff path
-          ],
-          path: '/c/1234/3..8/foo/bar',
-          hash: 'b123',
-        };
-        element._handleDiffLegacyRoute(ctx);
-        assert.isFalse(redirectStub.called);
-        assert.isTrue(normalizeRouteStub.calledOnce);
-        assert.deepEqual(normalizeRouteStub.lastCall.args[0], {
-          changeNum: 1234,
-          basePatchNum: 3,
-          patchNum: 8,
-          view: GerritView.DIFF,
-          path: 'foo/bar',
-          lineNum: 123,
-          leftSide: true,
-        });
+        await flush();
+        assert.isTrue(redirectStub.calledWithExactly('/c/project/+/1234' +
+            '/comment/6789'));
       });
 
       test('_handleLegacyLinenum w/ @321', () => {
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
index b79e448..2abedf1 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
@@ -21,16 +21,13 @@
 import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-change-table-editor_html';
-import {ChangeTableMixin} from '../../../mixins/gr-change-table-mixin/gr-change-table-mixin';
 import {customElement, property, observe} from '@polymer/decorators';
 import {ServerInfo} from '../../../types/common';
 import {appContext} from '../../../services/app-context';
-
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = ChangeTableMixin(PolymerElement);
+import {columnNames} from '../../change-list/gr-change-list/gr-change-list';
 
 @customElement('gr-change-table-editor')
-export class GrChangeTableEditor extends base {
+export class GrChangeTableEditor extends PolymerElement {
   static get template() {
     return htmlTemplate;
   }
@@ -51,18 +48,33 @@
 
   @observe('serverConfig')
   _configChanged(config: ServerInfo) {
-    this.defaultColumns = this.getEnabledColumns(
-      this.columnNames,
-      config,
-      this.flagsService.enabledExperiments
+    this.defaultColumns = columnNames.filter(col =>
+      this._isColumnEnabled(col, config, this.flagsService.enabledExperiments)
     );
     if (!this.displayedColumns) return;
     this.displayedColumns = this.displayedColumns.filter(column =>
-      this.isColumnEnabled(column, config, this.flagsService.enabledExperiments)
+      this._isColumnEnabled(
+        column,
+        config,
+        this.flagsService.enabledExperiments
+      )
     );
   }
 
   /**
+   * Is the column disabled by a server config or experiment? For example the
+   * assignee feature might be disabled and thus the corresponding column is
+   * also disabled.
+   *
+   */
+  _isColumnEnabled(column: string, config: ServerInfo, experiments: string[]) {
+    if (!config || !config.change) return true;
+    if (column === 'Assignee') return !!config.change.enable_assignee;
+    if (column === 'Comments') return experiments.includes('comments-column');
+    return true;
+  }
+
+  /**
    * Get the list of enabled column names from whichever checkboxes are
    * checked (excluding the number checkbox).
    */
@@ -79,6 +91,13 @@
       .map(checkbox => checkbox.name);
   }
 
+  _computeIsColumnHidden(columnToCheck?: string, columnsToDisplay?: string[]) {
+    if (!columnsToDisplay || !columnToCheck) {
+      return false;
+    }
+    return !columnsToDisplay.includes(columnToCheck);
+  }
+
   /**
    * Handle a click on a checkbox container and relay the click to the checkbox it
    * contains.
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.ts
index a05ec73..e756a20 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.ts
@@ -74,7 +74,7 @@
                 type="checkbox"
                 name="[[item]]"
                 on-click="_handleTargetClick"
-                checked$="[[!isColumnHidden(item, displayedColumns)]]"
+                checked$="[[!_computeIsColumnHidden(item, displayedColumns)]]"
               />
             </td>
           </tr>
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
index 4f61972..4f8d0a0 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
@@ -117,7 +117,7 @@
 
   test('_getDisplayedColumns', () => {
     const enabledColumns = columns.filter(column =>
-      element.isColumnEnabled(column, element.serverConfig!, [])
+      element._isColumnEnabled(column, element.serverConfig!, [])
     );
     assert.deepEqual(element._getDisplayedColumns(), enabledColumns);
     const input = queryAndAssert<HTMLInputElement>(
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
index da4bb0a..61498f6 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
@@ -47,7 +47,6 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-settings-view_html';
 import {getDocsBaseUrl} from '../../../utils/url-util';
-import {ChangeTableMixin} from '../../../mixins/gr-change-table-mixin/gr-change-table-mixin';
 import {customElement, property, observe} from '@polymer/decorators';
 import {AppElementParams} from '../../gr-app-types';
 import {GrAccountInfo} from '../gr-account-info/gr-account-info';
@@ -76,6 +75,7 @@
   EmailStrategy,
   TimeFormat,
 } from '../../../constants/constants';
+import {columnNames} from '../../change-list/gr-change-list/gr-change-list';
 
 const PREFS_SECTION_FIELDS: Array<keyof PreferencesInput> = [
   'changes_per_page',
@@ -139,11 +139,8 @@
   };
 }
 
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = ChangeTableMixin(PolymerElement);
-
 @customElement('gr-settings-view')
-export class GrSettingsView extends base {
+export class GrSettingsView extends PolymerElement {
   static get template() {
     return htmlTemplate;
   }
@@ -262,8 +259,10 @@
         this._localMenu = this._cloneMenu(prefs.my);
         this._localChangeTableColumns =
           prefs.change_table.length === 0
-            ? this.columnNames
-            : this.renameProjectToRepoColumn(prefs.change_table);
+            ? columnNames
+            : prefs.change_table.map(column =>
+                column === 'Project' ? 'Repo' : column
+              );
       })
     );
 
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 418cd0e..1f2983a 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -675,7 +675,13 @@
 
   @observe('comment.message')
   _commentMessageChanged(message: string) {
-    this._messageText = message || '';
+    /*
+     * Only overwrite the message text user has typed if there is no existing
+     * text typed by the user. This prevents the bug where creating another
+     * comment triggered a recomputation of comments and the text written by
+     * the user was lost.
+     */
+    if (!this._messageText) this._messageText = message || '';
   }
 
   _messageTextChanged(_: string, oldValue: string) {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
index 31a1614..b963d4b 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
@@ -217,6 +217,29 @@
       assert.isTrue(storageStub.called);
     });
 
+    test('comment message sets messageText only when empty', () => {
+      element.changeNum = 1 as NumericChangeId;
+      element.patchNum = 1 as PatchSetNum;
+      element._messageText = '';
+      element.comment = {
+        author: {
+          name: 'Mr. Peanutbutter',
+          email: 'tenn1sballchaser@aol.com' as EmailAddress,
+        },
+        line: 5,
+        path: 'test',
+        __editing: true,
+        __draft: true,
+        message: 'hello world',
+      };
+      // messageText was empty so overwrite the message now
+      assert.equal(element._messageText, 'hello world');
+
+      element.comment!.message = 'new message';
+      // messageText was already set so do not overwrite it
+      assert.equal(element._messageText, 'hello world');
+    });
+
     test('_getPatchNum', () => {
       element.side = 'PARENT';
       element.patchNum = 1 as PatchSetNum;
diff --git a/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin.ts b/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin.ts
deleted file mode 100644
index 9e4608f..0000000
--- a/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin.ts
+++ /dev/null
@@ -1,118 +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 {PolymerElement} from '@polymer/polymer';
-import {Constructor} from '../../utils/common-util';
-import {property} from '@polymer/decorators';
-import {ServerInfo} from '../../types/common';
-
-/**
- * @polymer
- * @mixinFunction
- */
-export const ChangeTableMixin = <T extends Constructor<PolymerElement>>(
-  superClass: T
-) => {
-  /**
-   * @polymer
-   * @mixinClass
-   */
-  class Mixin extends superClass {
-    @property({type: Array})
-    readonly columnNames: string[] = [
-      'Subject',
-      'Status',
-      'Owner',
-      'Assignee',
-      'Reviewers',
-      'Comments',
-      'Repo',
-      'Branch',
-      'Updated',
-      'Size',
-    ];
-
-    isColumnHidden(columnToCheck?: string, columnsToDisplay?: string[]) {
-      if (!columnsToDisplay || !columnToCheck) {
-        return false;
-      }
-      return !columnsToDisplay.includes(columnToCheck);
-    }
-
-    /**
-     * Is the column disabled by a server config or experiment? For example the
-     * assignee feature might be disabled and thus the corresponding column is
-     * also disabled.
-     *
-     */
-    isColumnEnabled(column: string, config: ServerInfo, experiments: string[]) {
-      if (!config || !config.change) return true;
-      if (column === 'Assignee') return !!config.change.enable_assignee;
-      if (column === 'Comments') return experiments.includes('comments-column');
-      return true;
-    }
-
-    /**
-     * @return enabled columns, see isColumnEnabled().
-     */
-    getEnabledColumns(
-      columns: string[],
-      config: ServerInfo,
-      experiments: string[]
-    ) {
-      return columns.filter(col =>
-        this.isColumnEnabled(col, config, experiments)
-      );
-    }
-
-    /**
-     * The Project column was renamed to Repo, but some users may have
-     * preferences that use its old name. If that column is found, rename it
-     * before use.
-     *
-     * @return If the column was renamed, returns a new array
-     * with the corrected name. Otherwise, it returns the original param.
-     */
-    renameProjectToRepoColumn(columns: string[]) {
-      const projectIndex = columns.indexOf('Project');
-      if (projectIndex === -1) {
-        return columns;
-      }
-      const newColumns = [...columns];
-      newColumns[projectIndex] = 'Repo';
-      return newColumns;
-    }
-  }
-
-  return Mixin as T & Constructor<ChangeTableMixinInterface>;
-};
-
-export interface ChangeTableMixinInterface {
-  readonly columnNames: string[];
-  isColumnHidden(columnToCheck?: string, columnsToDisplay?: string[]): boolean;
-  isColumnEnabled(
-    column: string,
-    config: ServerInfo,
-    experiments: string[]
-  ): boolean;
-  getEnabledColumns(
-    columns: string[],
-    config: ServerInfo,
-    experiments: string[]
-  ): string[];
-  renameProjectToRepoColumn(columns: string[]): string[];
-}
diff --git a/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin_test.js b/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin_test.js
deleted file mode 100644
index 8bc223f..0000000
--- a/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin_test.js
+++ /dev/null
@@ -1,81 +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 '../../test/common-test-setup-karma.js';
-import {ChangeTableMixin} from './gr-change-table-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = ChangeTableMixin(PolymerElement);
-
-class GrChangeTableMixinTestElement extends base {
-  static get is() { return 'gr-change-table-mixin-test-element'; }
-}
-
-customElements.define(GrChangeTableMixinTestElement.is,
-    GrChangeTableMixinTestElement);
-
-const basicFixture = fixtureFromElement(
-    'gr-change-table-mixin-test-element');
-
-suite('gr-change-table-mixin tests', () => {
-  let element;
-
-  setup(() => {
-    element = basicFixture.instantiate();
-  });
-
-  test('isColumnHidden', () => {
-    const columnToCheck = 'Repo';
-    let columnsToDisplay = [
-      'Subject',
-      'Status',
-      'Owner',
-      'Assignee',
-      'Repo',
-      'Branch',
-      'Updated',
-      'Size',
-    ];
-    assert.isFalse(element.isColumnHidden(columnToCheck, columnsToDisplay));
-
-    columnsToDisplay = [
-      'Subject',
-      'Status',
-      'Owner',
-      'Assignee',
-      'Branch',
-      'Updated',
-      'Size',
-    ];
-    assert.isTrue(element.isColumnHidden(columnToCheck, columnsToDisplay));
-  });
-
-  test('renameProjectToRepoColumn maps Project to Repo', () => {
-    const columns = [
-      'Subject',
-      'Status',
-      'Owner',
-    ];
-    assert.deepEqual(element.renameProjectToRepoColumn(columns),
-        columns.slice(0));
-    assert.deepEqual(
-        element.renameProjectToRepoColumn(columns.concat(['Project'])),
-        columns.slice(0).concat(['Repo']));
-  });
-});
-