.classpath
@@ -40,6 +40,7 @@ <classpathentry kind="lib" path="ext/force-partner-api-24.0.0.jar" sourcepath="ext/src/force-partner-api-24.0.0.jar" /> <classpathentry kind="lib" path="ext/force-wsc-24.0.0.jar" sourcepath="ext/src/force-wsc-24.0.0.jar" /> <classpathentry kind="lib" path="ext/js-1.7R2.jar" sourcepath="ext/src/js-1.7R2.jar" /> <classpathentry kind="lib" path="ext/freemarker-2.3.19.jar" sourcepath="ext/src/freemarker-2.3.19.jar" /> <classpathentry kind="lib" path="ext/junit-4.11.jar" sourcepath="ext/src/junit-4.11.jar" /> <classpathentry kind="lib" path="ext/hamcrest-core-1.3.jar" sourcepath="ext/src/hamcrest-core-1.3.jar" /> <classpathentry kind="lib" path="ext/selenium-java-2.28.0.jar" sourcepath="ext/src/selenium-java-2.28.0.jar" /> NOTICE
@@ -270,3 +270,11 @@ MIT License. http://angularjs.org/ --------------------------------------------------------------------------- FreeMarker --------------------------------------------------------------------------- FreeMarker, release under a modified BSD License. (http://www.freemarker.org/docs/app_license.html) http://www.freemarker.org/ build.moxie
@@ -148,6 +148,7 @@ - compile 'com.toedter:jcalendar:1.3.2' :authority - compile 'org.apache.commons:commons-compress:1.4.1' :war - compile 'com.force.api:force-partner-api:24.0.0' :war - compile 'org.freemarker:freemarker:2.3.19' :war - test 'junit' # Dependencies for Selenium web page testing - test 'org.seleniumhq.selenium:selenium-java:${selenium.version}' @jar gitblit.iml
@@ -413,6 +413,17 @@ </SOURCES> </library> </orderEntry> <orderEntry type="module-library"> <library name="freemarker-2.3.19.jar"> <CLASSES> <root url="jar://$MODULE_DIR$/ext/freemarker-2.3.19.jar!/" /> </CLASSES> <JAVADOC /> <SOURCES> <root url="jar://$MODULE_DIR$/ext/src/freemarker-2.3.19.jar!/" /> </SOURCES> </library> </orderEntry> <orderEntry type="module-library" scope="TEST"> <library name="junit-4.11.jar"> <CLASSES> src/main/java/com/gitblit/wicket/freemarker/Freemarker.java
New file @@ -0,0 +1,46 @@ /* * Copyright 2013 gitblit.com. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.gitblit.wicket.freemarker; import java.io.IOException; import java.io.Writer; import java.util.Map; import freemarker.template.Configuration; import freemarker.template.DefaultObjectWrapper; import freemarker.template.Template; import freemarker.template.TemplateException; public class Freemarker { private static final Configuration fm; static { fm = new Configuration(); fm.setObjectWrapper(new DefaultObjectWrapper()); fm.setOutputEncoding("UTF-8"); fm.setClassForTemplateLoading(Freemarker.class, "templates"); } public static Template getTemplate(String name) throws IOException { return fm.getTemplate(name); } public static void evaluate(Template template, Map<String, Object> values, Writer out) throws TemplateException, IOException { template.process(values, out); } } src/main/java/com/gitblit/wicket/freemarker/FreemarkerPanel.java
New file @@ -0,0 +1,308 @@ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.gitblit.wicket.freemarker; import java.io.IOException; import java.io.StringWriter; import java.util.Map; import org.apache.wicket.MarkupContainer; import org.apache.wicket.WicketRuntimeException; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.IMarkupCacheKeyProvider; import org.apache.wicket.markup.IMarkupResourceStreamProvider; import org.apache.wicket.markup.MarkupStream; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.apache.wicket.util.resource.IResourceStream; import org.apache.wicket.util.resource.StringResourceStream; import org.apache.wicket.util.string.Strings; import com.gitblit.utils.StringUtils; import freemarker.template.Template; import freemarker.template.TemplateException; /** * This class allows FreeMarker to be used as a Wicket preprocessor or as a * snippet injector for something like a CMS. There are some cases where Wicket * is not flexible enough to generate content, especially when you need to generate * hybrid HTML/JS content outside the scope of Wicket. * * @author James Moger * */ @SuppressWarnings("unchecked") public class FreemarkerPanel extends Panel implements IMarkupResourceStreamProvider, IMarkupCacheKeyProvider { private static final long serialVersionUID = 1L; private final String template; private boolean parseGeneratedMarkup; private boolean escapeHtml; private boolean throwFreemarkerExceptions; private transient String stackTraceAsString; private transient String evaluatedTemplate; /** * Construct. * * @param id * Component id * @param template * The Freemarker template * @param values * values map that can be substituted by Freemarker. */ public FreemarkerPanel(final String id, String template, final Map<String, Object> values) { this(id, template, Model.ofMap(values)); } /** * Construct. * * @param id * Component id * @param templateResource * The Freemarker template as a string resource * @param model * Model with variables that can be substituted by Freemarker. */ public FreemarkerPanel(final String id, final String template, final IModel< ? extends Map<String, Object>> model) { super(id, model); this.template = template; } /** * Gets the Freemarker template. * * @return the Freemarker template */ private Template getTemplate() { if (StringUtils.isEmpty(template)) { throw new IllegalArgumentException("Template not specified!"); } try { return Freemarker.getTemplate(template); } catch (IOException e) { onException(e); } return null; } /** * @see org.apache.wicket.markup.html.panel.Panel#onComponentTagBody(org.apache.wicket.markup. * MarkupStream, org.apache.wicket.markup.ComponentTag) */ @Override protected void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag) { if (!Strings.isEmpty(stackTraceAsString)) { // TODO: only display the Freemarker error/stacktrace in development // mode? replaceComponentTagBody(markupStream, openTag, Strings .toMultilineMarkup(stackTraceAsString)); } else if (!parseGeneratedMarkup) { // check that no components have been added in case the generated // markup should not be // parsed if (size() > 0) { throw new WicketRuntimeException( "Components cannot be added if the generated markup should not be parsed."); } if (evaluatedTemplate == null) { // initialize evaluatedTemplate getMarkupResourceStream(null, null); } replaceComponentTagBody(markupStream, openTag, evaluatedTemplate); } else { super.onComponentTagBody(markupStream, openTag); } } /** * Either print or rethrow the throwable. * * @param exception * the cause * @param markupStream * the markup stream * @param openTag * the open tag */ private void onException(final Exception exception) { if (!throwFreemarkerExceptions) { // print the exception on the panel stackTraceAsString = Strings.toString(exception); } else { // rethrow the exception throw new WicketRuntimeException(exception); } } /** * Gets whether to escape HTML characters. * * @return whether to escape HTML characters. The default value is false. */ public void setEscapeHtml(boolean value) { this.escapeHtml = value; } /** * Evaluates the template and returns the result. * * @param templateReader * used to read the template * @return the result of evaluating the velocity template */ private String evaluateFreemarkerTemplate(Template template) { if (evaluatedTemplate == null) { // Get model as a map final Map<String, Object> map = (Map<String, Object>)getDefaultModelObject(); // create a writer for capturing the Velocity output StringWriter writer = new StringWriter(); // string to be used as the template name for log messages in case // of error try { // execute the Freemarker script and capture the output in writer Freemarker.evaluate(template, map, writer); // replace the tag's body the Freemarker output evaluatedTemplate = writer.toString(); if (escapeHtml) { // encode the result in order to get valid html output that // does not break the rest of the page evaluatedTemplate = Strings.escapeMarkup(evaluatedTemplate).toString(); } return evaluatedTemplate; } catch (IOException e) { onException(e); } catch (TemplateException e) { onException(e); } return null; } return evaluatedTemplate; } /** * Gets whether to parse the resulting Wicket markup. * * @return whether to parse the resulting Wicket markup. The default is false. */ public void setParseGeneratedMarkup(boolean value) { this.parseGeneratedMarkup = value; } /** * Whether any Freemarker exception should be trapped and displayed on the panel (false) or thrown * up to be handled by the exception mechanism of Wicket (true). The default is false, which * traps and displays any exception without having consequences for the other components on the * page. * <p> * Trapping these exceptions without disturbing the other components is especially useful in CMS * like applications, where 'normal' users are allowed to do basic scripting. On errors, you * want them to be able to have them correct them while the rest of the application keeps on * working. * </p> * * @return Whether any Freemarker exceptions should be thrown or trapped. The default is false. */ public void setThrowFreemarkerExceptions(boolean value) { this.throwFreemarkerExceptions = value; } /** * @see org.apache.wicket.markup.IMarkupResourceStreamProvider#getMarkupResourceStream(org.apache * .wicket.MarkupContainer, java.lang.Class) */ public final IResourceStream getMarkupResourceStream(MarkupContainer container, Class< ? > containerClass) { Template template = getTemplate(); if (template == null) { throw new WicketRuntimeException("could not find Freemarker template for panel: " + this); } // evaluate the template and return a new StringResourceStream StringBuffer sb = new StringBuffer(); sb.append("<wicket:panel>"); sb.append(evaluateFreemarkerTemplate(template)); sb.append("</wicket:panel>"); return new StringResourceStream(sb.toString()); } /** * @see org.apache.wicket.markup.IMarkupCacheKeyProvider#getCacheKey(org.apache.wicket. * MarkupContainer, java.lang.Class) */ public final String getCacheKey(MarkupContainer container, Class< ? > containerClass) { // don't cache the evaluated template return null; } /** * @see org.apache.wicket.Component#onDetach() */ @Override protected void onDetach() { super.onDetach(); stackTraceAsString = null; evaluatedTemplate = null; } } src/main/java/com/gitblit/wicket/freemarker/templates/FilterableProjectList.fm
New file @@ -0,0 +1,15 @@ <div ng-controller="${ngCtrl}" style="border: 1px solid #ddd;border-radius: 4px;margin-bottom: 20px;"> <div class="header" style="padding: 5px;border: none;"><i class="icon-folder-close"></i> <span wicket:id="${ngList}Title"></span> <div style="padding: 5px 0px 0px;"> <input type="text" ng-model="query.n" class="input-large" wicket:message="placeholder:gb.filter" style="border-radius: 14px; padding: 3px 14px;margin: 0px;"></input> </div> </div> <div ng-repeat="item in ${ngList} | filter:query" style="padding: 3px;border-top: 1px solid #ddd;"> <a href="project/{{item.p}}" title="{{item.i}}"><b>{{item.n}}</b></a> <span class="link hidden-tablet hidden-phone" style="color: #bbb;" title="{{item.d}}">{{item.t}}</span> <span class="pull-right"> <span style="padding: 0px 5px;color: #888;font-weight:bold;vertical-align:middle;" wicket:message="title:gb.repositories">{{item.c | number}}</span> </span> </div> </div> src/main/java/com/gitblit/wicket/freemarker/templates/FilterableRepositoryList.fm
New file @@ -0,0 +1,19 @@ <div ng-controller="${ngCtrl}" style="border: 1px solid #ddd;border-radius: 4px;"> <div class="header" style="padding: 5px;border: none;"><i wicket:id="${ngList}Icon"></i> <span wicket:id="${ngList}Title"></span> <div class="hidden-phone pull-right"> <span wicket:id="${ngList}Button"></span> </div> <div style="padding: 5px 0px 0px;"> <input type="text" ng-model="query.r" class="input-large" wicket:message="placeholder:gb.filter" style="border-radius: 14px; padding: 3px 14px;margin: 0px;"></input> </div> </div> <div ng-repeat="item in ${ngList} | filter:query" style="padding: 3px;border-top: 1px solid #ddd;"> <b><span class="repositorySwatch" style="background-color:{{item.c}};"><span ng-show="item.wc">!</span><span ng-show="!item.wc"> </span></span></b> <a href="summary/?r={{item.r}}" title="{{item.i}}">{{item.p}}<b>{{item.n}}</b></a> <span class="link hidden-tablet hidden-phone" style="color: #bbb;" title="{{item.d}}">{{item.t}}</span> <span ng-show="item.s" class="pull-right"> <span style="padding: 0px 5px;color: #888;font-weight:bold;vertical-align:middle;">{{item.s | number}} <i style="vertical-align:baseline;" class="iconic-star"></i></span> </span> </div> </div> src/main/java/com/gitblit/wicket/pages/MyDashboardPage.html
@@ -29,7 +29,7 @@ <div wicket:id="active">[recently active]</div> </div> <div class="tab-pane" id="projects"> <div wicket:id="projectList">[all projects]</div> <div wicket:id="projects">[all projects]</div> </div> </div> </wicket:fragment> @@ -52,7 +52,7 @@ <div wicket:id="active">[recently active]</div> </div> <div class="tab-pane" id="projects"> <div wicket:id="projectList">[all projects]</div> <div wicket:id="projects">[all projects]</div> </div> </div> </wicket:fragment> @@ -72,85 +72,6 @@ <td><div id="chartAuthors" style="display:inline-block;width: 175px; height: 175px;"></div></td> </tr> </table> </wicket:fragment> <wicket:fragment wicket:id="starredListFragment"> <div ng-controller="starredCtrl" style="border: 1px solid #ddd;border-radius: 4px;margin-bottom: 20px;"> <div class="header" style="padding: 5px;border: none;"><i class="icon-star"></i> <wicket:message key="gb.starredRepositories"></wicket:message> ({{starred.length}}) <div style="padding: 5px 0px 0px;"> <input type="text" ng-model="query.r" class="input-large" wicket:message="placeholder:gb.filter" style="border-radius: 14px; padding: 3px 14px;margin: 0px;"></input> </div> </div> <div ng-repeat="item in starred | filter:query" style="padding: 3px;border-top: 1px solid #ddd;"> <b><span class="repositorySwatch" style="background-color:{{item.c}};"><span ng-show="item.wc">!</span><span ng-show="!item.wc"> </span></span></b> <a href="summary/?r={{item.r}}" title="{{item.i}}">{{item.p}}<b>{{item.n}}</b></a> <span class="link hidden-tablet hidden-phone" style="color: #aaa;" title="{{item.d}}">{{item.t}}</span> <span ng-show="item.s" class="pull-right"> <span style="padding: 0px 5px;color: #888;font-weight:bold;vertical-align:middle;">{{item.s | number}} <i style="vertical-align:baseline;" class="iconic-star"></i></span> </span> </div> </div> </wicket:fragment> <wicket:fragment wicket:id="ownedListFragment"> <div ng-controller="ownedCtrl" style="border: 1px solid #ddd;border-radius: 4px;"> <div class="header" style="padding: 5px;border: none;"><i class="icon-user"></i> <wicket:message key="gb.myRepositories"></wicket:message> ({{owned.length}}) <div class="hidden-phone pull-right"> <span wicket:id="create"></span> </div> <div style="padding: 5px 0px 0px;"> <input type="text" ng-model="query.r" class="input-large" wicket:message="placeholder:gb.filter" style="border-radius: 14px; padding: 3px 14px;margin: 0px;"></input> </div> </div> <div ng-repeat="item in owned | filter:query" style="padding: 3px;border-top: 1px solid #ddd;"> <b><span class="repositorySwatch" style="background-color:{{item.c}};"><span ng-show="item.wc">!</span><span ng-show="!item.wc"> </span></span></b> <a href="summary/?r={{item.r}}" title="{{item.i}}">{{item.p}}<b>{{item.n}}</b></a> <span class="link hidden-tablet hidden-phone" style="color: #bbb;" title="{{item.d}}">{{item.t}}</span> <span ng-show="item.s" class="pull-right"> <span style="padding: 0px 5px;color: #888;font-weight:bold;vertical-align:middle;">{{item.s | number}} <i style="vertical-align:baseline;" class="iconic-star"></i></span> </span> </div> </div> </wicket:fragment> <wicket:fragment wicket:id="activeListFragment"> <div ng-controller="activeCtrl" style="border: 1px solid #ddd;border-radius: 4px;margin-bottom: 20px;"> <div class="header" style="padding: 5px;border: none;"><i class="icon-user"></i> <wicket:message key="gb.activeRepositories"></wicket:message> ({{active.length}}) <div style="padding: 5px 0px 0px;"> <input type="text" ng-model="query.r" class="input-large" wicket:message="placeholder:gb.filter" style="border-radius: 14px; padding: 3px 14px;margin: 0px;"></input> </div> </div> <div ng-repeat="item in active | filter:query" style="padding: 3px;border-top: 1px solid #ddd;"> <b><span class="repositorySwatch" style="background-color:{{item.c}};"><span ng-show="item.wc">!</span><span ng-show="!item.wc"> </span></span></b> <a href="summary/?r={{item.r}}" title="{{item.i}}">{{item.p}}<b>{{item.n}}</b></a> <span class="link hidden-tablet hidden-phone" style="color: #bbb;" title="{{item.d}}">{{item.t}}</span> <span ng-show="item.s" class="pull-right"> <span style="padding: 0px 5px;color: #888;font-weight:bold;vertical-align:middle;">{{item.s | number}} <i style="vertical-align:baseline;" class="iconic-star"></i></span> </span> </div> </div> </wicket:fragment> <wicket:fragment wicket:id="projectListFragment"> <div ng-controller="projectListCtrl" style="border: 1px solid #ddd;border-radius: 4px;margin-bottom: 20px;"> <div class="header" style="padding: 5px;border: none;"><i class="icon-folder-close"></i> <wicket:message key="gb.projects"></wicket:message> ({{projectList.length}}) <div style="padding: 5px 0px 0px;"> <input type="text" ng-model="query.n" class="input-large" wicket:message="placeholder:gb.filter" style="border-radius: 14px; padding: 3px 14px;margin: 0px;"></input> </div> </div> <div ng-repeat="item in projectList | filter:query" style="padding: 3px;border-top: 1px solid #ddd;"> <a href="project/{{item.p}}" title="{{item.i}}"><b>{{item.n}}</b></a> <span class="link hidden-tablet hidden-phone" style="color: #bbb;" title="{{item.d}}">{{item.t}}</span> <span class="pull-right"> <span style="padding: 0px 5px;color: #888;font-weight:bold;vertical-align:middle;" wicket:message="title:gb.repositories">{{item.c | number}}</span> </span> </div> </div> </wicket:fragment> </wicket:extend> src/main/java/com/gitblit/wicket/pages/MyDashboardPage.java
@@ -19,10 +19,7 @@ import java.io.FileInputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Serializable; import java.text.DateFormat; import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; @@ -34,7 +31,6 @@ import org.apache.wicket.Component; import org.apache.wicket.PageParameters; import org.apache.wicket.behavior.HeaderContributor; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.panel.Fragment; import org.eclipse.jgit.lib.Constants; @@ -49,8 +45,8 @@ import com.gitblit.utils.StringUtils; import com.gitblit.wicket.GitBlitWebSession; import com.gitblit.wicket.WicketUtils; import com.gitblit.wicket.ng.NgController; import com.gitblit.wicket.panels.LinkPanel; import com.gitblit.wicket.panels.FilterableProjectList; import com.gitblit.wicket.panels.FilterableRepositoryList; public class MyDashboardPage extends DashboardPage { @@ -152,38 +148,36 @@ add(repositoryTabs); Fragment projectList = createProjectList(); repositoryTabs.add(projectList); // projects list List<ProjectModel> projects = GitBlit.self().getProjectModels(getRepositoryModels(), false); repositoryTabs.add(new FilterableProjectList("projects", projects)); // active repository list if (active.isEmpty()) { repositoryTabs.add(new Label("active").setVisible(false)); } else { Fragment activeView = createNgList("active", "activeListFragment", "activeCtrl", active); repositoryTabs.add(activeView); FilterableRepositoryList repoList = new FilterableRepositoryList("active", active); repoList.setTitle(getString("gb.activeRepositories"), "icon-time"); repositoryTabs.add(repoList); } // starred repository list if (ArrayUtils.isEmpty(starred)) { repositoryTabs.add(new Label("starred").setVisible(false)); } else { Fragment starredView = createNgList("starred", "starredListFragment", "starredCtrl", starred); repositoryTabs.add(starredView); FilterableRepositoryList repoList = new FilterableRepositoryList("starred", starred); repoList.setTitle(getString("gb.starredRepositories"), "icon-star"); repositoryTabs.add(repoList); } // owned repository list if (ArrayUtils.isEmpty(owned)) { repositoryTabs.add(new Label("owned").setVisible(false)); } else { Fragment ownedView = createNgList("owned", "ownedListFragment", "ownedCtrl", owned); if (user.canCreate) { // create button ownedView.add(new LinkPanel("create", "btn btn-mini", getString("gb.newRepository"), EditRepositoryPage.class)); } else { // no button ownedView.add(new Label("create").setVisible(false)); } repositoryTabs.add(ownedView); FilterableRepositoryList repoList = new FilterableRepositoryList("owned", starred); repoList.setTitle(getString("gb.myRepositories"), "icon-user"); repoList.setAllowCreate(user.canCreate() || user.canAdmin()); repositoryTabs.add(repoList); } } @@ -258,54 +252,5 @@ } } return MessageFormat.format(getString("gb.failedToReadMessage"), file); } protected Fragment createProjectList() { String format = GitBlit.getString(Keys.web.datestampShortFormat, "MM/dd/yy"); final DateFormat df = new SimpleDateFormat(format); df.setTimeZone(getTimeZone()); List<ProjectModel> projects = GitBlit.self().getProjectModels(getRepositoryModels(), false); Collections.sort(projects, new Comparator<ProjectModel>() { @Override public int compare(ProjectModel o1, ProjectModel o2) { return o2.lastChange.compareTo(o1.lastChange); } }); List<ProjectListItem> list = new ArrayList<ProjectListItem>(); for (ProjectModel proj : projects) { if (proj.isUserProject() || proj.repositories.isEmpty()) { // exclude user projects from list continue; } ProjectListItem item = new ProjectListItem(); item.p = proj.name; item.n = StringUtils.isEmpty(proj.title) ? proj.name : proj.title; item.i = proj.description; item.t = getTimeUtils().timeAgo(proj.lastChange); item.d = df.format(proj.lastChange); item.c = proj.repositories.size(); list.add(item); } // inject an AngularJS controller with static data NgController ctrl = new NgController("projectListCtrl"); ctrl.addVariable("projectList", list); add(new HeaderContributor(ctrl)); Fragment fragment = new Fragment("projectList", "projectListFragment", this); return fragment; } protected class ProjectListItem implements Serializable { private static final long serialVersionUID = 1L; String p; // path String n; // name String t; // time ago String d; // last updated String i; // information/description long c; // repository count } } src/main/java/com/gitblit/wicket/pages/ProjectPage.html
@@ -52,24 +52,6 @@ </table> </wicket:fragment> <wicket:fragment wicket:id="repositoryListFragment"> <div ng-controller="repositoryListCtrl" style="border: 1px solid #ddd;border-radius: 4px;margin-bottom: 20px;"> <div class="header" style="padding: 5px;border: none;"><img style="vertical-align: middle;" src="git-black-16x16.png"/> <wicket:message key="gb.repositories"></wicket:message> ({{repositoryList.length}}) <div style="padding: 5px 0px 0px;"> <input type="text" ng-model="query.r" class="input-large" wicket:message="placeholder:gb.filter" style="border-radius: 14px; padding: 3px 14px;margin: 0px;"></input> </div> </div> <div ng-repeat="item in repositoryList | filter:query" style="padding: 3px;border-top: 1px solid #ddd;"> <b><span class="repositorySwatch" style="background-color:{{item.c}};"><span ng-show="item.wc">!</span><span ng-show="!item.wc"> </span></span></b> <a href="summary/?r={{item.r}}" title="{{item.i}}">{{item.p}}<b>{{item.n}}</b></a> <span class="link hidden-tablet hidden-phone" style="color: #bbb;" title="{{item.d}}">{{item.t}}</span> <span ng-show="item.s" class="pull-right"> <span style="padding: 0px 5px;color: #888;font-weight:bold;vertical-align:middle;">{{item.s | number}} <i style="vertical-align:baseline;" class="iconic-star"></i></span> </span> </div> </div> </wicket:fragment> </wicket:extend> </body> </html> src/main/java/com/gitblit/wicket/pages/ProjectPage.java
@@ -24,7 +24,6 @@ import org.apache.wicket.PageParameters; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.link.ExternalLink; import org.apache.wicket.markup.html.panel.Fragment; import com.gitblit.GitBlit; import com.gitblit.Keys; @@ -41,6 +40,7 @@ import com.gitblit.wicket.PageRegistration.DropDownMenuItem; import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration; import com.gitblit.wicket.WicketUtils; import com.gitblit.wicket.panels.FilterableRepositoryList; public class ProjectPage extends DashboardPage { @@ -128,8 +128,9 @@ if (repositories.isEmpty()) { add(new Label("repositoryList").setVisible(false)); } else { Fragment activeView = createNgList("repositoryList", "repositoryListFragment", "repositoryListCtrl", repositories); add(activeView); FilterableRepositoryList repoList = new FilterableRepositoryList("repositoryList", repositories); repoList.setAllowCreate(user.canCreate(project.name + "/")); add(repoList); } } src/main/java/com/gitblit/wicket/panels/FilterableProjectList.html
New file @@ -0,0 +1,10 @@ <!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"> <wicket:panel> <div wicket:id="listComponent">[component]</div> </wicket:panel> </html> src/main/java/com/gitblit/wicket/panels/FilterableProjectList.java
New file @@ -0,0 +1,139 @@ /* * Copyright 2013 gitblit.com. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.gitblit.wicket.panels; import java.io.Serializable; import java.text.DateFormat; import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.wicket.behavior.HeaderContributor; import org.apache.wicket.markup.html.basic.Label; import com.gitblit.GitBlit; import com.gitblit.Keys; import com.gitblit.models.ProjectModel; import com.gitblit.utils.StringUtils; import com.gitblit.wicket.WicketUtils; import com.gitblit.wicket.freemarker.FreemarkerPanel; import com.gitblit.wicket.ng.NgController; /** * A client-side filterable rich project list which uses Freemarker, Wicket, * and AngularJS. * * @author James Moger * */ public class FilterableProjectList extends BasePanel { private static final long serialVersionUID = 1L; private final List<ProjectModel> projects; private String title; private String iconClass; public FilterableProjectList(String id, List<ProjectModel> projects) { super(id); this.projects = projects; } public void setTitle(String title, String iconClass) { this.title = title; this.iconClass = iconClass; } @Override protected void onInitialize() { super.onInitialize(); String id = getId(); String ngCtrl = id + "Ctrl"; String ngList = id + "List"; Map<String, Object> values = new HashMap<String, Object>(); values.put("ngCtrl", ngCtrl); values.put("ngList", ngList); // use Freemarker to setup an AngularJS/Wicket html snippet FreemarkerPanel panel = new FreemarkerPanel("listComponent", "FilterableProjectList.fm", values); panel.setParseGeneratedMarkup(true); panel.setRenderBodyOnly(true); add(panel); // add the Wicket controls that are referenced in the snippet String listTitle = StringUtils.isEmpty(title) ? getString("gb.projects") : title; panel.add(new Label(ngList + "Title", MessageFormat.format("{0} ({1})", listTitle, projects.size()))); if (StringUtils.isEmpty(iconClass)) { panel.add(new Label(ngList + "Icon").setVisible(false)); } else { Label icon = new Label(ngList + "Icon"); WicketUtils.setCssClass(icon, iconClass); panel.add(icon); } String format = GitBlit.getString(Keys.web.datestampShortFormat, "MM/dd/yy"); final DateFormat df = new SimpleDateFormat(format); df.setTimeZone(getTimeZone()); Collections.sort(projects, new Comparator<ProjectModel>() { @Override public int compare(ProjectModel o1, ProjectModel o2) { return o2.lastChange.compareTo(o1.lastChange); } }); List<ProjectListItem> list = new ArrayList<ProjectListItem>(); for (ProjectModel proj : projects) { if (proj.isUserProject() || proj.repositories.isEmpty()) { // exclude user projects from list continue; } ProjectListItem item = new ProjectListItem(); item.p = proj.name; item.n = StringUtils.isEmpty(proj.title) ? proj.name : proj.title; item.i = proj.description; item.t = getTimeUtils().timeAgo(proj.lastChange); item.d = df.format(proj.lastChange); item.c = proj.repositories.size(); list.add(item); } // inject an AngularJS controller with static data NgController ctrl = new NgController(ngCtrl); ctrl.addVariable(ngList, list); add(new HeaderContributor(ctrl)); } protected class ProjectListItem implements Serializable { private static final long serialVersionUID = 1L; String p; // path String n; // name String t; // time ago String d; // last updated String i; // information/description long c; // repository count } } src/main/java/com/gitblit/wicket/panels/FilterableRepositoryList.html
New file @@ -0,0 +1,10 @@ <!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"> <wicket:panel> <div wicket:id="listComponent">[component]</div> </wicket:panel> </html> src/main/java/com/gitblit/wicket/panels/FilterableRepositoryList.java
New file @@ -0,0 +1,154 @@ /* * Copyright 2013 gitblit.com. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.gitblit.wicket.panels; import java.io.Serializable; import java.text.DateFormat; import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.wicket.behavior.HeaderContributor; import org.apache.wicket.markup.html.basic.Label; import com.gitblit.GitBlit; import com.gitblit.Keys; import com.gitblit.models.RepositoryModel; import com.gitblit.utils.StringUtils; import com.gitblit.wicket.WicketUtils; import com.gitblit.wicket.freemarker.FreemarkerPanel; import com.gitblit.wicket.ng.NgController; import com.gitblit.wicket.pages.EditRepositoryPage; /** * A client-side filterable rich repository list which uses Freemarker, Wicket, * and AngularJS. * * @author James Moger * */ public class FilterableRepositoryList extends BasePanel { private static final long serialVersionUID = 1L; private final List<RepositoryModel> repositories; private String title; private String iconClass; private boolean allowCreate; public FilterableRepositoryList(String id, List<RepositoryModel> repositories) { super(id); this.repositories = repositories; } public void setTitle(String title, String iconClass) { this.title = title; this.iconClass = iconClass; } public void setAllowCreate(boolean value) { this.allowCreate = value; } @Override protected void onInitialize() { super.onInitialize(); String id = getId(); String ngCtrl = id + "Ctrl"; String ngList = id + "List"; Map<String, Object> values = new HashMap<String, Object>(); values.put("ngCtrl", ngCtrl); values.put("ngList", ngList); // use Freemarker to setup an AngularJS/Wicket html snippet FreemarkerPanel panel = new FreemarkerPanel("listComponent", "FilterableRepositoryList.fm", values); panel.setParseGeneratedMarkup(true); panel.setRenderBodyOnly(true); add(panel); // add the Wicket controls that are referenced in the snippet String listTitle = StringUtils.isEmpty(title) ? getString("gb.repositories") : title; panel.add(new Label(ngList + "Title", MessageFormat.format("{0} ({1})", listTitle, repositories.size()))); if (StringUtils.isEmpty(iconClass)) { panel.add(new Label(ngList + "Icon").setVisible(false)); } else { Label icon = new Label(ngList + "Icon"); WicketUtils.setCssClass(icon, iconClass); panel.add(icon); } if (allowCreate) { panel.add(new LinkPanel(ngList + "Button", "btn btn-mini", getString("gb.newRepository"), EditRepositoryPage.class)); } else { panel.add(new Label(ngList + "Button").setVisible(false)); } String format = GitBlit.getString(Keys.web.datestampShortFormat, "MM/dd/yy"); final DateFormat df = new SimpleDateFormat(format); df.setTimeZone(getTimeZone()); // prepare the simplified repository models list List<RepoListItem> list = new ArrayList<RepoListItem>(); for (RepositoryModel repo : repositories) { String name = StringUtils.stripDotGit(repo.name); String path = ""; if (name.indexOf('/') > -1) { path = name.substring(0, name.lastIndexOf('/') + 1); name = name.substring(name.lastIndexOf('/') + 1); } RepoListItem item = new RepoListItem(); item.n = name; item.p = path; item.r = repo.name; item.i = repo.description; item.s = GitBlit.self().getStarCount(repo); item.t = getTimeUtils().timeAgo(repo.lastChange); item.d = df.format(repo.lastChange); item.c = StringUtils.getColor(StringUtils.stripDotGit(repo.name)); item.wc = repo.isBare ? 0 : 1; list.add(item); } // inject an AngularJS controller with static data NgController ctrl = new NgController(ngCtrl); ctrl.addVariable(ngList, list); add(new HeaderContributor(ctrl)); } protected class RepoListItem implements Serializable { private static final long serialVersionUID = 1L; String r; // repository String n; // name String p; // project/path String t; // time ago String d; // last updated String i; // information/description long s; // stars String c; // html color int wc; // working copy: 1 = true, 0 = false } } src/site/design.mkd
@@ -47,6 +47,7 @@ - [JCalendar](http://www.toedter.com/en/jcalendar) (LGPL 2.1) - [Commons-Compress](http://commons.apache.org/compress) (Apache 2.0) - [XZ for Java](http://tukaani.org/xz/java.html) (Public Domain) - [FreeMarker](http://www.freemarker.org) (modified BSD) ### Other Build Dependencies - [Fancybox image viewer](http://fancybox.net) (MIT and GPL dual-licensed)