--- /dev/null
+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;
+ }
+}
--- /dev/null
+<!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"> </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"> </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
--- /dev/null
+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());
+ }
+ }
+}
</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
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
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
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
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
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
} 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
add(fragment);\r
}\r
}\r
-\r
-\r
}\r
\r
private static class GroupRepositoryModel extends RepositoryModel {\r
+++ /dev/null
-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;
- }
-}
$(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
import org.junit.Test;
import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.TreeNodeModel;
public class TreeNodeModelTest {