James Moger
2013-06-19 430496317177893eeb94579b2946dbafea6d0727
Generate filterable project/repository list with FreeMarker
8 files added
9 files modified
922 ■■■■ changed files
.classpath 1 ●●●● patch | view | raw | blame | history
NOTICE 10 ●●●●● patch | view | raw | blame | history
build.moxie 1 ●●●● patch | view | raw | blame | history
gitblit.iml 11 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/freemarker/Freemarker.java 46 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/freemarker/FreemarkerPanel.java 308 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/freemarker/templates/FilterableProjectList.fm 15 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/freemarker/templates/FilterableRepositoryList.fm 19 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/MyDashboardPage.html 83 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/MyDashboardPage.java 85 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/ProjectPage.html 22 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/ProjectPage.java 7 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/FilterableProjectList.html 10 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/FilterableProjectList.java 139 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/FilterableRepositoryList.html 10 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/FilterableRepositoryList.java 154 ●●●●● patch | view | raw | blame | history
src/site/design.mkd 1 ●●●● patch | view | raw | blame | history
.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
@@ -269,4 +269,12 @@
   AngularJS, release under the
   MIT License.
   
   http://angularjs.org/
   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">&nbsp;</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">&nbsp;</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">&nbsp;</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">&nbsp;</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
@@ -51,25 +51,7 @@
        </tr>
    </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">&nbsp;</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>
</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)