diff --git a/.classpath b/.classpath index 42cbfbc87..c66b3856a 100644 --- a/.classpath +++ b/.classpath @@ -80,6 +80,7 @@ + diff --git a/build.moxie b/build.moxie index f21241d1b..fd6471eb6 100644 --- a/build.moxie +++ b/build.moxie @@ -181,6 +181,7 @@ dependencies: - compile 'ro.fortsoft.pf4j:pf4j:0.9.0' :war - compile 'org.apache.tika:tika-core:1.5' :war - compile 'org.jsoup:jsoup:1.7.3' :war +- compile 'javax.activation:javax.activation-api:1.2.0' - test 'junit:junit:4.12' # Dependencies for Selenium web page testing - test 'org.seleniumhq.selenium:selenium-java:${selenium.version}' @jar diff --git a/gitblit.iml b/gitblit.iml index 1f4aa2483..0c059a5e1 100644 --- a/gitblit.iml +++ b/gitblit.iml @@ -824,6 +824,17 @@ + + + + + + + + + + + diff --git a/src/main/distrib/data/defaults.properties b/src/main/distrib/data/defaults.properties index 51fa1253a..157731c5c 100644 --- a/src/main/distrib/data/defaults.properties +++ b/src/main/distrib/data/defaults.properties @@ -2147,3 +2147,17 @@ filestore.storageFolder = ${baseFolder}/lfs # Common unit suffixes of k, m, or g are supported. # SINCE 1.7.0 filestore.maxUploadSize = -1 + +# Specify the behaviour of the Repository groups on the "Repositories" +# page, specifically whether they can be collapsed and expanded, and +# their default state on loading the page. +# Only on repositoryListType grouped +# +# Values (case-insensitive): +# disabled - Repository groups cannot collapsed; maintains behaviour +# from previous versions of GitBlit. +# expanded - On loading the page all repository groups are expanded. +# collapsed - On loading the page all repository groups are collapsed. +# +# SINCE 1.9.0 +web.collapsibleRepositoryGroups = disabled diff --git a/src/main/java/com/gitblit/models/TreeNodeModel.java b/src/main/java/com/gitblit/models/TreeNodeModel.java new file mode 100644 index 000000000..a69393e25 --- /dev/null +++ b/src/main/java/com/gitblit/models/TreeNodeModel.java @@ -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 { + + private static final long serialVersionUID = 1L; + final TreeNodeModel parent; + final String name; + final List subFolders = new ArrayList<>(); + final List 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 getTreeAsListForFrontend(){ + List l = new ArrayList<>(); + getTreeAsListForFrontend(l, this); + return l; + } + + private static void getTreeAsListForFrontend(List 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 getSubFolders() { + return subFolders; + } + + public List getRepositories() { + return repositories; + } +} diff --git a/src/main/java/com/gitblit/wicket/pages/BasePage.html b/src/main/java/com/gitblit/wicket/pages/BasePage.html index 4dbc2e574..49810ddc0 100644 --- a/src/main/java/com/gitblit/wicket/pages/BasePage.html +++ b/src/main/java/com/gitblit/wicket/pages/BasePage.html @@ -51,7 +51,8 @@ - + + \ No newline at end of file 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 index 000000000..563e022fa --- /dev/null +++ b/src/main/java/com/gitblit/wicket/panels/NestedRepositoryTreePanel.html @@ -0,0 +1,106 @@ + + + + + + + + + + + + +
 
+ [repository + name] + + [repository description] + [repository owner] + + [last change] + [repository + size] + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Repository + Description + Owner + + Last Change + + + + + + +
 
+ [group + name] + [description] +
+ +
+ + \ 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 index 000000000..fbe1991d3 --- /dev/null +++ b/src/main/java/com/gitblit/wicket/panels/NestedRepositoryTreePanel.java @@ -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 model, final Map 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("subFolders", node.getSubFolders()) { + private static final long serialVersionUID = 1L; + + @Override + protected void populateItem(ListItem item) { + item.add(new NestedRepositoryTreePanel("rowContent", item.getModel(), accessRestrictionTranslations, linksActive)); + } + + @Override + public boolean isVisible() { + return super.isVisible() && !node.getSubFolders().isEmpty(); + } + }); + + add(new ListView("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 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 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", "(" + getString("gb.empty") + ")").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()); + } + } +} diff --git a/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html b/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html index 2de52b09f..bb5f67593 100644 --- a/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html +++ b/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html @@ -18,6 +18,9 @@ + + + @@ -72,9 +75,15 @@ + + + + + + Repository @@ -86,8 +95,16 @@ + + + + + + + + - [group name] + [group name] [description] diff --git a/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java b/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java index c3f07099d..1bf3efec9 100644 --- a/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java +++ b/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java @@ -28,6 +28,7 @@ import org.apache.wicket.extensions.markup.html.repeater.data.sort.OrderByBorder; import org.apache.wicket.extensions.markup.html.repeater.util.SortParam; import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider; +import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.link.BookmarkablePageLink; import org.apache.wicket.markup.html.link.Link; @@ -43,6 +44,7 @@ import com.gitblit.Keys; import com.gitblit.models.ProjectModel; 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; @@ -59,6 +61,25 @@ public class RepositoriesPanel extends BasePanel { private static final long serialVersionUID = 1L; + private enum CollapsibleRepositorySetting { + DISABLED, + + EXPANDED, + + COLLAPSED; + + public static CollapsibleRepositorySetting get(String name) { + CollapsibleRepositorySetting returnVal = CollapsibleRepositorySetting.DISABLED; + for (CollapsibleRepositorySetting setting : values()) { + if (setting.name().equalsIgnoreCase(name)) { + returnVal = setting; + break; + } + } + return returnVal; + } + } + public RepositoriesPanel(String wicketId, final boolean showAdmin, final boolean showManagement, List models, boolean enableLinks, final Map accessRestrictionTranslations) { @@ -66,10 +87,12 @@ public RepositoriesPanel(String wicketId, final boolean showAdmin, final boolean final boolean linksActive = enableLinks; final boolean showSize = app().settings().getBoolean(Keys.web.showRepositorySizes, true); + final String collapsibleRespositorySetting = app().settings().getString(Keys.web.collapsibleRepositoryGroups, null); + final CollapsibleRepositorySetting collapsibleRepoGroups = CollapsibleRepositorySetting.get(collapsibleRespositorySetting); final UserModel user = GitBlitWebSession.get().getUser(); - final IDataProvider dp; + IDataProvider dp = null; Fragment managementLinks; if (showAdmin) { @@ -97,7 +120,28 @@ public void onClick() { add (new Label("managementPanel").setVisible(false)); } - if (app().settings().getString(Keys.web.repositoryListType, "flat").equalsIgnoreCase("grouped")) { + if (app().settings().getString(Keys.web.repositoryListType, "flat").equalsIgnoreCase("tree")) { + TreeNodeModel tree = new TreeNodeModel(); + for (RepositoryModel model : models) { + String rootPath = StringUtils.getRootPath(model.name); + if (StringUtils.isEmpty(rootPath)) { + tree.add(model); + } else { + // create folder structure + tree.add(rootPath, model); + } + } + + WebMarkupContainer container = new WebMarkupContainer("row"); + add(container); + container.add(new NestedRepositoryTreePanel("rowContent", Model.of(tree), accessRestrictionTranslations, enableLinks)); + + Fragment fragment = new Fragment("headerContent", "groupRepositoryHeader", this); + Fragment allCollapsible = new Fragment("allCollapsible", "tableAllCollapsible", this); + fragment.add(allCollapsible); + add(fragment); + + } else if (app().settings().getString(Keys.web.repositoryListType, "flat").equalsIgnoreCase("grouped")) { List rootRepositories = new ArrayList(); Map> groups = new HashMap>(); for (RepositoryModel model : models) { @@ -140,6 +184,7 @@ public void onClick() { dp = new SortableRepositoriesProvider(models); } + if (dp != null) { final boolean showSwatch = app().settings().getBoolean(Keys.web.repositoryListSwatches, true); DataView dataView = new DataView("row", dp) { @@ -160,6 +205,16 @@ public void populateItem(final Item item) { GroupRepositoryModel groupRow = (GroupRepositoryModel) entry; currGroupName = entry.name; Fragment row = new Fragment("rowContent", "groupRepositoryRow", this); + if(collapsibleRepoGroups == CollapsibleRepositorySetting.EXPANDED) { + Fragment groupCollapsible = new Fragment("groupCollapsible", "tableGroupMinusCollapsible", this); + row.add(groupCollapsible); + } else if(collapsibleRepoGroups == CollapsibleRepositorySetting.COLLAPSED) { + Fragment groupCollapsible = new Fragment("groupCollapsible", "tableGroupPlusCollapsible", this); + row.add(groupCollapsible); + } else { + Fragment groupCollapsible = new Fragment("groupCollapsible", "emptyFragment", this); + row.add(groupCollapsible); + } item.add(row); String name = groupRow.name; @@ -174,7 +229,7 @@ public void populateItem(final Item item) { row.add(new LinkPanel("groupName", null, groupRow.toString(), ProjectPage.class, WicketUtils.newProjectParameter(entry.name))); row.add(new Label("groupDescription", entry.description == null ? "":entry.description)); } - WicketUtils.setCssClass(item, "group"); + WicketUtils.setCssClass(item, "group collapsible"); // reset counter so that first row is light background counter = 0; return; @@ -319,8 +374,17 @@ public void populateItem(final Item item) { } else { // not sortable Fragment fragment = new Fragment("headerContent", "groupRepositoryHeader", this); + if(collapsibleRepoGroups == CollapsibleRepositorySetting.EXPANDED || + collapsibleRepoGroups == CollapsibleRepositorySetting.COLLAPSED) { + Fragment allCollapsible = new Fragment("allCollapsible", "tableAllCollapsible", this); + fragment.add(allCollapsible); + } else { + Fragment allCollapsible = new Fragment("allCollapsible", "emptyFragment", this); + fragment.add(allCollapsible); + } add(fragment); } + } } private static class GroupRepositoryModel extends RepositoryModel { diff --git a/src/main/resources/gitblit/js/collapsible-table.js b/src/main/resources/gitblit/js/collapsible-table.js new file mode 100644 index 000000000..fc28b1e43 --- /dev/null +++ b/src/main/resources/gitblit/js/collapsible-table.js @@ -0,0 +1,71 @@ +$(function() { + $('i.table-group-collapsible') + .click(function(){ + var nodeId = $(this).closest('tr.group.collapsible.tree').data('nodeId'); + if(nodeId!==undefined){ + //we are in tree view + if($(this).hasClass('fa-minus-square-o')){ + $(this).closest('tr.group.collapsible.tree').nextAll('tr.child-of-'+nodeId).hide(); + $(this).closest('tr.group.collapsible.tree').nextAll('tr.child-of-'+nodeId).addClass('hidden-by-'+nodeId); + }else{ + $(this).closest('tr.group.collapsible.tree').nextAll('tr.child-of-'+nodeId).removeClass('hidden-by-'+nodeId); + $(this).closest('tr.group.collapsible.tree').nextAll('tr.child-of-'+nodeId+':not([class*="hidden-by-"])').show(); + } + }else{ + $(this).closest('tr.group.collapsible').nextUntil('tr.group.collapsible').toggle(); + } + $(this).toggleClass('fa-minus-square-o'); + $(this).toggleClass('fa-plus-square-o'); + }); + + + $('i.table-openall-collapsible') + .click(function(){ + $('tr.group.collapsible').first().find('i').addClass('fa-minus-square-o'); + $('tr.group.collapsible').first().find('i').removeClass('fa-plus-square-o'); + $('tr.group.collapsible').first().nextAll('tr:not(tr.group.collapsible),tr.group.collapsible.tree').show(); + $('tr.group.collapsible').first().nextAll('tr.group.collapsible').find('i').addClass('fa-minus-square-o'); + $('tr.group.collapsible').first().nextAll('tr.group.collapsible').find('i').removeClass('fa-plus-square-o'); + + var nodeId = $('tr.group.collapsible.tree').data('nodeId'); + if(nodeId!==undefined){ + //we are in tree view + $('tr[class*="child-of-"]').removeClass(function(index, className){ + return (className.match(/\hidden-by-\S+/g)||[]).join(' '); + }); + $('tr.group.collapsible > i').addClass('fa-minus-square-o'); + $('tr.group.collapsible > i').removeClass('fa-plus-square-o'); + } + }); + + $('i.table-closeall-collapsible') + .click(function(){ + $('tr.group.collapsible').first().find('i').addClass('fa-plus-square-o'); + $('tr.group.collapsible').first().find('i').removeClass('fa-minus-square-o'); + $('tr.group.collapsible').first().nextAll('tr:not(tr.group.collapsible),tr.group.collapsible.tree').hide(); + $('tr.group.collapsible').first().nextAll('tr.group.collapsible').find('i').addClass('fa-plus-square-o'); + $('tr.group.collapsible').first().nextAll('tr.group.collapsible').find('i').removeClass('fa-minus-square-o'); + + var nodeId = $('tr.group.collapsible.tree').first().data('nodeId'); + if(nodeId!==undefined){ + //we are in tree view, hide all sub trees + $('tr[class*="child-of-"]').each(function(){ + var row = $(this); + var classList = row.attr('class').split('/\s+/'); + $.each(classList, function(index, c){ + if(c.match(/^child-of-*/)){ + row.addClass(c.replace(/^child-of-(\d)/, 'hidden-by-$1')); + } + }); + }); + $('tr.group.collapsible i').addClass('fa-plus-square-o'); + $('tr.group.collapsible i').removeClass('fa-minus-square-o'); + } + }); + + $( document ).ready(function() { + if($('tr.group.collapsible').first().find('i').hasClass('fa-plus-square-o')) { + $('tr.group.collapsible').first().nextAll('tr:not(tr.group.collapsible),tr.group.collapsible.tree').hide(); + } + }); +}); \ 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 index 000000000..449688b0f --- /dev/null +++ b/src/test/java/com/gitblit/wicket/panels/TreeNodeModelTest.java @@ -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()); + } + + +}