]> source.dussan.org Git - gitblit.git/commitdiff
Add support nested groups on the Repositories page
authorFlorian Zschocke <zschocke@gmx.de>
Sat, 15 Jun 2019 12:34:29 +0000 (14:34 +0200)
committerFlorian Zschocke <zschocke@gmx.de>
Sat, 15 Jun 2019 12:50:50 +0000 (14:50 +0200)
Fix for #725.
Also covers #527.

This is a squashed commit of the following commits,
merging and closing pull request #1267:

commit 55fee41769ffab1aff59344fe117d481687aa743
Author: Martin Spielmann <mail@martinspielmann.de>
Date:   Mon Nov 6 17:19:53 2017 +0100

    declared local variable final to fix travis build

commit 131e4d14a48c2a3fdce621fa54637de50684d040
Author: Martin Spielmann <mail@martinspielmann.de>
Date:   Mon Nov 6 14:11:55 2017 +0100

    fix formatting (use tab for identation)

commit 8da5f6d5967894f157251c320928acdab3a451e7
Author: Martin Spielmann <mail@martinspielmann.de>
Date:   Mon Nov 6 13:45:39 2017 +0100

    Add repositoryListType tree. Addresses #725, 527 and includes #1224

commit 6c061651fb95212ae242dbca06c8d9ef80146201
Merge: f365daa3 40ee9653
Author: Martin Spielmann <mail@martinspielmann.de>
Date:   Sat Nov 4 13:19:08 2017 +0100

    Merge remote-tracking branch 'collapsible/ticket/527' into 725_nested_repos

commit f365daa3b1d6be135365f9b11bdece320beabf4e
Author: Martin Spielmann <mail@martinspielmann.de>
Date:   Sat Nov 4 13:10:24 2017 +0100

    first working version of tree model

src/main/java/com/gitblit/models/TreeNodeModel.java [new file with mode: 0644]
src/main/java/com/gitblit/wicket/panels/NestedRepositoryTreePanel.html [new file with mode: 0644]
src/main/java/com/gitblit/wicket/panels/NestedRepositoryTreePanel.java [new file with mode: 0644]
src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html
src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java
src/main/resources/gitblit/js/collapsible-table.js
src/test/java/com/gitblit/wicket/panels/TreeNodeModelTest.java [new file with mode: 0644]

diff --git a/src/main/java/com/gitblit/models/TreeNodeModel.java b/src/main/java/com/gitblit/models/TreeNodeModel.java
new file mode 100644 (file)
index 0000000..a69393e
--- /dev/null
@@ -0,0 +1,178 @@
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import com.gitblit.utils.StringUtils;
+
+public class TreeNodeModel implements Serializable, Comparable<TreeNodeModel> {
+
+    private static final long serialVersionUID = 1L;
+    final TreeNodeModel parent;
+    final String name;
+    final List<TreeNodeModel> subFolders = new ArrayList<>();
+    final List<RepositoryModel> repositories = new ArrayList<>();
+
+    /**
+     * Create a new tree root
+     */
+    public TreeNodeModel() {
+        this.name = "/";
+        this.parent = null;
+    }
+
+    protected TreeNodeModel(String name, TreeNodeModel parent) {
+        this.name = name;
+        this.parent = parent;
+    }
+
+    public int getDepth() {
+        if(parent == null) {
+            return 0;
+        }else {
+            return parent.getDepth() +1;
+        }
+    }
+
+    /**
+     * Add a new sub folder to the current folder
+     *
+     * @param subFolder the subFolder to create
+     * @return the newly created folder to allow chaining
+     */
+    public TreeNodeModel add(String subFolder) {
+        TreeNodeModel n = new TreeNodeModel(subFolder, this);
+        subFolders.add(n);
+        Collections.sort(subFolders);
+        return n;
+    }
+
+    /**
+     * Add the given repo to the current folder
+     *
+     * @param repo
+     */
+    public void add(RepositoryModel repo) {
+        repositories.add(repo);
+        Collections.sort(repositories);
+    }
+
+    /**
+     * Adds the given repository model within the given path. Creates parent folders if they do not exist
+     *
+     * @param path
+     * @param model
+     */
+    public void add(String path, RepositoryModel model) {
+        TreeNodeModel folder = getSubTreeNode(this, path, true);
+        folder.add(model);
+    }
+
+    @Override
+    public String toString() {
+        String string = name + "\n";
+        for(TreeNodeModel n : subFolders) {
+            string += "f";
+            for(int i = 0; i < n.getDepth(); i++) {
+                string += "-";
+            }
+            string += n.toString();
+        }
+
+        for(RepositoryModel n : repositories) {
+            string += "r";
+            for(int i = 0; i < getDepth()+1; i++) {
+                string += "-";
+            }
+            string += n.toString() + "\n";
+        }
+
+        return string;
+    }
+
+    public boolean containsSubFolder(String path) {
+        return containsSubFolder(this, path);
+    }
+
+    public TreeNodeModel getSubFolder(String path) {
+        return getSubTreeNode(this, path, false);
+    }
+
+    public List<Serializable> getTreeAsListForFrontend(){
+        List<Serializable> l = new ArrayList<>();
+        getTreeAsListForFrontend(l, this);
+        return l;
+    }
+
+    private static void getTreeAsListForFrontend(List<Serializable> list, TreeNodeModel node) {
+        list.add(node);
+        for(TreeNodeModel t : node.subFolders) {
+            getTreeAsListForFrontend(list, t);
+        }
+        for(RepositoryModel r : node.repositories) {
+            list.add(r);
+        }
+    }
+
+    private static TreeNodeModel getSubTreeNode(TreeNodeModel node, String path, boolean create) {
+        if(!StringUtils.isEmpty(path)) {
+            boolean isPathInCurrentHierarchyLevel = path.lastIndexOf('/') < 0;
+            if(isPathInCurrentHierarchyLevel) {
+                for(TreeNodeModel t : node.subFolders) {
+                    if(t.name.equals(path) ) {
+                        return t;
+                    }
+                }
+
+                if(create) {
+                    node.add(path);
+                    return getSubTreeNode(node, path, true);
+                }
+            }else {
+                //traverse into subFolder
+                String folderInCurrentHierarchyLevel = StringUtils.getFirstPathElement(path);
+
+                for(TreeNodeModel t : node.subFolders) {
+                    if(t.name.equals(folderInCurrentHierarchyLevel) ) {
+                        String folderInNextHierarchyLevel = path.substring(path.indexOf('/') + 1, path.length());
+                        return getSubTreeNode(t, folderInNextHierarchyLevel, create);
+                    }
+                }
+
+                if(create) {
+                    String folderInNextHierarchyLevel = path.substring(path.indexOf('/') + 1, path.length());
+                    return getSubTreeNode(node.add(folderInCurrentHierarchyLevel), folderInNextHierarchyLevel, true);
+                }
+            }
+        }
+
+        return null;
+    }
+
+    private static boolean containsSubFolder(TreeNodeModel node, String path) {
+        return getSubTreeNode(node, path, false) != null;
+    }
+
+    @Override
+    public int compareTo(TreeNodeModel t) {
+        return StringUtils.compareRepositoryNames(name, t.name);
+    }
+
+    public TreeNodeModel getParent() {
+        return parent;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public List<TreeNodeModel> getSubFolders() {
+        return subFolders;
+    }
+
+    public List<RepositoryModel> getRepositories() {
+        return repositories;
+    }
+}
diff --git a/src/main/java/com/gitblit/wicket/panels/NestedRepositoryTreePanel.html b/src/main/java/com/gitblit/wicket/panels/NestedRepositoryTreePanel.html
new file mode 100644 (file)
index 0000000..5f2aba2
--- /dev/null
@@ -0,0 +1,106 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+       xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+       xml:lang="en" lang="en">
+
+<body>
+       <wicket:panel>
+               <tr style="background-color: #bbb" wicket:id="nodeHeader" data-row-type="folder"></tr>
+               <tr wicket:id="subFolders">
+                       <span wicket:id="rowContent"></span>
+               </tr>
+               <wicket:container wicket:id="repositories">
+               <tr  wicket:id="rowContent" data-row-type="repo">
+                       <td wicket:id="firstColumn" class="left"
+                               style="padding-left: 3px;">
+                               <div style="border-left: 1px solid black; margin-left:6px; width: 19px;display: inline-block;float: left;"
+                                       wicket:id="depth">&nbsp;</div>
+                                       <span wicket:id="repoIcon"></span><span
+                               style="padding-left: 3px;" wicket:id="repositoryName">[repository
+                                       name]</span>
+                       </td>
+                       <td class="hidden-phone"><span class="list"
+                               wicket:id="repositoryDescription">[repository description]</span></td>
+                       <td class="hidden-tablet hidden-phone author"><span
+                               wicket:id="repositoryOwner">[repository owner]</span></td>
+                       <td class="hidden-phone"
+                               style="text-align: right; padding-right: 10px;"><img
+                               class="inlineIcon" wicket:id="sparkleshareIcon" /><img
+                               class="inlineIcon" wicket:id="frozenIcon" /><img class="inlineIcon"
+                               wicket:id="federatedIcon" /><img class="inlineIcon"
+                               wicket:id="accessRestrictionIcon" /></td>
+                       <td><span wicket:id="repositoryLastChange">[last change]</span></td>
+                       <td class="rightAlign hidden-phone"
+                               style="text-align: right; padding-right: 15px;"><span
+                               style="font-size: 0.8em;" wicket:id="repositorySize">[repository
+                                       size]</span></td>
+               </tr>
+               </wicket:container>
+
+               <wicket:fragment wicket:id="emptyFragment">
+               </wicket:fragment>
+
+               <wicket:fragment wicket:id="repoIconFragment">
+                       <span class="octicon octicon-centered octicon-repo"></span>
+               </wicket:fragment>
+
+               <wicket:fragment wicket:id="mirrorIconFragment">
+                       <span class="octicon octicon-centered octicon-mirror"></span>
+               </wicket:fragment>
+
+               <wicket:fragment wicket:id="forkIconFragment">
+                       <span class="octicon octicon-centered octicon-repo-forked"></span>
+               </wicket:fragment>
+
+               <wicket:fragment wicket:id="cloneIconFragment">
+                       <span class="octicon octicon-centered octicon-repo-push"
+                               wicket:message="title:gb.workingCopyWarning"></span>
+               </wicket:fragment>
+
+               <wicket:fragment wicket:id="tableGroupMinusCollapsible">
+               <i title="Click to expand/collapse" class="fa fa-minus-square-o table-group-collapsible" aria-hidden="true" style="padding-right:3px;cursor:pointer;"></i>
+    </wicket:fragment>
+
+    <wicket:fragment wicket:id="tableGroupPlusCollapsible">
+               <i title="Click to expand/collapse" class="fa fa-plus-square-o table-group-collapsible" aria-hidden="true" style="padding-right:3px;cursor:pointer;"></i>
+    </wicket:fragment>
+
+               <wicket:fragment wicket:id="tableAllCollapsible">
+                       <i title="Click to expand all"
+                               class="fa fa-plus-square-o table-openall-collapsible"
+                               aria-hidden="true" style="padding-right: 3px; cursor: pointer;"></i>
+                       <i title="Click to collapse all"
+                               class="fa fa-minus-square-o table-closeall-collapsible"
+                               aria-hidden="true" style="padding-right: 3px; cursor: pointer;"></i>
+               </wicket:fragment>
+
+               <wicket:fragment wicket:id="groupRepositoryHeader">
+                       <tr>
+                               <th class="left"><span wicket:id="allCollapsible"></span> <img
+                                       style="vertical-align: middle;" src="git-black-16x16.png" /> <wicket:message
+                                               key="gb.repository">Repository</wicket:message></th>
+                               <th class="hidden-phone"><span><wicket:message
+                                                       key="gb.description">Description</wicket:message></span></th>
+                               <th class="hidden-tablet hidden-phone"><span><wicket:message
+                                                       key="gb.owner">Owner</wicket:message></span></th>
+                               <th class="hidden-phone"></th>
+                               <th><wicket:message key="gb.lastChange">Last Change</wicket:message></th>
+                               <th class="right hidden-phone"></th>
+                       </tr>
+               </wicket:fragment>
+
+               <wicket:fragment wicket:id="groupRepositoryRow">
+                       <td wicket:id="firstColumn" style="" colspan="1">
+                       <div style="border-left: 1px solid black; margin-left:6px; width: 19px; display: inline-block;float: left;"
+                                       wicket:id="depth">&nbsp;</div>
+                       <span
+                               wicket:id="groupCollapsible"></span><span wicket:id="groupName">[group
+                                       name]</span></td>
+                       <td colspan="6" style="padding: 2px;"><span class="hidden-phone"
+                               style="font-weight: normal; color: #666;"
+                               wicket:id="groupDescription">[description]</span></td>
+               </wicket:fragment>
+
+       </wicket:panel>
+</body>
+</html>
diff --git a/src/main/java/com/gitblit/wicket/panels/NestedRepositoryTreePanel.java b/src/main/java/com/gitblit/wicket/panels/NestedRepositoryTreePanel.java
new file mode 100644 (file)
index 0000000..fbe1991
--- /dev/null
@@ -0,0 +1,231 @@
+package com.gitblit.wicket.panels;
+
+import java.util.Map;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.behavior.AttributeAppender;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.list.ListItem;
+import org.apache.wicket.markup.html.list.ListView;
+import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.markup.repeater.RepeatingView;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.Keys;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.TreeNodeModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.ModelUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.BasePage;
+import com.gitblit.wicket.pages.ProjectPage;
+import com.gitblit.wicket.pages.SummaryPage;
+import com.gitblit.wicket.pages.UserPage;
+
+public class NestedRepositoryTreePanel extends BasePanel {
+
+       private static final long serialVersionUID = 1L;
+
+       public NestedRepositoryTreePanel(final String wicketId, final IModel<TreeNodeModel> model, final Map<AccessRestrictionType, String> accessRestrictionTranslations, final boolean linksActive) {
+               super(wicketId);
+
+               final boolean showSize = app().settings().getBoolean(Keys.web.showRepositorySizes, true);
+               final boolean showSwatch = app().settings().getBoolean(Keys.web.repositoryListSwatches, true);
+
+               final TreeNodeModel node = model.getObject();
+               Fragment nodeHeader = new Fragment("nodeHeader", "groupRepositoryRow", this);
+               add(nodeHeader);
+               WebMarkupContainer firstColumn = new WebMarkupContainer("firstColumn");
+               nodeHeader.add(firstColumn);
+               RepeatingView depth = new RepeatingView("depth");
+               for(int i=0; i<node.getDepth();i++) {
+                       depth.add(new WebMarkupContainer(depth.newChildId()));
+               }
+               firstColumn.add(depth);
+               firstColumn.add(new Fragment("groupCollapsible", "tableGroupMinusCollapsible", this));
+               if(node.getParent()!=null) {
+                       addChildOfNodeIdCssClassesToRow(nodeHeader, node.getParent());
+               }
+               nodeHeader.add(new AttributeAppender("data-node-id", Model.of(node.hashCode()), " "));
+
+               String name = node.getName();
+               if (name.startsWith(ModelUtils.getUserRepoPrefix())) {
+                       // user page
+                       String username = ModelUtils.getUserNameFromRepoPath(name);
+                       UserModel user = app().users().getUserModel(username);
+                       firstColumn.add(new LinkPanel("groupName", null, (user == null ? username : user.getDisplayName()), UserPage.class, WicketUtils.newUsernameParameter(username)));
+                       nodeHeader.add(new Label("groupDescription", getString("gb.personalRepositories")));
+               } else {
+                       // project page
+                       firstColumn.add(new LinkPanel("groupName", null, name, ProjectPage.class, WicketUtils.newProjectParameter(name)));
+                       nodeHeader.add(new Label("groupDescription", ""));
+               }
+               WicketUtils.addCssClass(nodeHeader, "group collapsible tree");
+
+               add(new ListView<TreeNodeModel>("subFolders", node.getSubFolders()) {
+                       private static final long serialVersionUID = 1L;
+
+                       @Override
+                       protected void populateItem(ListItem<TreeNodeModel> item) {
+                               item.add(new NestedRepositoryTreePanel("rowContent", item.getModel(), accessRestrictionTranslations, linksActive));
+                       }
+
+                       @Override
+                       public boolean isVisible() {
+                               return super.isVisible() && !node.getSubFolders().isEmpty();
+                       }
+               });
+
+               add(new ListView<RepositoryModel>("repositories", node.getRepositories()) {
+                       private static final long serialVersionUID = 1L;
+
+                       int counter = 0;
+
+                       @Override
+                       public boolean isVisible() {
+                               return super.isVisible() && !node.getRepositories().isEmpty();
+                       }
+
+                       @Override
+                       protected void populateItem(ListItem<RepositoryModel> item) {
+
+                               RepositoryModel entry = item.getModelObject();
+                               WebMarkupContainer rowContent = new WebMarkupContainer("rowContent");
+                               item.add(rowContent);
+                               addChildOfNodeIdCssClassesToRow(rowContent, node);
+                               WebMarkupContainer firstColumn = new WebMarkupContainer("firstColumn");
+                               rowContent.add(firstColumn);
+                               RepeatingView depth = new RepeatingView("depth");
+                               for(int i=0; i<node.getDepth();i++) {
+                                       depth.add(new WebMarkupContainer(depth.newChildId()));
+                               }
+                               firstColumn.add(depth);
+
+                               // show colored repository type icon
+                               Fragment iconFragment;
+                               if (entry.isMirror) {
+                                       iconFragment = new Fragment("repoIcon", "mirrorIconFragment", this);
+                               } else if (entry.isFork()) {
+                                       iconFragment = new Fragment("repoIcon", "forkIconFragment", this);
+                               } else if (entry.isBare) {
+                                       iconFragment = new Fragment("repoIcon", "repoIconFragment", this);
+                               } else {
+                                       iconFragment = new Fragment("repoIcon", "cloneIconFragment", this);
+                               }
+                               if (showSwatch) {
+                                       WicketUtils.setCssStyle(iconFragment, "color:" + StringUtils.getColor(entry.toString()));
+                               }
+                               firstColumn.add(iconFragment);
+
+                               // try to strip group name for less cluttered list
+                               String repoName = StringUtils.getLastPathElement(entry.toString());
+
+                               if (linksActive) {
+                                       Class<? extends BasePage> linkPage = SummaryPage.class;
+                                       PageParameters pp = WicketUtils.newRepositoryParameter(entry.name);
+                                       firstColumn.add(new LinkPanel("repositoryName", "list", repoName, linkPage, pp));
+                                       rowContent.add(new LinkPanel("repositoryDescription", "list", entry.description, linkPage, pp));
+                               } else {
+                                       // no links like on a federation page
+                                       firstColumn.add(new Label("repositoryName", repoName));
+                                       rowContent.add(new Label("repositoryDescription", entry.description));
+                               }
+                               if (entry.hasCommits) {
+                                       // Existing repository
+                                       rowContent.add(new Label("repositorySize", entry.size).setVisible(showSize));
+                               } else {
+                                       // New repository
+                                       rowContent.add(new Label("repositorySize", "<span class='empty'>(" + getString("gb.empty") + ")</span>").setEscapeModelStrings(false));
+                               }
+
+                               if (entry.isSparkleshared()) {
+                                       rowContent.add(WicketUtils.newImage("sparkleshareIcon", "star_16x16.png", getString("gb.isSparkleshared")));
+                               } else {
+                                       rowContent.add(WicketUtils.newClearPixel("sparkleshareIcon").setVisible(false));
+                               }
+
+                               if (!entry.isMirror && entry.isFrozen) {
+                                       rowContent.add(WicketUtils.newImage("frozenIcon", "cold_16x16.png", getString("gb.isFrozen")));
+                               } else {
+                                       rowContent.add(WicketUtils.newClearPixel("frozenIcon").setVisible(false));
+                               }
+
+                               if (entry.isFederated) {
+                                       rowContent.add(WicketUtils.newImage("federatedIcon", "federated_16x16.png", getString("gb.isFederated")));
+                               } else {
+                                       rowContent.add(WicketUtils.newClearPixel("federatedIcon").setVisible(false));
+                               }
+
+                               if (entry.isMirror) {
+                                       rowContent.add(WicketUtils.newImage("accessRestrictionIcon", "mirror_16x16.png", getString("gb.isMirror")));
+                               } else {
+                                       switch (entry.accessRestriction) {
+                                       case NONE:
+                                               rowContent.add(WicketUtils.newBlankImage("accessRestrictionIcon"));
+                                               break;
+                                       case PUSH:
+                                               rowContent.add(WicketUtils.newImage("accessRestrictionIcon", "lock_go_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction)));
+                                               break;
+                                       case CLONE:
+                                               rowContent.add(WicketUtils.newImage("accessRestrictionIcon", "lock_pull_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction)));
+                                               break;
+                                       case VIEW:
+                                               rowContent.add(WicketUtils.newImage("accessRestrictionIcon", "shield_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction)));
+                                               break;
+                                       default:
+                                               rowContent.add(WicketUtils.newBlankImage("accessRestrictionIcon"));
+                                       }
+                               }
+
+                               String owner = "";
+                               if (!ArrayUtils.isEmpty(entry.owners)) {
+                                       // display first owner
+                                       for (String username : entry.owners) {
+                                               UserModel ownerModel = app().users().getUserModel(username);
+                                               if (ownerModel != null) {
+                                                       owner = ownerModel.getDisplayName();
+                                                       break;
+                                               }
+                                       }
+                                       if (entry.owners.size() > 1) {
+                                               owner += ", ...";
+                                       }
+                               }
+                               Label ownerLabel = new Label("repositoryOwner", owner);
+                               WicketUtils.setHtmlTooltip(ownerLabel, ArrayUtils.toString(entry.owners));
+                               rowContent.add(ownerLabel);
+
+                               String lastChange;
+                               if (entry.lastChange.getTime() == 0) {
+                                       lastChange = "--";
+                               } else {
+                                       lastChange = getTimeUtils().timeAgo(entry.lastChange);
+                               }
+                               Label lastChangeLabel = new Label("repositoryLastChange", lastChange);
+                               rowContent.add(lastChangeLabel);
+                               WicketUtils.setCssClass(lastChangeLabel, getTimeUtils().timeAgoCss(entry.lastChange));
+                               if (!StringUtils.isEmpty(entry.lastChangeAuthor)) {
+                                       WicketUtils.setHtmlTooltip(lastChangeLabel, getString("gb.author") + ": " + entry.lastChangeAuthor);
+                               }
+
+                               String clazz = counter % 2 == 0 ? "light" : "dark";
+                               WicketUtils.addCssClass(rowContent, clazz);
+                               counter++;
+                       }
+               });
+
+       }
+
+       private void addChildOfNodeIdCssClassesToRow(Component row, TreeNodeModel parentNode) {
+               row.add(new AttributeAppender("class", Model.of("child-of-"+ parentNode.hashCode()), " "));
+               if(parentNode.getParent() != null) {
+                       addChildOfNodeIdCssClassesToRow(row, parentNode.getParent());
+               }
+       }
+}
index 7c3b3082b175747f7f6634a7e980603369618a7b..35a26b6167f09d5d40829a207c550f001e320497 100644 (file)
        \r
 </wicket:panel>\r
 </body>\r
-</html>
\ No newline at end of file
+</html>\r
index aab602e5c75d847ef466a4e82f0bacf9a82a072a..982f8b2d571dda33febfd0212bfc0dacda418862 100644 (file)
@@ -28,6 +28,7 @@ import org.apache.wicket.PageParameters;
 import org.apache.wicket.extensions.markup.html.repeater.data.sort.OrderByBorder;\r
 import org.apache.wicket.extensions.markup.html.repeater.util.SortParam;\r
 import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider;\r
+import org.apache.wicket.markup.html.WebMarkupContainer;\r
 import org.apache.wicket.markup.html.basic.Label;\r
 import org.apache.wicket.markup.html.link.BookmarkablePageLink;\r
 import org.apache.wicket.markup.html.link.Link;\r
@@ -43,6 +44,7 @@ import com.gitblit.Constants.AccessRestrictionType;
 import com.gitblit.Keys;\r
 import com.gitblit.models.ProjectModel;\r
 import com.gitblit.models.RepositoryModel;\r
+import com.gitblit.models.TreeNodeModel;\r
 import com.gitblit.models.UserModel;\r
 import com.gitblit.utils.ArrayUtils;\r
 import com.gitblit.utils.ModelUtils;\r
@@ -58,7 +60,8 @@ import com.gitblit.wicket.pages.UserPage;
 public class RepositoriesPanel extends BasePanel {\r
 \r
        private static final long serialVersionUID = 1L;\r
-       \r
+\r
+\r
        private enum CollapsibleRepositorySetting {\r
                DISABLED,\r
 \r
@@ -90,7 +93,7 @@ public class RepositoriesPanel extends BasePanel {
 \r
                final UserModel user = GitBlitWebSession.get().getUser();\r
 \r
-               final IDataProvider<RepositoryModel> dp;\r
+               IDataProvider<RepositoryModel> dp = null;\r
 \r
                Fragment managementLinks;\r
                if (showAdmin) {\r
@@ -118,7 +121,28 @@ public class RepositoriesPanel extends BasePanel {
                        add (new Label("managementPanel").setVisible(false));\r
                }\r
 \r
-               if (app().settings().getString(Keys.web.repositoryListType, "flat").equalsIgnoreCase("grouped")) {\r
+               if (app().settings().getString(Keys.web.repositoryListType, "flat").equalsIgnoreCase("tree")) {\r
+                       TreeNodeModel tree = new TreeNodeModel();\r
+                       for (RepositoryModel model : models) {\r
+                               String rootPath = StringUtils.getRootPath(model.name);\r
+                               if (StringUtils.isEmpty(rootPath)) {\r
+                                       tree.add(model);\r
+                               } else {\r
+                                       // create folder structure\r
+                                       tree.add(rootPath, model);\r
+                               }\r
+                       }\r
+\r
+                       WebMarkupContainer container = new WebMarkupContainer("row");\r
+                       add(container);\r
+                       container.add(new NestedRepositoryTreePanel("rowContent", Model.of(tree), accessRestrictionTranslations, enableLinks));\r
+\r
+                       Fragment fragment = new Fragment("headerContent", "groupRepositoryHeader", this);\r
+                       Fragment allCollapsible = new Fragment("allCollapsible", "tableAllCollapsible", this);\r
+                       fragment.add(allCollapsible);\r
+                       add(fragment);\r
+\r
+               } else if (app().settings().getString(Keys.web.repositoryListType, "flat").equalsIgnoreCase("grouped")) {\r
                        List<RepositoryModel> rootRepositories = new ArrayList<RepositoryModel>();\r
                        Map<String, List<RepositoryModel>> groups = new HashMap<String, List<RepositoryModel>>();\r
                        for (RepositoryModel model : models) {\r
@@ -161,6 +185,7 @@ public class RepositoriesPanel extends BasePanel {
                        dp = new SortableRepositoriesProvider(models);\r
                }\r
 \r
+               if (dp != null) {\r
                final boolean showSwatch = app().settings().getBoolean(Keys.web.repositoryListSwatches, true);\r
 \r
                DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("row", dp) {\r
@@ -350,7 +375,7 @@ public class RepositoriesPanel extends BasePanel {
                } else {\r
                        // not sortable\r
                        Fragment fragment = new Fragment("headerContent", "groupRepositoryHeader", this);\r
-                       if(collapsibleRepoGroups == CollapsibleRepositorySetting.EXPANDED || \r
+                       if(collapsibleRepoGroups == CollapsibleRepositorySetting.EXPANDED ||\r
                                        collapsibleRepoGroups == CollapsibleRepositorySetting.COLLAPSED) {\r
                                Fragment allCollapsible = new Fragment("allCollapsible", "tableAllCollapsible", this);\r
                                fragment.add(allCollapsible);\r
@@ -360,6 +385,7 @@ public class RepositoriesPanel extends BasePanel {
                        }\r
                        add(fragment);\r
                }\r
+               }\r
        }\r
 \r
        private static class GroupRepositoryModel extends RepositoryModel {\r
index 538b412e6ed49a05e88e6f106e3a5d3daad3afdf..ca89b8fd8794a56472a66ba31ea4a96ddf62f0b0 100644 (file)
@@ -1,32 +1,71 @@
 $(function() {\r
        $('i.table-group-collapsible')\r
                .click(function(){\r
-                       $(this).closest('tr.group.collapsible').nextUntil('tr.group.collapsible').toggle();\r
+                       var nodeId = $(this).closest('tr.group.collapsible.tree').data('nodeId');\r
+                       if(nodeId!==undefined){\r
+                               //we are in tree view\r
+                               if($(this).hasClass('fa-minus-square-o')){\r
+                                       $(this).closest('tr.group.collapsible.tree').nextAll('tr.child-of-'+nodeId).hide();\r
+                                       $(this).closest('tr.group.collapsible.tree').nextAll('tr.child-of-'+nodeId).addClass('hidden-by-'+nodeId);\r
+                               }else{\r
+                                       $(this).closest('tr.group.collapsible.tree').nextAll('tr.child-of-'+nodeId).removeClass('hidden-by-'+nodeId);\r
+                                       $(this).closest('tr.group.collapsible.tree').nextAll('tr.child-of-'+nodeId+':not([class*="hidden-by-"])').show();\r
+                               }\r
+                       }else{\r
+                               $(this).closest('tr.group.collapsible').nextUntil('tr.group.collapsible').toggle();\r
+                       }\r
                        $(this).toggleClass('fa-minus-square-o');\r
                        $(this).toggleClass('fa-plus-square-o');\r
                });\r
-       \r
+\r
+\r
        $('i.table-openall-collapsible')\r
                .click(function(){\r
                        $('tr.group.collapsible').first().find('i').addClass('fa-minus-square-o');\r
                        $('tr.group.collapsible').first().find('i').removeClass('fa-plus-square-o');\r
-                       $('tr.group.collapsible').first().nextAll('tr:not(tr.group.collapsible)').show();\r
+                       $('tr.group.collapsible').first().nextAll('tr:not(tr.group.collapsible),tr.group.collapsible.tree').show();\r
                        $('tr.group.collapsible').first().nextAll('tr.group.collapsible').find('i').addClass('fa-minus-square-o');\r
                        $('tr.group.collapsible').first().nextAll('tr.group.collapsible').find('i').removeClass('fa-plus-square-o');\r
+\r
+                       var nodeId = $('tr.group.collapsible.tree').data('nodeId');\r
+                       if(nodeId!==undefined){\r
+                               //we are in tree view\r
+                               $('tr[class*="child-of-"]').removeClass(function(index, className){\r
+                                       return (className.match(/\hidden-by-\S+/g)||[]).join(' ');\r
+                               });\r
+                               $('tr.group.collapsible > i').addClass('fa-minus-square-o');\r
+                               $('tr.group.collapsible > i').removeClass('fa-plus-square-o');\r
+                       }\r
                });\r
-       \r
+\r
        $('i.table-closeall-collapsible')\r
                .click(function(){\r
                        $('tr.group.collapsible').first().find('i').addClass('fa-plus-square-o');\r
                        $('tr.group.collapsible').first().find('i').removeClass('fa-minus-square-o');\r
-                       $('tr.group.collapsible').first().nextAll('tr:not(tr.group.collapsible)').hide();\r
+                       $('tr.group.collapsible').first().nextAll('tr:not(tr.group.collapsible),tr.group.collapsible.tree').hide();\r
                        $('tr.group.collapsible').first().nextAll('tr.group.collapsible').find('i').addClass('fa-plus-square-o');\r
                        $('tr.group.collapsible').first().nextAll('tr.group.collapsible').find('i').removeClass('fa-minus-square-o');\r
+\r
+                       var nodeId = $('tr.group.collapsible.tree').first().data('nodeId');\r
+                       if(nodeId!==undefined){\r
+                               //we are in tree view, hide all sub trees\r
+                               $('tr[class*="child-of-"]').each(function(){\r
+                                       var row = $(this);\r
+                                       var classList = row.attr('class').split('/\s+/');\r
+                                       $.each(classList, function(index, c){\r
+                                               if(c.match(/^child-of-*/)){\r
+                                                       row.addClass(c.replace(/^child-of-(\d)/, 'hidden-by-$1'));\r
+                                               }\r
+                                       });\r
+                               });\r
+                               $('tr.group.collapsible i').addClass('fa-plus-square-o');\r
+                               $('tr.group.collapsible i').removeClass('fa-minus-square-o');\r
+                       }\r
                });\r
-       \r
+\r
        $( document ).ready(function() {\r
                if($('tr.group.collapsible').first().find('i').hasClass('fa-plus-square-o')) {\r
-                       $('tr.group.collapsible').first().nextAll('tr:not(tr.group.collapsible)').hide();\r
+                       $('tr.group.collapsible').first().nextAll('tr:not(tr.group.collapsible),tr.group.collapsible.tree').hide();\r
                }\r
        });\r
-});
\ No newline at end of file
+});
diff --git a/src/test/java/com/gitblit/wicket/panels/TreeNodeModelTest.java b/src/test/java/com/gitblit/wicket/panels/TreeNodeModelTest.java
new file mode 100644 (file)
index 0000000..449688b
--- /dev/null
@@ -0,0 +1,49 @@
+package com.gitblit.wicket.panels;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.TreeNodeModel;
+
+public class TreeNodeModelTest {
+
+    @Test
+    public void testContainsSubFolder() {
+        TreeNodeModel tree = new TreeNodeModel();
+        tree.add("foo").add("bar").add("baz");
+
+        assertTrue(tree.containsSubFolder("foo/bar/baz"));
+        assertTrue(tree.containsSubFolder("foo/bar"));
+        assertFalse(tree.containsSubFolder("foo/bar/blub"));
+    }
+
+    @Test
+    public void testAddInHierarchy() {
+        TreeNodeModel tree = new TreeNodeModel();
+        tree.add("foo").add("bar");
+
+        RepositoryModel model = new RepositoryModel("test","","",null);
+
+        // add model to non-existing folder. should be created automatically
+        tree.add("foo/bar/baz", model);
+        tree.add("another/non/existing/folder", model);
+
+        assertTrue(tree.containsSubFolder("foo/bar/baz"));
+        assertTrue(tree.containsSubFolder("another/non/existing/folder"));
+    }
+
+    @Test
+    public void testGetDepth() {
+        TreeNodeModel tree = new TreeNodeModel();
+        TreeNodeModel bar = tree.add("foo").add("bar").add("baz");
+
+        assertEquals(0, tree.getDepth());
+        assertEquals(3, bar.getDepth());
+    }
+
+
+}