Implement pagination in branch list screen

Branch list screen should have the same pagination as Project and Group
list screens. When a branch is created, a confirmation message should be
shown letting the user know that it was successful. The branch is
displayed in the current table if the page size limit has not yet been
reached.

Change-Id: Idaa2c48b02aa018727561e86d4bc208380f47ae6
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java
index e85633f..ab3ff5d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java
@@ -25,8 +25,8 @@
 
 public class ConfirmationDialog extends AutoCenterDialogBox {
 
-
   private Button cancelButton;
+  private Button okButton;
 
   public ConfirmationDialog(final String dialogTitle, final SafeHtml message,
       final ConfirmationCallback callback) {
@@ -36,7 +36,7 @@
 
     final FlowPanel buttons = new FlowPanel();
 
-    final Button okButton = new Button();
+    okButton = new Button();
     okButton.setText(Gerrit.C.confirmationDialogOk());
     okButton.addClickHandler(new ClickHandler() {
       @Override
@@ -76,4 +76,11 @@
     GlobalKey.dialog(this);
     cancelButton.setFocus(true);
   }
+
+  public void setCancelVisible(boolean visible) {
+    cancelButton.setVisible(visible);
+    if (!visible) {
+      okButton.setFocus(true);
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
index d7bd00d8..6d94f1d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
@@ -846,6 +846,11 @@
             return new NotFoundScreen();
           }
 
+          int q = rest.lastIndexOf('?');
+          if (q > 0 && rest.lastIndexOf(',', q) > 0) {
+            c = rest.substring(0, q - 1).lastIndexOf(',');
+          }
+
           Project.NameKey k = Project.NameKey.parse(rest.substring(0, c));
           String panel = rest.substring(c + 1);
 
@@ -853,7 +858,8 @@
             return new ProjectInfoScreen(k);
           }
 
-          if (ProjectScreen.BRANCH.equals(panel)) {
+          if (ProjectScreen.BRANCH.equals(panel)
+              || matchPrefix(ProjectScreen.BRANCH, panel)) {
             return new ProjectBranchesScreen(k);
           }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
index 64025db..6bbc8f1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
@@ -36,6 +36,9 @@
   String confirmationDialogOk();
   String confirmationDialogCancel();
 
+  String branchCreationDialogTitle();
+  String branchCreationConfirmationMessage();
+
   String branchDeletionDialogTitle();
   String branchDeletionConfirmationMessage();
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
index a335d6b..05de983 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
@@ -17,6 +17,9 @@
 confirmationDialogOk = OK
 confirmationDialogCancel = Cancel
 
+branchCreationDialogTitle = Branch Creation
+branchCreationConfirmationMessage = The following branch was successfully created:
+
 branchDeletionDialogTitle = Branch Deletion
 branchDeletionConfirmationMessage = Do you really want to delete the following branches?
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
index 1e62573..eb5f29a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
@@ -38,6 +38,7 @@
   String avatarInfoPanel();
   String blockHeader();
   String bottomheader();
+  String branchTablePrevNextLinks();
   String cAPPROVAL();
   String cLastUpdate();
   String cOWNER();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
index 66d9d67..9ec8c6c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
@@ -31,10 +31,12 @@
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.HintTextBox;
+import com.google.gerrit.client.ui.Hyperlink;
+import com.google.gerrit.client.ui.NavigationTable;
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -48,12 +50,14 @@
 import com.google.gwt.event.dom.client.KeyPressHandler;
 import com.google.gwt.event.logical.shared.ValueChangeEvent;
 import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.http.client.URL;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.HorizontalPanel;
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.InlineLabel;
 import com.google.gwt.user.client.ui.TextBox;
@@ -67,15 +71,62 @@
 import java.util.Set;
 
 public class ProjectBranchesScreen extends ProjectScreen {
+  private Hyperlink prev;
+  private Hyperlink next;
   private BranchesTable branchTable;
   private Button delBranch;
   private Button addBranch;
   private HintTextBox nameTxtBox;
   private HintTextBox irevTxtBox;
   private FlowPanel addPanel;
+  private int pageSize;
+  private int start;
+  private Query query;
 
   public ProjectBranchesScreen(final Project.NameKey toShow) {
     super(toShow);
+    configurePageSize();
+  }
+
+  private void configurePageSize() {
+    if (Gerrit.isSignedIn()) {
+      AccountGeneralPreferences p =
+          Gerrit.getUserAccount().getGeneralPreferences();
+      short m = p.getMaximumPageSize();
+      pageSize = 0 < m ? m : AccountGeneralPreferences.DEFAULT_PAGESIZE;
+    } else {
+      pageSize = AccountGeneralPreferences.DEFAULT_PAGESIZE;
+    }
+  }
+
+  private void parseToken() {
+    String token = getToken();
+
+    for (String kvPair : token.split("[,;&/?]")) {
+      String[] kv = kvPair.split("=", 2);
+      if (kv.length != 2 || kv[0].isEmpty()) {
+        continue;
+      }
+
+      if ("skip".equals(kv[0])
+          && URL.decodeQueryString(kv[1]).matches("^[\\d]+")) {
+        start = Integer.parseInt(URL.decodeQueryString(kv[1]));
+      }
+    }
+  }
+
+  private void setupNavigationLink(Hyperlink link, int skip) {
+    link.setTargetHistoryToken(getTokenForScreen(skip));
+    link.setVisible(true);
+  }
+
+  private String getTokenForScreen(int skip) {
+    String token = PageLinks.toProjectBranches(getProjectKey());
+
+    if (skip > 0) {
+      token += "?skip=" + skip;
+    }
+    return token;
   }
 
   @Override
@@ -89,28 +140,10 @@
             addPanel.setVisible(result.canAddRefs());
           }
         });
-    refreshBranches();
+    query = new Query().start(start).run();
     savedPanel = BRANCH;
   }
 
-  private void refreshBranches() {
-    ProjectApi.getBranches(getProjectKey(),
-        new ScreenLoadCallback<JsArray<BranchInfo>>(this) {
-          @Override
-          public void preDisplay(final JsArray<BranchInfo> result) {
-            Set<String> checkedRefs = branchTable.getCheckedRefs();
-            display(Natives.asList(result));
-            branchTable.setChecked(checkedRefs);
-            updateForm();
-          }
-        });
-  }
-
-  private void display(final List<BranchInfo> branches) {
-    branchTable.display(branches);
-    delBranch.setVisible(branchTable.hasBranchCanDelete());
-  }
-
   private void updateForm() {
     branchTable.updateDeleteButton();
     addBranch.setEnabled(true);
@@ -121,6 +154,13 @@
   @Override
   protected void onInitUI() {
     super.onInitUI();
+    parseToken();
+
+    prev = new Hyperlink(Util.C.pagedListPrev(), true, "");
+    prev.setVisible(false);
+
+    next = new Hyperlink(Util.C.pagedListNext(), true, "");
+    next.setVisible(false);
 
     addPanel = new FlowPanel();
 
@@ -175,9 +215,13 @@
         branchTable.deleteChecked();
       }
     });
-
+    HorizontalPanel buttons = new HorizontalPanel();
+    buttons.setStyleName(Gerrit.RESOURCES.css().branchTablePrevNextLinks());
+    buttons.add(delBranch);
+    buttons.add(prev);
+    buttons.add(next);
     add(branchTable);
-    add(delBranch);
+    add(buttons);
     add(addPanel);
   }
 
@@ -206,6 +250,7 @@
         new GerritCallback<BranchInfo>() {
           @Override
           public void onSuccess(BranchInfo branch) {
+            showAddedBranch(branch);
             addBranch.setEnabled(true);
             nameTxtBox.setText("");
             irevTxtBox.setText("");
@@ -213,21 +258,43 @@
             delBranch.setVisible(branchTable.hasBranchCanDelete());
           }
 
-      @Override
-      public void onFailure(Throwable caught) {
-        addBranch.setEnabled(true);
-        selectAllAndFocus(nameTxtBox);
-        new ErrorDialog(caught.getMessage()).center();
-      }
-    });
+          @Override
+          public void onFailure(Throwable caught) {
+            addBranch.setEnabled(true);
+            selectAllAndFocus(nameTxtBox);
+            new ErrorDialog(caught.getMessage()).center();
+          }
+        });
   }
 
-  private static void selectAllAndFocus(final TextBox textBox) {
+  void showAddedBranch(BranchInfo branch) {
+    SafeHtmlBuilder b = new SafeHtmlBuilder();
+    b.openElement("b");
+    b.append(Gerrit.C.branchCreationConfirmationMessage());
+    b.closeElement("b");
+
+    b.openElement("p");
+    b.append(branch.ref());
+    b.closeElement("p");
+
+    ConfirmationDialog confirmationDialog =
+        new ConfirmationDialog(Gerrit.C.branchCreationDialogTitle(),
+            b.toSafeHtml(), new ConfirmationCallback() {
+      @Override
+      public void onOk() {
+        //do nothing
+      }
+    });
+    confirmationDialog.center();
+    confirmationDialog.setCancelVisible(false);
+  }
+
+  private static void selectAllAndFocus(TextBox textBox) {
     textBox.selectAll();
     textBox.setFocus(true);
   }
 
-  private class BranchesTable extends FancyFlexTable<BranchInfo> {
+  private class BranchesTable extends NavigationTable<BranchInfo> {
     private ValueChangeHandler<Boolean> updateDeleteHandler;
     boolean canDelete;
 
@@ -332,19 +399,23 @@
 
             @Override
             public void onFailure(Throwable caught) {
-              refreshBranches();
+              query = new Query().start(start).run();
               super.onFailure(caught);
             }
           });
     }
 
     void display(List<BranchInfo> branches) {
+      displaySubset(branches, 0, branches.size());
+    }
+
+    void displaySubset(List<BranchInfo> branches, int fromIndex, int toIndex) {
       canDelete = false;
 
       while (1 < table.getRowCount())
         table.removeRow(table.getRowCount() - 1);
 
-      for (final BranchInfo k : branches) {
+      for (BranchInfo k : branches.subList(fromIndex, toIndex)) {
         final int row = table.getRowCount();
         table.insertRow(row);
         applyDataRowStyle(row);
@@ -353,17 +424,21 @@
     }
 
     void insert(BranchInfo info) {
-      Comparator<BranchInfo> c = new Comparator<BranchInfo>() {
-        @Override
-        public int compare(BranchInfo a, BranchInfo b) {
-          return a.ref().compareTo(b.ref());
+      if (table.getRowCount() <= pageSize || pageSize == 0) {
+        Comparator<BranchInfo> c = new Comparator<BranchInfo>() {
+          @Override
+          public int compare(BranchInfo a, BranchInfo b) {
+            return a.ref().compareTo(b.ref());
+          }
+        };
+        int insertPos = getInsertRow(c, info);
+        if (insertPos >= 0) {
+          table.insertRow(insertPos);
+          applyDataRowStyle(insertPos);
+          populate(insertPos, info);
         }
-      };
-      int insertPos = getInsertRow(c, info);
-      if (insertPos >= 0) {
-        table.insertRow(insertPos);
-        applyDataRowStyle(insertPos);
-        populate(insertPos, info);
+      } else {
+        setupNavigationLink(next, ProjectBranchesScreen.this.start + pageSize);
       }
     }
 
@@ -530,5 +605,74 @@
       }
       delBranch.setEnabled(on);
     }
+
+    @Override
+    protected void onOpenRow(int row) {
+      if (row > 0) {
+        movePointerTo(row);
+      }
+    }
+
+    @Override
+    protected Object getRowItemKey(BranchInfo item) {
+      return item.ref();
+    }
+  }
+
+  private class Query {
+    private int qStart;
+
+    Query start(int start) {
+      this.qStart = start;
+      return this;
+    }
+
+    Query run() {
+      // Retrieve one more branch than page size to determine if there are more
+      // branches to display
+      ProjectApi.getBranches(getProjectKey(), pageSize + 1, qStart,
+              new ScreenLoadCallback<JsArray<BranchInfo>>(ProjectBranchesScreen.this) {
+                @Override
+                public void preDisplay(JsArray<BranchInfo> result) {
+                  if (!isAttached()) {
+                    // View has been disposed.
+                  } else if (query == Query.this) {
+                    query = null;
+                    showList(result);
+                  } else {
+                    query.run();
+                  }
+                }
+          });
+      return this;
+    }
+
+    void showList(JsArray<BranchInfo> result) {
+      setToken(getTokenForScreen(qStart));
+      ProjectBranchesScreen.this.start = qStart;
+
+      if (result.length() <= pageSize) {
+        branchTable.display(Natives.asList(result));
+        next.setVisible(false);
+      } else {
+        branchTable.displaySubset(Natives.asList(result), 0,
+            result.length() - 1);
+        setupNavigationLink(next, qStart + pageSize);
+      }
+      if (qStart > 0) {
+        setupNavigationLink(prev, qStart - pageSize);
+      } else {
+        prev.setVisible(false);
+      }
+
+      delBranch.setVisible(branchTable.hasBranchCanDelete());
+      Set<String> checkedRefs = branchTable.getCheckedRefs();
+      branchTable.setChecked(checkedRefs);
+      updateForm();
+
+      if (!isCurrentView()) {
+        display();
+      }
+    }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
index a9082e2..ea84f45 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
@@ -1596,6 +1596,20 @@
   cursor: pointer;
 }
 
+.branchTablePrevNextLinks {
+  position: relative;
+}
+.branchTablePrevNextLinks td {
+  float: left;
+  width: 5em;
+  text-align: left;
+  padding-right: 10px;
+}
+.branchTablePrevNextLinks .gwt-Hyperlink {
+  font-size: 9pt;
+  color: #2a5db0;
+}
+
 /** PluginListScreen **/
 .pluginsTable {
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
index f4f7087..b657f23 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -58,6 +58,14 @@
     project(name).view("branches").get(cb);
   }
 
+  public static void getBranches(Project.NameKey name, int limit, int start,
+       AsyncCallback<JsArray<BranchInfo>> cb) {
+    RestApi call = project(name).view("branches");
+    call.addParameter("n", limit);
+    call.addParameter("s", start);
+    call.get(cb);
+  }
+
   /**
    * Delete branches. One call is fired to the server to delete all the
    * branches.