]> source.dussan.org Git - gitblit.git/commitdiff
Add repositoryListType tree. Addresses #725, 527 and includes #1224
authorMartin Spielmann <mail@martinspielmann.de>
Mon, 6 Nov 2017 12:45:39 +0000 (13:45 +0100)
committerMartin Spielmann <mail@martinspielmann.de>
Mon, 6 Nov 2017 12:45:39 +0000 (13:45 +0100)
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/java/com/gitblit/wicket/panels/TreeNodeModel.java [deleted file]
src/main/resources/gitblit/js/collapsible-table.js
src/test/java/com/gitblit/wicket/panels/TreeNodeModelTest.java

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..563e022
--- /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">\r
+<html xmlns="http://www.w3.org/1999/xhtml"\r
+       xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"\r
+       xml:lang="en" lang="en">\r
+\r
+<body>\r
+       <wicket:panel>\r
+               <tr style="background-color: #bbb" wicket:id="nodeHeader" data-row-type="folder"></tr>\r
+               <tr wicket:id="subFolders">\r
+                       <span wicket:id="rowContent"></span>\r
+               </tr>\r
+               <wicket:container wicket:id="repositories">\r
+               <tr  wicket:id="rowContent" data-row-type="repo">\r
+                       <td wicket:id="firstColumn" class="left"\r
+                               style="padding-left: 3px;">\r
+                               <div style="border-left: 1px solid black; margin-left:6px; width: 19px;display: inline-block;float: left;"\r
+                                       wicket:id="depth">&nbsp;</div> \r
+                                       <span wicket:id="repoIcon"></span><span\r
+                               style="padding-left: 3px;" wicket:id="repositoryName">[repository\r
+                                       name]</span>\r
+                       </td>\r
+                       <td class="hidden-phone"><span class="list"\r
+                               wicket:id="repositoryDescription">[repository description]</span></td>\r
+                       <td class="hidden-tablet hidden-phone author"><span\r
+                               wicket:id="repositoryOwner">[repository owner]</span></td>\r
+                       <td class="hidden-phone"\r
+                               style="text-align: right; padding-right: 10px;"><img\r
+                               class="inlineIcon" wicket:id="sparkleshareIcon" /><img\r
+                               class="inlineIcon" wicket:id="frozenIcon" /><img class="inlineIcon"\r
+                               wicket:id="federatedIcon" /><img class="inlineIcon"\r
+                               wicket:id="accessRestrictionIcon" /></td>\r
+                       <td><span wicket:id="repositoryLastChange">[last change]</span></td>\r
+                       <td class="rightAlign hidden-phone"\r
+                               style="text-align: right; padding-right: 15px;"><span\r
+                               style="font-size: 0.8em;" wicket:id="repositorySize">[repository\r
+                                       size]</span></td>\r
+               </tr>\r
+               </wicket:container>\r
+\r
+               <wicket:fragment wicket:id="emptyFragment">\r
+               </wicket:fragment>\r
+\r
+               <wicket:fragment wicket:id="repoIconFragment">\r
+                       <span class="octicon octicon-centered octicon-repo"></span>\r
+               </wicket:fragment>\r
+\r
+               <wicket:fragment wicket:id="mirrorIconFragment">\r
+                       <span class="octicon octicon-centered octicon-mirror"></span>\r
+               </wicket:fragment>\r
+\r
+               <wicket:fragment wicket:id="forkIconFragment">\r
+                       <span class="octicon octicon-centered octicon-repo-forked"></span>\r
+               </wicket:fragment>\r
+\r
+               <wicket:fragment wicket:id="cloneIconFragment">\r
+                       <span class="octicon octicon-centered octicon-repo-push"\r
+                               wicket:message="title:gb.workingCopyWarning"></span>\r
+               </wicket:fragment>\r
+               \r
+               <wicket:fragment wicket:id="tableGroupMinusCollapsible">\r
+               <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>\r
+    </wicket:fragment>\r
+    \r
+    <wicket:fragment wicket:id="tableGroupPlusCollapsible">\r
+               <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>\r
+    </wicket:fragment>\r
+\r
+               <wicket:fragment wicket:id="tableAllCollapsible">\r
+                       <i title="Click to expand all"\r
+                               class="fa fa-plus-square-o table-openall-collapsible"\r
+                               aria-hidden="true" style="padding-right: 3px; cursor: pointer;"></i>\r
+                       <i title="Click to collapse all"\r
+                               class="fa fa-minus-square-o table-closeall-collapsible"\r
+                               aria-hidden="true" style="padding-right: 3px; cursor: pointer;"></i>\r
+               </wicket:fragment>\r
+\r
+               <wicket:fragment wicket:id="groupRepositoryHeader">\r
+                       <tr>\r
+                               <th class="left"><span wicket:id="allCollapsible"></span> <img\r
+                                       style="vertical-align: middle;" src="git-black-16x16.png" /> <wicket:message\r
+                                               key="gb.repository">Repository</wicket:message></th>\r
+                               <th class="hidden-phone"><span><wicket:message\r
+                                                       key="gb.description">Description</wicket:message></span></th>\r
+                               <th class="hidden-tablet hidden-phone"><span><wicket:message\r
+                                                       key="gb.owner">Owner</wicket:message></span></th>\r
+                               <th class="hidden-phone"></th>\r
+                               <th><wicket:message key="gb.lastChange">Last Change</wicket:message></th>\r
+                               <th class="right hidden-phone"></th>\r
+                       </tr>\r
+               </wicket:fragment>\r
+\r
+               <wicket:fragment wicket:id="groupRepositoryRow">\r
+                       <td wicket:id="firstColumn" style="" colspan="1">\r
+                       <div style="border-left: 1px solid black; margin-left:6px; width: 19px; display: inline-block;float: left;"\r
+                                       wicket:id="depth">&nbsp;</div> \r
+                       <span\r
+                               wicket:id="groupCollapsible"></span><span wicket:id="groupName">[group\r
+                                       name]</span></td>\r
+                       <td colspan="6" style="padding: 2px;"><span class="hidden-phone"\r
+                               style="font-weight: normal; color: #666;"\r
+                               wicket:id="groupDescription">[description]</span></td>\r
+               </wicket:fragment>\r
+\r
+       </wicket:panel>\r
+</body>\r
+</html>
\ No newline at end of file
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..6c7b01f
--- /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(String wicketId, IModel<TreeNodeModel> model, final Map<AccessRestrictionType, String> accessRestrictionTranslations, boolean linksActive) {
+        super(wicketId);
+
+        final boolean showSize = app().settings().getBoolean(Keys.web.showRepositorySizes, true);
+        final boolean showSwatch = app().settings().getBoolean(Keys.web.repositoryListSwatches, true);
+
+        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..bb5f67593c3fb8a2e9c632c12d5b8779f87f6498 100644 (file)
     </wicket:fragment>\r
        \r
        <wicket:fragment wicket:id="groupRepositoryRow">\r
-        <td colspan="1"><span wicket:id="groupCollapsible"></span><span wicket:id="groupName">[group name]</span></td>\r
+        <td  colspan="1"><span wicket:id="groupCollapsible"></span><span wicket:id="groupName">[group name]</span></td>\r
         <td colspan="6" style="padding: 2px;"><span class="hidden-phone" style="font-weight:normal;color:#666;" wicket:id="groupDescription">[description]</span></td>\r
        </wicket:fragment>\r
                \r
index 6d68022946be731c94185073238c2ca2128ea8c4..4b16c20150d505c4645340cec4e63b88970b8957 100644 (file)
@@ -30,7 +30,6 @@ import org.apache.wicket.extensions.markup.html.repeater.util.SortParam;
 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.basic.MultiLineLabel;\r
 import org.apache.wicket.markup.html.link.BookmarkablePageLink;\r
 import org.apache.wicket.markup.html.link.Link;\r
 import org.apache.wicket.markup.html.panel.Fragment;\r
@@ -45,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
@@ -79,6 +79,7 @@ public class RepositoriesPanel extends BasePanel {
             return returnVal;\r
         }\r
     }\r
+\r
     public RepositoriesPanel(String wicketId, final boolean showAdmin, final boolean showManagement, List<RepositoryModel> models, boolean enableLinks,\r
             final Map<AccessRestrictionType, String> accessRestrictionTranslations) {\r
         super(wicketId);\r
@@ -118,38 +119,27 @@ public class RepositoriesPanel extends BasePanel {
             add(new Label("managementPanel").setVisible(false));\r
         }\r
 \r
-        if (true) {\r
-            // if (app().settings().getString(Keys.web.repositoryListType,\r
-            // "flat").equalsIgnoreCase("tree")) {\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
-                    // root repository\r
-                    // rootRepositories.add(model);\r
                     tree.add(model);\r
                 } else {\r
                     // create folder structure\r
                     tree.add(rootPath, model);\r
-                    // non-root, grouped repository\r
-                    // if (!groups.containsKey(rootPath)) {\r
-                    // groups.put(rootPath, new ArrayList<RepositoryModel>());\r
-                    // }\r
-                    // groups.get(rootPath).add(model);\r
                 }\r
             }\r
 \r
-            WebMarkupContainer row = new WebMarkupContainer("row");\r
-            add(row);\r
-            row.add(new MultiLineLabel("rowContent", tree.toString()));\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
-\r
-\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
@@ -214,10 +204,10 @@ public class RepositoriesPanel extends BasePanel {
                         GroupRepositoryModel groupRow = (GroupRepositoryModel) entry;\r
                         currGroupName = entry.name;\r
                         Fragment row = new Fragment("rowContent", "groupRepositoryRow", this);\r
-                        if(collapsibleRepoGroups == CollapsibleRepositorySetting.EXPANDED) {\r
+                        if (collapsibleRepoGroups == CollapsibleRepositorySetting.EXPANDED) {\r
                             Fragment groupCollapsible = new Fragment("groupCollapsible", "tableGroupMinusCollapsible", this);\r
                             row.add(groupCollapsible);\r
-                        } else if(collapsibleRepoGroups == CollapsibleRepositorySetting.COLLAPSED) {\r
+                        } else if (collapsibleRepoGroups == CollapsibleRepositorySetting.COLLAPSED) {\r
                             Fragment groupCollapsible = new Fragment("groupCollapsible", "tableGroupPlusCollapsible", this);\r
                             row.add(groupCollapsible);\r
                         } else {\r
@@ -375,8 +365,7 @@ public class RepositoriesPanel extends BasePanel {
             } else {\r
                 // not sortable\r
                 Fragment fragment = new Fragment("headerContent", "groupRepositoryHeader", this);\r
-                if(collapsibleRepoGroups == CollapsibleRepositorySetting.EXPANDED ||\r
-                        collapsibleRepoGroups == CollapsibleRepositorySetting.COLLAPSED) {\r
+                if (collapsibleRepoGroups == CollapsibleRepositorySetting.EXPANDED || collapsibleRepoGroups == CollapsibleRepositorySetting.COLLAPSED) {\r
                     Fragment allCollapsible = new Fragment("allCollapsible", "tableAllCollapsible", this);\r
                     fragment.add(allCollapsible);\r
                 } else {\r
@@ -386,8 +375,6 @@ public class RepositoriesPanel extends BasePanel {
                 add(fragment);\r
             }\r
         }\r
-\r
-\r
     }\r
 \r
     private static class GroupRepositoryModel extends RepositoryModel {\r
diff --git a/src/main/java/com/gitblit/wicket/panels/TreeNodeModel.java b/src/main/java/com/gitblit/wicket/panels/TreeNodeModel.java
deleted file mode 100644 (file)
index 4887c65..0000000
+++ /dev/null
@@ -1,142 +0,0 @@
-package com.gitblit.wicket.panels;
-
-import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.List;
-
-import com.gitblit.models.RepositoryModel;
-import com.gitblit.utils.StringUtils;
-
-public class TreeNodeModel implements Serializable {
-
-    private static final long serialVersionUID = 1L;
-    final TreeNodeModel parent;
-    final String name;
-    private final List<TreeNodeModel> subFolders = new ArrayList<>();
-    private 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);
-        return n;
-    }
-
-    /**
-     * Add the given repo to the current folder
-     *
-     * @param repo
-     */
-    public void add(RepositoryModel repo) {
-        repositories.add(repo);
-    }
-
-    /**
-     * 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);
-    }
-
-
-
-
-    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 = path.substring(0, path.indexOf('/'));
-
-                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;
-    }
-}
index 538b412e6ed49a05e88e6f106e3a5d3daad3afdf..fc28b1e436c8e4adfe549a05d40bb2bdb1a21539 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
        $('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
        $('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
        $( 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
index 57051dd43e95025edb9eaa0e2485b724fa226128..449688b0f5334ddef105c49011bf8667150b4ad6 100644 (file)
@@ -7,6 +7,7 @@ import static org.junit.Assert.assertTrue;
 import org.junit.Test;
 
 import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.TreeNodeModel;
 
 public class TreeNodeModelTest {