Merge "Implement pagination in branch list screen"
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 cbf906e..c919c6b 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
@@ -31,6 +31,7 @@
   String addWatchPanel();
   String avatarInfoPanel();
   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 38b2f69..0d0f793 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
@@ -1232,6 +1232,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.