Merge "Merge branch 'stable-3.0'"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index 3f643dc..6ac2195 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -66,6 +66,9 @@
       .invisible {
         visibility: hidden;
       }
+      .header-row {
+        font-weight: var(--font-weight-bold);
+      }
       .controlRow {
         align-items: center;
         display: flex;
@@ -126,22 +129,27 @@
       .oldPath {
         color: var(--deemphasized-text-color);
       }
-      .comments,
+      .header-stats {
+        text-align: center;
+        min-width: 7.5em;
+      }
       .stats {
         text-align: right;
+        min-width: 7.5em;
       }
       .comments {
         padding-left: 2em;
+        min-width: 20em;
       }
-      .stats {
-        min-width: 7em;
-      }
-      .row:not(.header) .stats,
+      .row:not(.header-row) .stats,
       .total-stats {
         font-family: var(--monospace-font-family);
+        display: flex;
       }
       .sizeBars {
         margin-left: .5em;
+        min-width: 7em;
+        text-align: center;
       }
       .sizeBars.hide {
         display: none;
@@ -157,6 +165,8 @@
       .removed {
         color: var(--vote-text-color-disliked);
         text-align: left;
+        min-width: 4em;
+        padding-left: 0.5em;
       }
       .drafts {
         color: #C62828;
@@ -277,6 +287,18 @@
     <div
         id="container"
         on-tap="_handleFileListTap">
+      <div class="header-row row">
+        <div class="status"></div>
+        <div class="path">File</div>
+        <div class="comments">Comments</div>
+        <div class="sizeBars">Size</div>
+        <div class="header-stats">Delta</div>
+        <!-- Empty div here exists to keep spacing in sync with file rows. -->
+        <div class="reviewed hideOnEdit" hidden$="[[!_loggedIn]]"></div>
+        <div class="editFileControls showOnEdit"></div>
+        <div class="show-hide"></div>
+      </div>
+
       <template is="dom-repeat"
           items="[[_shownFiles]]"
           id="files"
@@ -362,6 +384,20 @@
                 [[_formatPercentage(file.size, file.size_delta)]]
               </span>
             </div>
+            <template is="dom-if" if="[[_showDynamicColumns]]">
+              <template is="dom-repeat" items="[[_dynamicContentEndpoints]]" as="contentEndpoint">
+                <div class$="[[_computeClass('', file.__path)]]">
+                  <gr-endpoint-decorator name="[[contentEndpoint]]">
+                    <gr-endpoint-param name="changeNum" value="[[changeNum]]">
+                    </gr-endpoint-param>
+                    <gr-endpoint-param name="patchRange" value="[[patchRange]]">
+                    </gr-endpoint-param>
+                    <gr-endpoint-param name="path" value="[[file.__path]]">
+                    </gr-endpoint-param>
+                  </gr-endpoint-decorator>
+                </div>
+              </template>
+            </template>
             <div class="reviewed hideOnEdit" hidden$="[[!_loggedIn]]" hidden>
               <span class$="reviewedLabel [[_computeReviewedClass(file.isReviewed)]]">Reviewed</span>
               <label>
@@ -426,8 +462,14 @@
           -[[_patchChange.deleted]]
         </span>
       </div>
+      <template is="dom-if" if="[[_showDynamicColumns]]">
+        <template is="dom-repeat" items="[[_dynamicSummaryEndpoints]]" as="summaryEndpoint">
+          <gr-endpoint-decorator name="[[summaryEndpoint]]">
+          </gr-endpoint-decorator>
+        </template>
+      </template>
       <!-- Empty div here exists to keep spacing in sync with file rows. -->
-      <div class="reviewed hideOnEdit" hidden$="[[!_loggedIn]]" hidden></div>
+      <div class="reviewed hideOnEdit" hidden$="[[!_loggedIn]]"></div>
       <div class="editFileControls showOnEdit"></div>
       <div class="show-hide"></div>
     </div>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index ef69ae4..55785f4 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -180,6 +180,23 @@
 
       /** @type {Function} */
       _cancelForEachDiff: Function,
+
+      _showDynamicColumns: {
+        type: Boolean,
+        computed: '_computeShowDynamicColumns(_dynamicHeaderEndpoints)',
+      },
+      /** @type {Array<string>} */
+      _dynamicHeaderEndpoints: {
+        type: Array,
+      },
+      /** @type {Array<string>} */
+      _dynamicContentEndpoints: {
+        type: Array,
+      },
+      /** @type {Array<string>} */
+      _dynamicSummaryEndpoints: {
+        type: Array,
+      },
     },
 
     behaviors: [
@@ -229,6 +246,28 @@
       keydown: '_scopedKeydownHandler',
     },
 
+    attached() {
+      Gerrit.awaitPluginsLoaded().then(() => {
+        this._dynamicHeaderEndpoints = Gerrit._endpoints.getDynamicEndpoints(
+            'change-view-file-list-header');
+        this._dynamicContentEndpoints = Gerrit._endpoints.getDynamicEndpoints(
+            'change-view-file-list-content');
+        this._dynamicSummaryEndpoints = Gerrit._endpoints.getDynamicEndpoints(
+            'change-view-file-list-summary');
+
+        if (this._dynamicHeaderEndpoints.length !==
+            this._dynamicContentEndpoints.length) {
+          console.warn(
+              'Different number of dynamic file-list header and content.');
+        }
+        if (this._dynamicHeaderEndpoints.length !==
+            this._dynamicSummaryEndpoints.length) {
+          console.warn(
+              'Different number of dynamic file-list headers and summary.');
+        }
+      });
+    },
+
     detached() {
       this._cancelDiffs();
     },
@@ -805,7 +844,10 @@
      * @param {string} path
      */
     _computeClass(baseClass, path) {
-      const classes = [baseClass];
+      const classes = [];
+      if (baseClass) {
+        classes.push(baseClass);
+      }
       if (path === this.COMMIT_MESSAGE_PATH || path === this.MERGE_LIST_PATH) {
         classes.push('invisible');
       }
@@ -1253,6 +1295,15 @@
       return `sizeBars desktop ${hideClass}`;
     },
 
+    _computeShowDynamicColumns(dynamicHeaderEndpoints) {
+      // During a design review, it was decided that dynamic columns should
+      // remain hidden until column headers (including existing columns such as
+      // "Comments") are in place to avoid confusion.
+      // TODO(crbug.com/939904): Enable dispaying dynamic columns when there is
+      // at least one of them registered.
+      return false;
+    },
+
     /**
      * Returns true if none of the inline diffs have been expanded.
      * @return {boolean}
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index 22df071..8db000c 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -802,7 +802,7 @@
 
       flushAsynchronousOperations();
       const fileRows =
-          Polymer.dom(element.root).querySelectorAll('.row:not(.header)');
+          Polymer.dom(element.root).querySelectorAll('.row:not(.header-row)');
       const checkSelector = 'input.reviewed[type="checkbox"]';
       const commitMsg = fileRows[0].querySelector(checkSelector);
       const fileAdded = fileRows[1].querySelector(checkSelector);
@@ -933,7 +933,7 @@
       sandbox.stub(element, '_expandedPathsChanged');
       flushAsynchronousOperations();
       const fileRows =
-          Polymer.dom(element.root).querySelectorAll('.row:not(.header)');
+          Polymer.dom(element.root).querySelectorAll('.row:not(.header-row)');
       // Because the label surrounds the input, the tap event is triggered
       // there first.
       const showHideLabel = fileRows[0].querySelector('label.show-hide');
@@ -960,7 +960,7 @@
 
       // Tap on a file to generate the diff.
       const row = Polymer.dom(element.root)
-          .querySelectorAll('.row:not(.header) label.show-hide')[0];
+          .querySelectorAll('.row:not(.header-row) label.show-hide')[0];
 
       MockInteractions.tap(row);
       flushAsynchronousOperations();
@@ -990,7 +990,7 @@
       sandbox.stub(element, '_expandedPathsChanged');
       flushAsynchronousOperations();
       const commitMsgFile = Polymer.dom(element.root)
-          .querySelectorAll('.row:not(.header) a.pathLink')[0];
+          .querySelectorAll('.row:not(.header-row) a.pathLink')[0];
 
       // Remove href attribute so the app doesn't route to a diff view
       commitMsgFile.removeAttribute('href');
@@ -1570,7 +1570,7 @@
         nextChunkStub = sandbox.stub(element.$.diffCursor,
             'moveToNextChunk');
         fileRows =
-            Polymer.dom(element.root).querySelectorAll('.row:not(.header)');
+            Polymer.dom(element.root).querySelectorAll('.row:not(.header-row)');
       });
 
       test('n key with some files expanded and no shift key', () => {
@@ -1700,7 +1700,8 @@
       // Commit message should not have edit controls.
       const editControls =
           Array.from(
-              Polymer.dom(element.root).querySelectorAll('.row:not(.header)'))
+              Polymer.dom(element.root)
+              .querySelectorAll('.row:not(.header-row)'))
               .map(row => row.querySelector('gr-edit-file-controls'));
       assert.isTrue(editControls[0].classList.contains('invisible'));
     });