Merge "Allow assigning code ownership to all users"
diff --git a/ui/code-owners-service.js b/ui/code-owners-service.js
index ae7b2f4..15bc3e0 100644
--- a/ui/code-owners-service.js
+++ b/ui/code-owners-service.js
@@ -64,7 +64,8 @@
    */
   listOwnersForPath(project, branch, path) {
     return this.restApi.get(
-        `/projects/${project}/branches/${branch}/code_owners/${encodeURIComponent(path)}?limit=5&o=DETAILS`
+        `/projects/${project}/branches/${branch}/` +
+        `code_owners/${encodeURIComponent(path)}?limit=5&o=DETAILS`
     );
   }
 
@@ -134,8 +135,9 @@
     return this.getStatus()
         .then(({codeOwnerStatusMap}) => {
           // only fetch those not approved yet
-          let filesToFetchOwners = [...codeOwnerStatusMap.keys()].filter(
-              file => codeOwnerStatusMap.get(file).status !== OwnerStatus.APPROVED
+          const filesToFetchOwners = [...codeOwnerStatusMap.keys()].filter(
+              file => codeOwnerStatusMap
+                  .get(file).status !== OwnerStatus.APPROVED
           );
           return this.batchFetchCodeOwners(filesToFetchOwners)
               .then(ownersMap =>
@@ -188,7 +190,10 @@
     const ownersFilesMap = new Map();
     const failedToFetchFiles = new Set();
     for (let i = 0; i < allFiles.length; i++) {
-      const fileInfo = {path: allFiles[i], status: this._computeFileStatus(codeOwnerStatusMap, allFiles[i])};
+      const fileInfo = {
+        path: allFiles[i],
+        status: this._computeFileStatus(codeOwnerStatusMap, allFiles[i]),
+      };
       // for files failed to fetch, add them to the special group
       if (fileOwnersMap[fileInfo.path].error) {
         failedToFetchFiles.add(fileInfo);
@@ -200,7 +205,10 @@
           .map(account => account._account_id)
           .sort()
           .join(',');
-      ownersFilesMap.set(ownersKey, ownersFilesMap.get(ownersKey) || {files: [], owners});
+      ownersFilesMap.set(
+          ownersKey,
+          ownersFilesMap.get(ownersKey) || {files: [], owners}
+      );
       ownersFilesMap.get(ownersKey).files.push(fileInfo);
     }
     const groupedItems = [];
@@ -218,7 +226,7 @@
       groupedItems.push({
         groupName: this.getGroupName(failedFiles),
         files: failedFiles,
-        error: new Error("Failed to fetch owner info")
+        error: new Error('Failed to fetch owner info'),
       });
     }
 
@@ -227,9 +235,10 @@
 
   getGroupName(files) {
     const fileName = files[0].path.split('/').pop();
-    return `${
-      files.length > 1 ? `(${files.length} files) ${fileName}, ...` : fileName
-    }`;
+    return {
+      name: fileName,
+      prefix: files.length > 1 ? `+ ${files.length - 1} more` : '',
+    };
   }
 
   /**
diff --git a/ui/owner-requirement.js b/ui/owner-requirement.js
index 111b9d9..140ccda 100644
--- a/ui/owner-requirement.js
+++ b/ui/owner-requirement.js
@@ -56,7 +56,9 @@
         <template is="dom-if" if="[[!isLoading]]">
           <span>[[statusText]]</span>
           <template is="dom-if" if="[[!allApproved]]">
-            <gr-button link on-click="_openReplyDialog">Suggest owners</gr-button>
+            <gr-button link on-click="_openReplyDialog">
+            Suggest owners
+          </gr-button>
           </template>
         </template>
       `;
@@ -85,7 +87,8 @@
         .then(({rawStatuses}) => {
           const statusText = [];
           const statusCount = this._computeStatusCount(rawStatuses);
-          this.allApproved = statusCount.missing === 0 && statusCount.pending === 0;
+          this.allApproved = statusCount.missing === 0
+            && statusCount.pending === 0;
           if (statusCount.missing) {
             statusText.push(`${statusCount.missing} missing`);
           }
diff --git a/ui/owner-status-column.js b/ui/owner-status-column.js
index d413272..1e5e9fe 100644
--- a/ui/owner-status-column.js
+++ b/ui/owner-status-column.js
@@ -206,7 +206,7 @@
             this.status = newPathStatus;
           } else {
             this.status = newPathStatus === STATUS_CODE.APPROVED
-              ? this._computeStatus(oldPathStatus, /* oldPath=*/ true)
+              ? this._computeStatus(oldPathStatus, /* oldPath= */ true)
               : newPathStatus;
           }
         })
diff --git a/ui/plugin.js b/ui/plugin.js
index e77e406..4ac253a 100644
--- a/ui/plugin.js
+++ b/ui/plugin.js
@@ -15,9 +15,10 @@
  * limitations under the License.
  */
 
-import {SuggestOwners, SuggestOwnersTrigger} from './suggest-owners.js';
+import {SuggestOwners} from './suggest-owners.js';
 import {OwnerStatusColumnContent, OwnerStatusColumnHeader} from './owner-status-column.js';
 import {OwnerRequirementValue} from './owner-requirement.js';
+import {SuggestOwnersTrigger} from './suggest-owners-trigger.js';
 
 Gerrit.install(plugin => {
   const ENABLED_EXPERIMENTS = window.ENABLED_EXPERIMENTS || [];
diff --git a/ui/suggest-owners-trigger.js b/ui/suggest-owners-trigger.js
new file mode 100644
index 0000000..5ef18d6
--- /dev/null
+++ b/ui/suggest-owners-trigger.js
@@ -0,0 +1,99 @@
+/**
+ * @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 {CodeOwnerService, OwnerStatus} from './code-owners-service.js';
+import {ownerState} from './owner-ui-state.js';
+
+export class SuggestOwnersTrigger extends Polymer.Element {
+  static get is() {
+    return 'suggest-owners-trigger';
+  }
+
+  static get properties() {
+    return {
+      change: Object,
+      expanded: {
+        type: Boolean,
+        value: false,
+      },
+      restApi: Object,
+      hidden: {
+        type: Boolean,
+        value: true,
+        reflectToAttribute: true,
+      },
+    };
+  }
+
+  static get observers() {
+    return ['onInputChanged(restApi, change)'];
+  }
+
+  static get template() {
+    return Polymer.html`
+        <style include="shared-styles">
+          iron-icon {
+            padding-left: var(--spacing-m);
+          }
+        </style>
+        <gr-button
+          on-click="toggleControlContent"
+          has-tooltip
+          title="Suggest owners for your change"
+        >
+          [[computeButtonText(expanded)]]
+          <iron-icon icon="gr-icons:info-outline"></iron-icon>
+        </gr-button>
+      `;
+  }
+
+  connectedCallback() {
+    super.connectedCallback();
+    ownerState.onExpandSuggestionChange(expanded => {
+      this.expanded = expanded;
+    });
+  }
+
+  onInputChanged(restApi, change) {
+    if ([restApi, change].includes(undefined)) return;
+    this.ownerService = CodeOwnerService.getOwnerService(
+        this.restApi,
+        this.change
+    );
+    this.ownerService.getStatus().then(({rawStatuses}) => {
+      const notAllApproved = rawStatuses.some(status => {
+        const oldPathStatus = status.old_path_status;
+        const newPathStatus = status.new_path_status;
+        if (newPathStatus.status !== OwnerStatus.APPROVED) {
+          return true;
+        }
+        return oldPathStatus && oldPathStatus.status !== OwnerStatus.APPROVED;
+      });
+      this.hidden = !notAllApproved;
+    });
+  }
+
+  toggleControlContent() {
+    this.expanded = !this.expanded;
+    ownerState.expandSuggestion = this.expanded;
+  }
+
+  computeButtonText(expanded) {
+    return expanded ? 'Hide owners' : 'Suggest owners';
+  }
+}
+
+customElements.define(SuggestOwnersTrigger.is, SuggestOwnersTrigger);
\ No newline at end of file
diff --git a/ui/suggest-owners.js b/ui/suggest-owners.js
index 7d6ad6a..ca38cda 100644
--- a/ui/suggest-owners.js
+++ b/ui/suggest-owners.js
@@ -14,91 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {CodeOwnerService, OwnerStatus, RenamedFileChip} from './code-owners-service.js';
+import {CodeOwnerService, RenamedFileChip} from './code-owners-service.js';
 import {ownerState} from './owner-ui-state.js';
 
-export class SuggestOwnersTrigger extends Polymer.Element {
-  static get is() {
-    return 'suggest-owners-trigger';
-  }
-
-  static get properties() {
-    return {
-      change: Object,
-      expanded: {
-        type: Boolean,
-        value: false,
-      },
-      restApi: Object,
-      hidden: {
-        type: Boolean,
-        value: true,
-        reflectToAttribute: true,
-      },
-    };
-  }
-
-
-  static get observers() {
-    return [
-      'onInputChanged(restApi, change)',
-    ];
-  }
-
-  static get template() {
-    return Polymer.html`
-      <style include="shared-styles">
-        iron-icon {
-          padding-left: var(--spacing-m);
-        }
-      </style>
-      <gr-button
-        on-click="toggleControlContent"
-        has-tooltip
-        title="Suggest owners for your change"
-      >
-        [[computeButtonText(expanded)]]
-        <iron-icon icon="gr-icons:info-outline"></iron-icon>
-      </gr-button>
-    `;
-  }
-
-  connectedCallback() {
-    super.connectedCallback();
-    ownerState.onExpandSuggestionChange(expanded => {
-      this.expanded = expanded;
-    });
-  }
-
-  onInputChanged(restApi, change) {
-    if ([restApi, change].includes(undefined)) return;
-    this.ownerService = CodeOwnerService
-        .getOwnerService(this.restApi, this.change);
-    this.ownerService.getStatus().then(({rawStatuses}) => {
-      const notAllApproved = rawStatuses.some(status => {
-        const oldPathStatus = status.old_path_status;
-        const newPathStatus = status.new_path_status;
-        if (newPathStatus.status !== OwnerStatus.APPROVED) {
-          return true;
-        }
-        return oldPathStatus && oldPathStatus.status !== OwnerStatus.APPROVED;
-      });
-      this.hidden = !notAllApproved;
-    });
-  }
-
-  toggleControlContent() {
-    this.expanded = !this.expanded;
-    ownerState.expandSuggestion = this.expanded;
-  }
-
-  computeButtonText(expanded) {
-    return expanded ? 'Hide owners' : 'Suggest owners';
-  }
-}
-
-customElements.define(SuggestOwnersTrigger.is, SuggestOwnersTrigger);
-
 class OwnerGroupFileList extends Polymer.Element {
   static get is() {
     return 'owner-group-file-list';
@@ -121,10 +39,10 @@
         color: var(--primary-text-color);
         font-size: var(--font-size-small);
       }
-      span.renamed-old {
+      .renamed-old {
         background-color: var(--dark-remove-highlight-color);
       }
-      span.renamed-new {
+      .renamed-new {
         background-color: var(--dark-add-highlight-color);
       }
       </style>
@@ -138,7 +56,9 @@
             [[computeFilePath(file)]]<!--
             --><strong>[[computeFileName(file)]]</strong>
             <template is="dom-if" if="[[file.status]]">
-              <span class$="[[computeStatusClass(file)]]">[[computeFileStatus(file)]]</span>
+              <span class$="[[computeStatusClass(file)]]">
+                [[computeFileStatus(file)]]
+              </span>
             </template>
           </li>
         </template>
@@ -147,12 +67,12 @@
   }
 
   computeFilePath(file) {
-    const parts = file.path.split("/");
-    return parts.slice(0, parts.length - 2).join("/") + "/";
+    const parts = file.path.split('/');
+    return parts.slice(0, parts.length - 2).join('/') + '/';
   }
 
   computeFileName(file) {
-    const parts = file.path.split("/");
+    const parts = file.path.split('/');
     return parts.pop();
   }
 
@@ -164,7 +84,6 @@
     if (!file.status) return '';
     return file.status === RenamedFileChip.NEW ? 'renamed-new' : 'renamed-old';
   }
-
 }
 
 customElements.define(OwnerGroupFileList.is, OwnerGroupFileList);
@@ -209,13 +128,17 @@
         .suggestion-row:hover {
           background: var(--hover-background-color);
         }
-        .suggestion-grou-name {
+        .suggestion-group-name {
           width: 200px;
+          line-height: 26px;
           text-overflow: ellipsis;
           overflow: hidden;
           padding-right: var(--spacing-l);
           white-space: nowrap;
         }
+        .group-name-prefix {
+          color: var(--deemphasized-text-color);
+        }
         .suggested-owners {
           flex: 1;
         }
@@ -252,9 +175,16 @@
           index-as="suggestionIndex"
         >
           <li class="suggestion-row">
-            <div class="suggestion-grou-name">
+            <div class="suggestion-group-name">
               <span>
-                [[suggestion.groupName]]
+                <template is="dom-if" if="[[suggestion.groupName.prefix]]">
+                  <span class="group-name-prefix">
+                    ([[suggestion.groupName.prefix]])
+                  </span>
+                </template>
+                <span>
+                  [[suggestion.groupName.name]]
+                </span>
                 <gr-hovercard hidden="[[suggestion.expanded]]">
                   <owner-group-file-list
                     files="[[suggestion.files]]"
@@ -271,6 +201,9 @@
               [[suggestion.error]]
             </template>
             <template is="dom-if" if="[[!suggestion.error]]">
+              <template is="dom-if" if="[[!suggestion.owners.length]]">
+                All owners are not visible to you or missing required permissions.
+              </template>
               <ul class="suggested-owners">
                 <template
                   is="dom-repeat"
@@ -404,7 +337,8 @@
 
   toggleAccount(e) {
     const grAccountLabel = e.currentTarget;
-    const owner = this.suggestedOwners[grAccountLabel.dataset.suggestionIndex].owners[grAccountLabel.dataset.ownerIndex];
+    const owner = this.suggestedOwners[grAccountLabel.dataset.suggestionIndex]
+        .owners[grAccountLabel.dataset.ownerIndex];
     if (this.isSelected(owner)) {
       this.removeAccount(owner);
     } else {
@@ -418,7 +352,8 @@
     this.suggestedOwners.forEach((suggestion, sId) => {
       suggestion.owners.forEach((owner, oId) => {
         if (
-          accounts.some(account => account._account_id === owner.account._account_id)
+          accounts.some(account => account._account_id
+              === owner.account._account_id)
         ) {
           this.set(
               ['suggestedOwners', sId, 'owners', oId],