Merge "Separate CCs from reviewers when NoteDb is enabled"
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
index 23f0b13..c5827d0 100644
--- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
+++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
@@ -26,6 +26,7 @@
     properties: {
       borderless: Boolean,
       change: Object,
+      filter: Function,
       placeholder: String,
 
       suggestFrom: {
@@ -33,30 +34,14 @@
         value: 3,
       },
 
-      filter: {
-        type: Function,
-        value: function() {
-          return this.notOwnerOrReviewer.bind(this);
-        },
-      },
-
       query: {
         type: Function,
         value: function() {
           return this._getReviewerSuggestions.bind(this);
         },
       },
-
-      _reviewers: {
-        type: Array,
-        value: function() { return []; },
-      },
     },
 
-    observers: [
-      '_reviewersChanged(change.reviewers.*, change.owner)',
-    ],
-
     get focusStart() {
       return this.$.input.focusStart;
     },
@@ -73,30 +58,6 @@
       this.fire('add', {value: e.detail.value});
     },
 
-    _reviewersChanged: function(changeRecord, owner) {
-      var reviewerSet = {};
-      reviewerSet[owner._account_id] = true;
-      var addReviewers = function(reviewers) {
-        if (!reviewers) {
-          return;
-        }
-        reviewers.forEach(function(reviewer) {
-          reviewerSet[reviewer._account_id] = true;
-        });
-      };
-
-      var reviewers = changeRecord.base;
-      addReviewers(reviewers.CC);
-      addReviewers(reviewers.REVIEWER);
-      this._reviewers = reviewerSet;
-    },
-
-    notOwnerOrReviewer: function(reviewer) {
-      var account = reviewer.account;
-      if (!account) { return true; }
-      return !this._reviewers[reviewer.account._account_id];
-    },
-
     _makeSuggestion: function(reviewer) {
       if (reviewer.account) {
         return {
@@ -117,6 +78,7 @@
 
       return xhr.then(function(reviewers) {
         if (!reviewers) { return []; }
+        if (!this.filter) { return reviewers.map(this._makeSuggestion); }
         return reviewers
             .filter(this.filter)
             .map(this._makeSuggestion);
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
index f9527ca..94db890 100644
--- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
+++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
@@ -84,15 +84,6 @@
       });
     });
 
-    test('notOwnerOrReviewer', function() {
-      var account = makeAccount();
-      assert.isTrue(element.notOwnerOrReviewer({}));
-      assert.isTrue(element.notOwnerOrReviewer({account: account}));
-      assert.isFalse(element.notOwnerOrReviewer({account: owner}));
-      assert.isFalse(element.notOwnerOrReviewer({account: existingReviewer1}));
-      assert.isFalse(element.notOwnerOrReviewer({account: existingReviewer2}));
-    });
-
     test('_makeSuggestion formats account or group accordingly', function() {
       var account = makeAccount();
       var suggestion = element._makeSuggestion({account: account});
@@ -111,36 +102,20 @@
 
     test('_getReviewerSuggestions excludes owner+reviewers', function(done) {
       element._getReviewerSuggestions().then(function(reviewers) {
-        assert.deepEqual(reviewers, [
-            element._makeSuggestion(suggestion1),
-            element._makeSuggestion(suggestion2),
-            element._makeSuggestion(suggestion3),
-        ]);
-        done();
+        // Default is no filtering.
+        assert.equal(reviewers.length, 6);
+
+        // Set up filter that only accepts suggestion1.
+        var accountId = suggestion1.account._account_id;
+        element.filter = function(suggestion) {
+          return suggestion.account &&
+              suggestion.account._account_id === accountId;
+        };
+
+        element._getReviewerSuggestions().then(function(reviewers) {
+          assert.deepEqual(reviewers, [element._makeSuggestion(suggestion1)]);
+        }).then(done);
       });
     });
-
-    test('_updateReviewers', function() {
-      // delete existingReviewer1
-      element.splice('change.reviewers.CC', 0, 1);
-      var expected = {};
-      expected[owner._account_id] = true;
-      expected[existingReviewer2._account_id] = true;
-      assert.deepEqual(element._reviewers, expected);
-
-      // delete existingReviewer2
-      element.splice('change.reviewers.REVIEWER', 0, 1);
-      delete expected[existingReviewer2._account_id];
-      assert.deepEqual(element._reviewers, expected);
-
-      // add two new reviewers
-      var account1 = makeAccount();
-      var account2 = makeAccount();
-      element.push('change.reviewers.CC', account1);
-      element.push('change.reviewers.REVIEWER', account2);
-      expected[account1._account_id] = true;
-      expected[account2._account_id] = true;
-      assert.deepEqual(element._reviewers, expected);
-    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
index c7de5d7..9f7f43f 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
+++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
@@ -23,6 +23,7 @@
         value: function() { return []; },
       },
       change: Object,
+      filter: Function,
       placeholder: String,
       pendingConfirmation: {
         type: Object,
@@ -30,13 +31,6 @@
         notify: true,
       },
       readonly: Boolean,
-
-      filter: {
-        type: Function,
-        value: function() {
-          return this._filterSuggestion.bind(this);
-        },
-      },
     },
 
     listeners: {
@@ -90,31 +84,6 @@
       return !this.readonly && !!account._pendingAdd;
     },
 
-    _filterSuggestion: function(reviewer) {
-      // If the reviewer is already on the change.
-      if (!this.$.entry.notOwnerOrReviewer(reviewer)) {
-        return false;
-      }
-
-      // If the reviewer is in the pending list to be added to the change.
-      for (var i = 0; i < this.accounts.length; i++) {
-        var account = this.accounts[i];
-        if (!account._pendingAdd) {
-          continue;
-        }
-        if (reviewer.group && account._group &&
-            reviewer.group.id === account.id) {
-          return false;
-        }
-        if (reviewer.account && !account._group &&
-            reviewer.account._account_id === account._account_id) {
-          return false;
-        }
-      }
-
-      return true;
-    },
-
     _handleRemove: function(e) {
       var toRemove = e.detail.account;
       for (var i = 0; i < this.accounts.length; i++) {
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
index 1bf2a30..bb55d08 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
@@ -232,57 +232,5 @@
         },
       ]);
     });
-
-    suite('_filterSuggestion', function() {
-      var notOwnerOrReviewerStub;
-
-      setup(function() {
-        notOwnerOrReviewerStub = sinon.stub(element.$.entry,
-            'notOwnerOrReviewer');
-      });
-
-      teardown(function() {
-        notOwnerOrReviewerStub.restore();
-      });
-
-
-      test('_filterSuggestion owner or reviewer', function() {
-        notOwnerOrReviewerStub.returns(false);
-        var reviewer = {account: makeAccount()};
-
-        var result = element._filterSuggestion(reviewer);
-
-        assert.isFalse(result);
-      });
-
-      test('_filterSuggestion new', function() {
-        notOwnerOrReviewerStub.returns(true);
-        var reviewer = {account: makeAccount()};
-
-        var result = element._filterSuggestion(reviewer);
-
-        assert.isTrue(result);
-      });
-
-      test('_filterSuggestion pending', function() {
-        notOwnerOrReviewerStub.returns(true);
-        var reviewer = {account: element.accounts[1]};
-        element.accounts[1]._pendingAdd = true;
-
-        var result = element._filterSuggestion(reviewer);
-
-        assert.isFalse(result);
-      });
-
-      test('_filterSuggestion new with pending', function() {
-        notOwnerOrReviewerStub.returns(true);
-        var reviewer = {account: makeAccount()};
-        element.accounts[1]._pendingAdd = true;
-
-        var result = element._filterSuggestion(reviewer);
-
-        assert.isTrue(result);
-      });
-    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index 9ddd66d9..bda9dee 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -89,15 +89,36 @@
         <gr-account-link account="[[change.owner]]"></gr-account-link>
       </span>
     </section>
-    <section>
-      <span class="title">Reviewers</span>
-      <span class="value">
-        <gr-reviewer-list
-            change="{{change}}"
-            mutable="[[mutable]]"
-            suggest-from="[[serverConfig.suggest.from]]"></gr-reviewer-list>
-      </span>
-    </section>
+    <template is="dom-if" if="[[_showReviewersByState]]">
+      <section>
+        <span class="title">Reviewers</span>
+        <span class="value">
+          <gr-reviewer-list
+              change="{{change}}"
+              mutable="[[mutable]]"
+              reviewers-only></gr-reviewer-list>
+        </span>
+      </section>
+      <section>
+        <span class="title">CC</span>
+        <span class="value">
+          <gr-reviewer-list
+              change="{{change}}"
+              mutable="[[mutable]]"
+              ccs-only></gr-reviewer-list>
+        </span>
+      </section>
+    </template>
+    <template is="dom-if" if="[[!_showReviewersByState]]">
+      <section>
+        <span class="title">Reviewers</span>
+        <span class="value">
+          <gr-reviewer-list
+              change="{{change}}"
+              mutable="[[mutable]]"></gr-reviewer-list>
+        </span>
+      </section>
+    </template>
     <section>
       <span class="title">Project</span>
       <span class="value">[[change.project]]</span>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index e24cc4a..af19703 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -42,6 +42,10 @@
         type: Boolean,
         computed: '_computeTopicReadOnly(mutable, change)',
       },
+      _showReviewersByState: {
+        type: Boolean,
+        computed: '_computeShowReviewersByState(serverConfig)',
+      },
     },
 
     behaviors: [
@@ -134,5 +138,9 @@
     _computeTopicPlaceholder: function(_topicReadOnly) {
       return _topicReadOnly ? 'No Topic' : 'Click to add topic';
     },
+
+    _computeShowReviewersByState: function(serverConfig) {
+      return !!serverConfig.note_db_enabled;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index a66d020..01f0649 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -140,5 +140,17 @@
       assert.equal(link, 'url-base/xx project-name xx commit-sha xx');
       assert.notEqual(link, '../../link-url');
     });
+
+    test('show CC section when NoteDb enabled', function() {
+      function hasCc() {
+        return element._showReviewersByState;
+      }
+
+      element.serverConfig = {};
+      assert.isFalse(hasCc());
+
+      element.serverConfig = {note_db_enabled: true};
+      assert.isTrue(hasCc());
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 08bfe77..8c22e7c 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -320,6 +320,7 @@
           labels="[[_change.labels]]"
           permitted-labels="[[_change.permitted_labels]]"
           diff-drafts="[[_diffDrafts]]"
+          server-config="[[serverConfig]]"
           on-send="_handleReplySent"
           on-cancel="_handleReplyCancel"
           hidden$="[[!_loggedIn]]">Reply</gr-reply-dialog>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 378e588..700f162 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -318,7 +318,11 @@
     },
 
     _handleShowReplyDialog: function(e) {
-      this._openReplyDialog(this.$.replyDialog.FocusTarget.REVIEWERS);
+      var target = this.$.replyDialog.FocusTarget.REVIEWERS;
+      if (e.detail.value && e.detail.value.ccsOnly) {
+        target = this.$.replyDialog.FocusTarget.CCS;
+      }
+      this._openReplyDialog(target);
     },
 
     _paramsChanged: function(value) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index bba0a63..141940d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -313,5 +313,20 @@
         assert.isNull(element._change.topic);
       });
     });
+
+    test('reply dialog focus can be controlled', function() {
+      var FocusTarget = element.$.replyDialog.FocusTarget;
+      var openSpy = sinon.spy(element, '_openReplyDialog');
+
+      var e = {detail: {}};
+      element._handleShowReplyDialog(e);
+      assert(openSpy.lastCall.calledWithExactly(FocusTarget.REVIEWERS),
+          '_openReplyDialog should have been passed REVIEWERS');
+
+      e.detail.value = {ccsOnly: true};
+      element._handleShowReplyDialog(e);
+      assert(openSpy.lastCall.calledWithExactly(FocusTarget.CCS),
+          '_openReplyDialog should have been passed CCS');
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index 66a1bd0..9e8f740 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -150,10 +150,24 @@
               id="reviewers"
               accounts="[[_reviewers]]"
               change="[[change]]"
+              filter="[[filterReviewerSuggestion]]"
               pending-confirmation="{{_reviewerPendingConfirmation}}"
               placeholder="Add reviewer...">
           </gr-account-list>
         </div>
+        <template is="dom-if" if="[[serverConfig.note_db_enabled]]">
+          <div class="peopleList">
+            <div class="peopleListLabel">CC</div>
+            <gr-account-list
+                id="ccs"
+                accounts="[[_ccs]]"
+                change="[[change]]"
+                filter="[[filterReviewerSuggestion]]"
+                pending-confirmation="{{_ccPendingConfirmation}}"
+                placeholder="Add CC...">
+            </gr-account-list>
+          </div>
+        </template>
         <gr-overlay
             id="reviewerConfirmationOverlay"
             on-iron-overlay-canceled="_cancelPendingReviewer"
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index d4b0cb2..5b6bf16 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -18,6 +18,7 @@
 
   var FocusTarget = {
     BODY: 'body',
+    CCS: 'cc',
     REVIEWERS: 'reviewers',
   };
 
@@ -51,11 +52,23 @@
         observer: '_draftChanged',
       },
       diffDrafts: Object,
+      filterReviewerSuggestion: {
+        type: Function,
+        value: function() {
+          return this._filterReviewerSuggestion.bind(this);
+        },
+      },
       labels: Object,
       permittedLabels: Object,
+      serverConfig: Object,
 
       _account: Object,
-      _owners: Array,
+      _ccs: Array,
+      _ccPendingConfirmation: {
+        type: Object,
+        observer: '_reviewerPendingConfirmationUpdated',
+      },
+      _owner: Object,
       _reviewers: Array,
       _reviewerPendingConfirmation: {
         type: Object,
@@ -70,7 +83,7 @@
     ],
 
     observers: [
-      '_changeUpdated(change.*)',
+      '_changeUpdated(change.reviewers.*, change.owner)',
     ],
 
     attached: function() {
@@ -110,6 +123,18 @@
       selectorEl.selectIndex(selectorEl.indexOf(item));
     },
 
+    _mapReviewer: function(reviewer) {
+      var reviewerId;
+      var confirmed;
+      if (reviewer.account) {
+        reviewerId = reviewer.account._account_id;
+      } else if (reviewer.group) {
+        reviewerId = reviewer.group.id;
+        confirmed = reviewer.group.confirmed;
+      }
+      return {reviewer: reviewerId, confirmed: confirmed};
+    },
+
     send: function() {
       var obj = {
         drafts: 'PUBLISH_ALL_REVISIONS',
@@ -131,27 +156,20 @@
         obj.message = this.draft;
       }
 
-      var newReviewers = this.$.reviewers.additions();
-      newReviewers.forEach(function(reviewer) {
-        var reviewerId;
-        var confirmed;
-        if (reviewer.account) {
-          reviewerId = reviewer.account._account_id;
-        } else if (reviewer.group) {
-          reviewerId = reviewer.group.id;
-          confirmed = reviewer.group.confirmed;
-        }
-        if (!obj.reviewers) {
-          obj.reviewers = [];
-        }
-        obj.reviewers.push({reviewer: reviewerId, confirmed: confirmed});
-      });
+      obj.reviewers = this.$.reviewers.additions().map(this._mapReviewer);
+      if (this.serverConfig.note_db_enabled) {
+        this.$$('#ccs').additions().forEach(function(reviewer) {
+          reviewer = this._mapReviewer(reviewer);
+          reviewer.state = 'CC';
+          obj.reviewers.push(reviewer);
+        }.bind(this));
+      }
 
       this.disabled = true;
 
       var errFn = this._handle400Error.bind(this);
       return this._saveReview(obj, errFn).then(function(response) {
-        if (!response.ok) {
+        if (!response || !response.ok) {
           return response;
         }
         this.disabled = false;
@@ -170,6 +188,9 @@
       } else if (section === FocusTarget.REVIEWERS) {
         var reviewerEntry = this.$.reviewers.focusStart;
         reviewerEntry.async(reviewerEntry.focus);
+      } else if (section === FocusTarget.CCS) {
+        var ccEntry = this.$$('#ccs').focusStart;
+        ccEntry.async(ccEntry.focus);
       }
     },
 
@@ -190,7 +211,7 @@
       if (response.status !== 400) {
         // This is all restAPI does when there is no custom error handling.
         this.fire('server-error', {response: response});
-        return;
+        return response;
       }
 
       // Process the response body, format a better error message, and fire
@@ -279,32 +300,67 @@
       return permittedLabels[label];
     },
 
-    _changeUpdated: function(changeRecord) {
-      if (!changeRecord.path || !changeRecord.base) {
-        return;
+    _changeUpdated: function(changeRecord, owner) {
+      this._owner = owner;
+
+      var reviewers = [];
+      var ccs = [];
+
+      for (var key in changeRecord.base) {
+        if (key !== 'REVIEWER' && key !== 'CC') {
+          console.warn('unexpected reviewer state:', key);
+          continue;
+        }
+        changeRecord.base[key].forEach(function(entry) {
+          if (entry._account_id === owner._account_id) {
+            return;
+          }
+          switch (key) {
+            case 'REVIEWER':
+              reviewers.push(entry);
+              break;
+            case 'CC':
+              ccs.push(entry);
+              break;
+          }
+        });
       }
 
-      if (changeRecord.path !== 'change' &&
-          changeRecord.path !== 'change.reviewers.CC.splices' &&
-          changeRecord.path !== 'change.reviewers.REVIEWER.splices') {
-        return;
+      if (this.serverConfig.note_db_enabled) {
+        this._ccs = ccs;
+      } else {
+        reviewers = reviewers.concat(ccs);
       }
-
-      var owner = changeRecord.base.owner;
-      this._owners = [owner];
-
-      if (!changeRecord.base.reviewers) {
-        return;
-      }
-
-      var reviewers = changeRecord.base.reviewers.REVIEWER || [];
-      reviewers = reviewers.concat(changeRecord.base.reviewers.CC);
-      reviewers = reviewers.filter(function(account) {
-        return account && account._account_id !== owner._account_id;
-      }.bind(this));
       this._reviewers = reviewers;
     },
 
+    _accountOrGroupKey: function(entry) {
+      return entry.id || entry._account_id;
+    },
+
+    _filterReviewerSuggestion: function(suggestion) {
+      var entry;
+      if (suggestion.account) {
+        entry = suggestion.account;
+      } else if (suggestion.group) {
+        entry = suggestion.group;
+      } else {
+        console.warn('received suggestion that was neither account nor group:',
+            suggestion);
+      }
+      if (entry._account_id === this._owner._account_id) {
+        return false;
+      }
+
+      var key = this._accountOrGroupKey(entry);
+      var finder = function(entry) {
+        return this._accountOrGroupKey(entry) === key;
+      }.bind(this);
+
+      return this._reviewers.find(finder) === undefined &&
+          this._ccs.find(finder) === undefined;
+    },
+
     _getAccount: function() {
       return this.$.restAPI.getAccount();
     },
@@ -333,13 +389,22 @@
     },
 
     _confirmPendingReviewer: function() {
-      this.$.reviewers.confirmGroup(this._reviewerPendingConfirmation.group);
-      this._focusOn(FocusTarget.REVIEWERS);
+      if (this._ccPendingConfirmation) {
+        this.$$('#ccs').confirmGroup(this._ccPendingConfirmation.group);
+        this._focusOn(FocusTarget.CCS);
+      } else {
+        this.$.reviewers.confirmGroup(this._reviewerPendingConfirmation.group);
+        this._focusOn(FocusTarget.REVIEWERS);
+      }
     },
 
     _cancelPendingReviewer: function() {
+      this._ccPendingConfirmation = null;
       this._reviewerPendingConfirmation = null;
-      this._focusOn(FocusTarget.REVIEWERS);
+
+      var target =
+          this._ccPendingConfirmation ? FocusTarget.CCS : FocusTarget.REVIEWERS;
+      this._focusOn(target);
     },
 
     _getStorageLocation: function() {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index 7dc601a..96b1590 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -87,6 +87,7 @@
           '+1'
         ]
       };
+      element.serverConfig = {};
 
       getDraftCommentStub = sandbox.stub(element.$.storage, 'getDraftComment');
       setDraftCommentStub = sandbox.stub(element.$.storage, 'setDraftComment');
@@ -144,7 +145,8 @@
               'Code-Review': -1,
               'Verified': -1
             },
-            message: 'I wholeheartedly disapprove'
+            message: 'I wholeheartedly disapprove',
+            reviewers: [],
           });
           return Promise.resolve({ok: true});
         });
@@ -308,11 +310,57 @@
         }
         event.detail.response.text().then(function(body) {
           assert.equal(body, 'first error, second error');
-          done();
         });
       });
+      element.send().then(done);
+    });
 
-      element.send();
+    test('ccs are displayed if NoteDb is enabled', function() {
+      function hasCc() {
+        flushAsynchronousOperations();
+        return !!element.$$('#ccs');
+      }
+
+      element.serverConfig = {};
+      assert.isFalse(hasCc());
+
+      element.serverConfig = {note_db_enabled: true};
+      assert.isTrue(hasCc());
+    });
+
+    test('filterReviewerSuggestion', function() {
+      var counter = 0;
+      function makeAccount() {
+        return {_account_id: counter++};
+      }
+      function makeGroup() {
+        return {id: counter++};
+      }
+
+      var owner = makeAccount();
+      var reviewer1 = makeAccount();
+      var reviewer2 = makeGroup();
+      var cc1 = makeAccount();
+      var cc2 = makeGroup();
+
+      element._owner = owner;
+      element._reviewers = [reviewer1, reviewer2];
+      element._ccs = [cc1, cc2];
+
+      assert.isTrue(
+          element._filterReviewerSuggestion({account: makeAccount()}));
+      assert.isTrue(element._filterReviewerSuggestion({group: makeGroup()}));
+
+      // Owner should be excluded.
+      assert.isFalse(element._filterReviewerSuggestion({account: owner}));
+
+      // Existing and pending reviewers should be excluded.
+      assert.isFalse(element._filterReviewerSuggestion({account: reviewer1}));
+      assert.isFalse(element._filterReviewerSuggestion({group: reviewer2}));
+
+      // Existing and pending CCs should be excluded.
+      assert.isFalse(element._filterReviewerSuggestion({account: cc1}));
+      assert.isFalse(element._filterReviewerSuggestion({group: cc2}));
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
index 99e3cd9..435b7de 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
@@ -68,7 +68,7 @@
           link
           id="addReviewer"
           class="addReviewer"
-          on-tap="_handleAddTap">Add reviewer</gr-button>
+          on-tap="_handleAddTap">[[_addLabel]]</gr-button>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
index 7037fc2..72a7c9b 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -34,9 +34,13 @@
         type: Boolean,
         value: false,
       },
-      suggestFrom: {
-        type: Number,
-        value: 3,
+      reviewersOnly: {
+        type: Boolean,
+        value: false,
+      },
+      ccsOnly: {
+        type: Boolean,
+        value: false,
       },
 
       _reviewers: {
@@ -47,6 +51,10 @@
         type: Boolean,
         value: false,
       },
+      _addLabel: {
+        type: String,
+        computed: '_computeAddLabel(ccsOnly)',
+      },
 
       // Used for testing.
       _lastAutocompleteRequest: Object,
@@ -61,7 +69,13 @@
       var result = [];
       var reviewers = changeRecord.base;
       for (var key in reviewers) {
-        if (key == 'REVIEWER' || key == 'CC') {
+        if (this.reviewersOnly && key !== 'REVIEWER') {
+          continue;
+        }
+        if (this.ccsOnly && key !== 'CC') {
+          continue;
+        }
+        if (key === 'REVIEWER' || key === 'CC') {
           result = result.concat(reviewers[key]);
         }
       }
@@ -110,11 +124,22 @@
 
     _handleAddTap: function(e) {
       e.preventDefault();
-      this.fire('show-reply-dialog');
+      var value = {};
+      if (this.reviewersOnly) {
+        value.reviewersOnly = true;
+      }
+      if (this.ccsOnly) {
+        value.ccsOnly = true;
+      }
+      this.fire('show-reply-dialog', {value: value});
     },
 
     _removeReviewer: function(id) {
       return this.$.restAPI.removeChangeReviewer(this.change._number, id);
     },
+
+    _computeAddLabel: function(ccsOnly) {
+      return ccsOnly ? 'Add CC' : 'Add reviewer';
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
index e6f7a20..6c6125c 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -34,9 +34,11 @@
 <script>
   suite('gr-reviewer-list tests', function() {
     var element;
+    var sandbox;
 
     setup(function() {
       element = fixture('basic');
+      sandbox = sinon.sandbox.create();
       stub('gr-rest-api-interface', {
         removeChangeReviewer: function() {
           return Promise.resolve({ok: true});
@@ -44,6 +46,10 @@
       });
     });
 
+    teardown(function() {
+      sandbox.restore();
+    });
+
     test('controls hidden on immutable element', function() {
       element.mutable = false;
       assert.isTrue(element.$$('.controlsContainer').hasAttribute('hidden'));
@@ -113,5 +119,65 @@
         }
       });
     });
+
+    test('tracking reviewers and ccs', function() {
+      var counter = 0;
+      function makeAccount() {
+        return {_account_id: counter++};
+      }
+
+      var owner = makeAccount();
+      var reviewer = makeAccount();
+      var cc = makeAccount();
+      var reviewers = {
+        REMOVED: [makeAccount()],
+        REVIEWER: [owner, reviewer],
+        CC: [owner, cc],
+      };
+
+      element.ccsOnly = false;
+      element.reviewersOnly = false;
+      element.change = {
+        owner: owner,
+        reviewers: reviewers,
+      };
+      assert.deepEqual(element._reviewers, [reviewer, cc]);
+
+      element.reviewersOnly = true;
+      element.change = {
+        owner: owner,
+        reviewers: reviewers,
+      };
+      assert.deepEqual(element._reviewers, [reviewer]);
+
+      element.ccsOnly = true;
+      element.reviewersOnly = false;
+      element.change = {
+        owner: owner,
+        reviewers: reviewers,
+      };
+      assert.deepEqual(element._reviewers, [cc]);
+    });
+
+    test('_handleAddTap passes mode with event', function() {
+      var fireStub = sandbox.stub(element, 'fire');
+      var e = {preventDefault: function() {}};
+
+      element.ccsOnly = false;
+      element.reviewersOnly = false;
+      element._handleAddTap(e);
+      assert.isTrue(fireStub.calledWith('show-reply-dialog', {value: {}}));
+
+      element.reviewersOnly = true;
+      element._handleAddTap(e);
+      assert.isTrue(fireStub.lastCall.calledWith('show-reply-dialog',
+          {value: {reviewersOnly: true}}));
+
+      element.ccsOnly = true;
+      element.reviewersOnly = false;
+      element._handleAddTap(e);
+      assert.isTrue(fireStub.lastCall.calledWith('show-reply-dialog',
+          {value: {ccsOnly: true}}));
+    });
   });
 </script>