diff options
author | Florian Zschocke <zschocke@gmx.de> | 2019-06-15 14:34:29 +0200 |
---|---|---|
committer | Florian Zschocke <zschocke@gmx.de> | 2019-06-15 14:50:50 +0200 |
commit | 0bace7e0323ed7f4c7e773f647f6d73f563dea38 (patch) | |
tree | df45d95ca490717c4c4157f326f6e7abfd3a550a /src | |
parent | 150ff287d220b93092e689c887a64b00a21a2693 (diff) | |
download | gitblit-0bace7e0323ed7f4c7e773f647f6d73f563dea38.tar.gz gitblit-0bace7e0323ed7f4c7e773f647f6d73f563dea38.zip |
Add support nested groups on the Repositories page
Fix for #725.
Also covers #527.
This is a squashed commit of the following commits,
merging and closing pull request #1267:
commit 55fee41769ffab1aff59344fe117d481687aa743
Author: Martin Spielmann <mail@martinspielmann.de>
Date: Mon Nov 6 17:19:53 2017 +0100
declared local variable final to fix travis build
commit 131e4d14a48c2a3fdce621fa54637de50684d040
Author: Martin Spielmann <mail@martinspielmann.de>
Date: Mon Nov 6 14:11:55 2017 +0100
fix formatting (use tab for identation)
commit 8da5f6d5967894f157251c320928acdab3a451e7
Author: Martin Spielmann <mail@martinspielmann.de>
Date: Mon Nov 6 13:45:39 2017 +0100
Add repositoryListType tree. Addresses #725, 527 and includes #1224
commit 6c061651fb95212ae242dbca06c8d9ef80146201
Merge: f365daa3 40ee9653
Author: Martin Spielmann <mail@martinspielmann.de>
Date: Sat Nov 4 13:19:08 2017 +0100
Merge remote-tracking branch 'collapsible/ticket/527' into 725_nested_repos
commit f365daa3b1d6be135365f9b11bdece320beabf4e
Author: Martin Spielmann <mail@martinspielmann.de>
Date: Sat Nov 4 13:10:24 2017 +0100
first working version of tree model
Diffstat (limited to 'src')
7 files changed, 642 insertions, 13 deletions
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 00000000..a69393e2 --- /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<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 index 00000000..5f2aba21 --- /dev/null +++ b/src/main/java/com/gitblit/wicket/panels/NestedRepositoryTreePanel.html @@ -0,0 +1,106 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd" + xml:lang="en" lang="en"> + +<body> + <wicket:panel> + <tr style="background-color: #bbb" wicket:id="nodeHeader" data-row-type="folder"></tr> + <tr wicket:id="subFolders"> + <span wicket:id="rowContent"></span> + </tr> + <wicket:container wicket:id="repositories"> + <tr wicket:id="rowContent" data-row-type="repo"> + <td wicket:id="firstColumn" class="left" + style="padding-left: 3px;"> + <div style="border-left: 1px solid black; margin-left:6px; width: 19px;display: inline-block;float: left;" + wicket:id="depth"> </div> + <span wicket:id="repoIcon"></span><span + style="padding-left: 3px;" wicket:id="repositoryName">[repository + name]</span> + </td> + <td class="hidden-phone"><span class="list" + wicket:id="repositoryDescription">[repository description]</span></td> + <td class="hidden-tablet hidden-phone author"><span + wicket:id="repositoryOwner">[repository owner]</span></td> + <td class="hidden-phone" + style="text-align: right; padding-right: 10px;"><img + class="inlineIcon" wicket:id="sparkleshareIcon" /><img + class="inlineIcon" wicket:id="frozenIcon" /><img class="inlineIcon" + wicket:id="federatedIcon" /><img class="inlineIcon" + wicket:id="accessRestrictionIcon" /></td> + <td><span wicket:id="repositoryLastChange">[last change]</span></td> + <td class="rightAlign hidden-phone" + style="text-align: right; padding-right: 15px;"><span + style="font-size: 0.8em;" wicket:id="repositorySize">[repository + size]</span></td> + </tr> + </wicket:container> + + <wicket:fragment wicket:id="emptyFragment"> + </wicket:fragment> + + <wicket:fragment wicket:id="repoIconFragment"> + <span class="octicon octicon-centered octicon-repo"></span> + </wicket:fragment> + + <wicket:fragment wicket:id="mirrorIconFragment"> + <span class="octicon octicon-centered octicon-mirror"></span> + </wicket:fragment> + + <wicket:fragment wicket:id="forkIconFragment"> + <span class="octicon octicon-centered octicon-repo-forked"></span> + </wicket:fragment> + + <wicket:fragment wicket:id="cloneIconFragment"> + <span class="octicon octicon-centered octicon-repo-push" + wicket:message="title:gb.workingCopyWarning"></span> + </wicket:fragment> + + <wicket:fragment wicket:id="tableGroupMinusCollapsible"> + <i title="Click to expand/collapse" class="fa fa-minus-square-o table-group-collapsible" aria-hidden="true" style="padding-right:3px;cursor:pointer;"></i> + </wicket:fragment> + + <wicket:fragment wicket:id="tableGroupPlusCollapsible"> + <i title="Click to expand/collapse" class="fa fa-plus-square-o table-group-collapsible" aria-hidden="true" style="padding-right:3px;cursor:pointer;"></i> + </wicket:fragment> + + <wicket:fragment wicket:id="tableAllCollapsible"> + <i title="Click to expand all" + class="fa fa-plus-square-o table-openall-collapsible" + aria-hidden="true" style="padding-right: 3px; cursor: pointer;"></i> + <i title="Click to collapse all" + class="fa fa-minus-square-o table-closeall-collapsible" + aria-hidden="true" style="padding-right: 3px; cursor: pointer;"></i> + </wicket:fragment> + + <wicket:fragment wicket:id="groupRepositoryHeader"> + <tr> + <th class="left"><span wicket:id="allCollapsible"></span> <img + style="vertical-align: middle;" src="git-black-16x16.png" /> <wicket:message + key="gb.repository">Repository</wicket:message></th> + <th class="hidden-phone"><span><wicket:message + key="gb.description">Description</wicket:message></span></th> + <th class="hidden-tablet hidden-phone"><span><wicket:message + key="gb.owner">Owner</wicket:message></span></th> + <th class="hidden-phone"></th> + <th><wicket:message key="gb.lastChange">Last Change</wicket:message></th> + <th class="right hidden-phone"></th> + </tr> + </wicket:fragment> + + <wicket:fragment wicket:id="groupRepositoryRow"> + <td wicket:id="firstColumn" style="" colspan="1"> + <div style="border-left: 1px solid black; margin-left:6px; width: 19px; display: inline-block;float: left;" + wicket:id="depth"> </div> + <span + wicket:id="groupCollapsible"></span><span wicket:id="groupName">[group + name]</span></td> + <td colspan="6" style="padding: 2px;"><span class="hidden-phone" + style="font-weight: normal; color: #666;" + wicket:id="groupDescription">[description]</span></td> + </wicket:fragment> + + </wicket:panel> +</body> +</html> diff --git a/src/main/java/com/gitblit/wicket/panels/NestedRepositoryTreePanel.java b/src/main/java/com/gitblit/wicket/panels/NestedRepositoryTreePanel.java new file mode 100644 index 00000000..fbe1991d --- /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<TreeNodeModel> model, final Map<AccessRestrictionType, String> accessRestrictionTranslations, final boolean linksActive) { + super(wicketId); + + final boolean showSize = app().settings().getBoolean(Keys.web.showRepositorySizes, true); + final boolean showSwatch = app().settings().getBoolean(Keys.web.repositoryListSwatches, true); + + final TreeNodeModel node = model.getObject(); + Fragment nodeHeader = new Fragment("nodeHeader", "groupRepositoryRow", this); + add(nodeHeader); + WebMarkupContainer firstColumn = new WebMarkupContainer("firstColumn"); + nodeHeader.add(firstColumn); + RepeatingView depth = new RepeatingView("depth"); + for(int i=0; i<node.getDepth();i++) { + depth.add(new WebMarkupContainer(depth.newChildId())); + } + firstColumn.add(depth); + firstColumn.add(new Fragment("groupCollapsible", "tableGroupMinusCollapsible", this)); + if(node.getParent()!=null) { + addChildOfNodeIdCssClassesToRow(nodeHeader, node.getParent()); + } + nodeHeader.add(new AttributeAppender("data-node-id", Model.of(node.hashCode()), " ")); + + String name = node.getName(); + if (name.startsWith(ModelUtils.getUserRepoPrefix())) { + // user page + String username = ModelUtils.getUserNameFromRepoPath(name); + UserModel user = app().users().getUserModel(username); + firstColumn.add(new LinkPanel("groupName", null, (user == null ? username : user.getDisplayName()), UserPage.class, WicketUtils.newUsernameParameter(username))); + nodeHeader.add(new Label("groupDescription", getString("gb.personalRepositories"))); + } else { + // project page + firstColumn.add(new LinkPanel("groupName", null, name, ProjectPage.class, WicketUtils.newProjectParameter(name))); + nodeHeader.add(new Label("groupDescription", "")); + } + WicketUtils.addCssClass(nodeHeader, "group collapsible tree"); + + add(new ListView<TreeNodeModel>("subFolders", node.getSubFolders()) { + private static final long serialVersionUID = 1L; + + @Override + protected void populateItem(ListItem<TreeNodeModel> item) { + item.add(new NestedRepositoryTreePanel("rowContent", item.getModel(), accessRestrictionTranslations, linksActive)); + } + + @Override + public boolean isVisible() { + return super.isVisible() && !node.getSubFolders().isEmpty(); + } + }); + + add(new ListView<RepositoryModel>("repositories", node.getRepositories()) { + private static final long serialVersionUID = 1L; + + int counter = 0; + + @Override + public boolean isVisible() { + return super.isVisible() && !node.getRepositories().isEmpty(); + } + + @Override + protected void populateItem(ListItem<RepositoryModel> item) { + + RepositoryModel entry = item.getModelObject(); + WebMarkupContainer rowContent = new WebMarkupContainer("rowContent"); + item.add(rowContent); + addChildOfNodeIdCssClassesToRow(rowContent, node); + WebMarkupContainer firstColumn = new WebMarkupContainer("firstColumn"); + rowContent.add(firstColumn); + RepeatingView depth = new RepeatingView("depth"); + for(int i=0; i<node.getDepth();i++) { + depth.add(new WebMarkupContainer(depth.newChildId())); + } + firstColumn.add(depth); + + // show colored repository type icon + Fragment iconFragment; + if (entry.isMirror) { + iconFragment = new Fragment("repoIcon", "mirrorIconFragment", this); + } else if (entry.isFork()) { + iconFragment = new Fragment("repoIcon", "forkIconFragment", this); + } else if (entry.isBare) { + iconFragment = new Fragment("repoIcon", "repoIconFragment", this); + } else { + iconFragment = new Fragment("repoIcon", "cloneIconFragment", this); + } + if (showSwatch) { + WicketUtils.setCssStyle(iconFragment, "color:" + StringUtils.getColor(entry.toString())); + } + firstColumn.add(iconFragment); + + // try to strip group name for less cluttered list + String repoName = StringUtils.getLastPathElement(entry.toString()); + + if (linksActive) { + Class<? extends BasePage> linkPage = SummaryPage.class; + PageParameters pp = WicketUtils.newRepositoryParameter(entry.name); + firstColumn.add(new LinkPanel("repositoryName", "list", repoName, linkPage, pp)); + rowContent.add(new LinkPanel("repositoryDescription", "list", entry.description, linkPage, pp)); + } else { + // no links like on a federation page + firstColumn.add(new Label("repositoryName", repoName)); + rowContent.add(new Label("repositoryDescription", entry.description)); + } + if (entry.hasCommits) { + // Existing repository + rowContent.add(new Label("repositorySize", entry.size).setVisible(showSize)); + } else { + // New repository + rowContent.add(new Label("repositorySize", "<span class='empty'>(" + getString("gb.empty") + ")</span>").setEscapeModelStrings(false)); + } + + if (entry.isSparkleshared()) { + rowContent.add(WicketUtils.newImage("sparkleshareIcon", "star_16x16.png", getString("gb.isSparkleshared"))); + } else { + rowContent.add(WicketUtils.newClearPixel("sparkleshareIcon").setVisible(false)); + } + + if (!entry.isMirror && entry.isFrozen) { + rowContent.add(WicketUtils.newImage("frozenIcon", "cold_16x16.png", getString("gb.isFrozen"))); + } else { + rowContent.add(WicketUtils.newClearPixel("frozenIcon").setVisible(false)); + } + + if (entry.isFederated) { + rowContent.add(WicketUtils.newImage("federatedIcon", "federated_16x16.png", getString("gb.isFederated"))); + } else { + rowContent.add(WicketUtils.newClearPixel("federatedIcon").setVisible(false)); + } + + if (entry.isMirror) { + rowContent.add(WicketUtils.newImage("accessRestrictionIcon", "mirror_16x16.png", getString("gb.isMirror"))); + } else { + switch (entry.accessRestriction) { + case NONE: + rowContent.add(WicketUtils.newBlankImage("accessRestrictionIcon")); + break; + case PUSH: + rowContent.add(WicketUtils.newImage("accessRestrictionIcon", "lock_go_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction))); + break; + case CLONE: + rowContent.add(WicketUtils.newImage("accessRestrictionIcon", "lock_pull_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction))); + break; + case VIEW: + rowContent.add(WicketUtils.newImage("accessRestrictionIcon", "shield_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction))); + break; + default: + rowContent.add(WicketUtils.newBlankImage("accessRestrictionIcon")); + } + } + + String owner = ""; + if (!ArrayUtils.isEmpty(entry.owners)) { + // display first owner + for (String username : entry.owners) { + UserModel ownerModel = app().users().getUserModel(username); + if (ownerModel != null) { + owner = ownerModel.getDisplayName(); + break; + } + } + if (entry.owners.size() > 1) { + owner += ", ..."; + } + } + Label ownerLabel = new Label("repositoryOwner", owner); + WicketUtils.setHtmlTooltip(ownerLabel, ArrayUtils.toString(entry.owners)); + rowContent.add(ownerLabel); + + String lastChange; + if (entry.lastChange.getTime() == 0) { + lastChange = "--"; + } else { + lastChange = getTimeUtils().timeAgo(entry.lastChange); + } + Label lastChangeLabel = new Label("repositoryLastChange", lastChange); + rowContent.add(lastChangeLabel); + WicketUtils.setCssClass(lastChangeLabel, getTimeUtils().timeAgoCss(entry.lastChange)); + if (!StringUtils.isEmpty(entry.lastChangeAuthor)) { + WicketUtils.setHtmlTooltip(lastChangeLabel, getString("gb.author") + ": " + entry.lastChangeAuthor); + } + + String clazz = counter % 2 == 0 ? "light" : "dark"; + WicketUtils.addCssClass(rowContent, clazz); + counter++; + } + }); + + } + + private void addChildOfNodeIdCssClassesToRow(Component row, TreeNodeModel parentNode) { + row.add(new AttributeAppender("class", Model.of("child-of-"+ parentNode.hashCode()), " ")); + if(parentNode.getParent() != null) { + addChildOfNodeIdCssClassesToRow(row, parentNode.getParent()); + } + } +} diff --git a/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html b/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html index 7c3b3082..35a26b61 100644 --- a/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html +++ b/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html @@ -119,4 +119,4 @@ </wicket:panel>
</body>
-</html>
\ No newline at end of file +</html>
diff --git a/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java b/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java index aab602e5..982f8b2d 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.PageParameters; 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.Constants.AccessRestrictionType; 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;
@@ -58,7 +60,8 @@ import com.gitblit.wicket.pages.UserPage; public class RepositoriesPanel extends BasePanel {
private static final long serialVersionUID = 1L;
-
+
+
private enum CollapsibleRepositorySetting {
DISABLED,
@@ -90,7 +93,7 @@ public class RepositoriesPanel extends BasePanel { final UserModel user = GitBlitWebSession.get().getUser();
- final IDataProvider<RepositoryModel> dp;
+ IDataProvider<RepositoryModel> dp = null;
Fragment managementLinks;
if (showAdmin) {
@@ -118,7 +121,28 @@ public class RepositoriesPanel extends BasePanel { 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<RepositoryModel> rootRepositories = new ArrayList<RepositoryModel>();
Map<String, List<RepositoryModel>> groups = new HashMap<String, List<RepositoryModel>>();
for (RepositoryModel model : models) {
@@ -161,6 +185,7 @@ public class RepositoriesPanel extends BasePanel { dp = new SortableRepositoriesProvider(models);
}
+ if (dp != null) {
final boolean showSwatch = app().settings().getBoolean(Keys.web.repositoryListSwatches, true);
DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("row", dp) {
@@ -350,7 +375,7 @@ public class RepositoriesPanel extends BasePanel { } else {
// not sortable
Fragment fragment = new Fragment("headerContent", "groupRepositoryHeader", this);
- if(collapsibleRepoGroups == CollapsibleRepositorySetting.EXPANDED ||
+ if(collapsibleRepoGroups == CollapsibleRepositorySetting.EXPANDED ||
collapsibleRepoGroups == CollapsibleRepositorySetting.COLLAPSED) {
Fragment allCollapsible = new Fragment("allCollapsible", "tableAllCollapsible", this);
fragment.add(allCollapsible);
@@ -360,6 +385,7 @@ public class RepositoriesPanel extends BasePanel { }
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 index 538b412e..ca89b8fd 100644 --- a/src/main/resources/gitblit/js/collapsible-table.js +++ b/src/main/resources/gitblit/js/collapsible-table.js @@ -1,32 +1,71 @@ $(function() {
$('i.table-group-collapsible')
.click(function(){
- $(this).closest('tr.group.collapsible').nextUntil('tr.group.collapsible').toggle();
+ 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)').show();
+ $('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)').hide();
+ $('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)').hide();
+ $('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 00000000..449688b0 --- /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()); + } + + +} |