From 430496317177893eeb94579b2946dbafea6d0727 Mon Sep 17 00:00:00 2001 From: James Moger <james.moger@gitblit.com> Date: Wed, 19 Jun 2013 16:26:58 -0400 Subject: [PATCH] Generate filterable project/repository list with FreeMarker --- src/main/java/com/gitblit/wicket/freemarker/FreemarkerPanel.java | 308 +++++++++++++++++++ NOTICE | 10 src/main/java/com/gitblit/wicket/freemarker/Freemarker.java | 46 ++ src/main/java/com/gitblit/wicket/pages/MyDashboardPage.html | 83 ----- .classpath | 1 src/main/java/com/gitblit/wicket/panels/FilterableRepositoryList.java | 154 +++++++++ src/main/java/com/gitblit/wicket/panels/FilterableProjectList.html | 10 src/main/java/com/gitblit/wicket/panels/FilterableRepositoryList.html | 10 src/main/java/com/gitblit/wicket/pages/ProjectPage.html | 22 - src/main/java/com/gitblit/wicket/freemarker/templates/FilterableProjectList.fm | 15 src/main/java/com/gitblit/wicket/pages/MyDashboardPage.java | 85 ---- src/site/design.mkd | 1 src/main/java/com/gitblit/wicket/freemarker/templates/FilterableRepositoryList.fm | 19 + build.moxie | 1 src/main/java/com/gitblit/wicket/pages/ProjectPage.java | 7 src/main/java/com/gitblit/wicket/panels/FilterableProjectList.java | 139 ++++++++ gitblit.iml | 11 17 files changed, 747 insertions(+), 175 deletions(-) diff --git a/.classpath b/.classpath index e25f68c..8cd68de 100644 --- a/.classpath +++ b/.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" /> diff --git a/NOTICE b/NOTICE index 0e23d53..ab0a086 100644 --- a/NOTICE +++ b/NOTICE @@ -269,4 +269,12 @@ AngularJS, release under the MIT License. - http://angularjs.org/ \ No newline at end of file + http://angularjs.org/ + +--------------------------------------------------------------------------- +FreeMarker +--------------------------------------------------------------------------- + FreeMarker, release under a + modified BSD License. (http://www.freemarker.org/docs/app_license.html) + + http://www.freemarker.org/ \ No newline at end of file diff --git a/build.moxie b/build.moxie index be9a21c..9fc08dc 100644 --- a/build.moxie +++ b/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 diff --git a/gitblit.iml b/gitblit.iml index b90adbd..38a014a 100644 --- a/gitblit.iml +++ b/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> diff --git a/src/main/java/com/gitblit/wicket/freemarker/Freemarker.java b/src/main/java/com/gitblit/wicket/freemarker/Freemarker.java new file mode 100644 index 0000000..ad7aa96 --- /dev/null +++ b/src/main/java/com/gitblit/wicket/freemarker/Freemarker.java @@ -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); + } + +} diff --git a/src/main/java/com/gitblit/wicket/freemarker/FreemarkerPanel.java b/src/main/java/com/gitblit/wicket/freemarker/FreemarkerPanel.java new file mode 100644 index 0000000..d57c3a0 --- /dev/null +++ b/src/main/java/com/gitblit/wicket/freemarker/FreemarkerPanel.java @@ -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; + } +} diff --git a/src/main/java/com/gitblit/wicket/freemarker/templates/FilterableProjectList.fm b/src/main/java/com/gitblit/wicket/freemarker/templates/FilterableProjectList.fm new file mode 100644 index 0000000..691f089 --- /dev/null +++ b/src/main/java/com/gitblit/wicket/freemarker/templates/FilterableProjectList.fm @@ -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> \ No newline at end of file diff --git a/src/main/java/com/gitblit/wicket/freemarker/templates/FilterableRepositoryList.fm b/src/main/java/com/gitblit/wicket/freemarker/templates/FilterableRepositoryList.fm new file mode 100644 index 0000000..cf1b7a8 --- /dev/null +++ b/src/main/java/com/gitblit/wicket/freemarker/templates/FilterableRepositoryList.fm @@ -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> \ No newline at end of file diff --git a/src/main/java/com/gitblit/wicket/pages/MyDashboardPage.html b/src/main/java/com/gitblit/wicket/pages/MyDashboardPage.html index a9fd1ba..ce1f028 100644 --- a/src/main/java/com/gitblit/wicket/pages/MyDashboardPage.html +++ b/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> diff --git a/src/main/java/com/gitblit/wicket/pages/MyDashboardPage.java b/src/main/java/com/gitblit/wicket/pages/MyDashboardPage.java index 858821d..f6f9685 100644 --- a/src/main/java/com/gitblit/wicket/pages/MyDashboardPage.java +++ b/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 } } diff --git a/src/main/java/com/gitblit/wicket/pages/ProjectPage.html b/src/main/java/com/gitblit/wicket/pages/ProjectPage.html index 102a49e..32139d0 100644 --- a/src/main/java/com/gitblit/wicket/pages/ProjectPage.html +++ b/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"> </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> \ No newline at end of file diff --git a/src/main/java/com/gitblit/wicket/pages/ProjectPage.java b/src/main/java/com/gitblit/wicket/pages/ProjectPage.java index b101b40..bfc8493 100644 --- a/src/main/java/com/gitblit/wicket/pages/ProjectPage.java +++ b/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); } } diff --git a/src/main/java/com/gitblit/wicket/panels/FilterableProjectList.html b/src/main/java/com/gitblit/wicket/panels/FilterableProjectList.html new file mode 100644 index 0000000..4c3aecd --- /dev/null +++ b/src/main/java/com/gitblit/wicket/panels/FilterableProjectList.html @@ -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> \ No newline at end of file diff --git a/src/main/java/com/gitblit/wicket/panels/FilterableProjectList.java b/src/main/java/com/gitblit/wicket/panels/FilterableProjectList.java new file mode 100644 index 0000000..a5b7413 --- /dev/null +++ b/src/main/java/com/gitblit/wicket/panels/FilterableProjectList.java @@ -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 + } +} \ No newline at end of file diff --git a/src/main/java/com/gitblit/wicket/panels/FilterableRepositoryList.html b/src/main/java/com/gitblit/wicket/panels/FilterableRepositoryList.html new file mode 100644 index 0000000..4c3aecd --- /dev/null +++ b/src/main/java/com/gitblit/wicket/panels/FilterableRepositoryList.html @@ -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> \ No newline at end of file diff --git a/src/main/java/com/gitblit/wicket/panels/FilterableRepositoryList.java b/src/main/java/com/gitblit/wicket/panels/FilterableRepositoryList.java new file mode 100644 index 0000000..6c43b78 --- /dev/null +++ b/src/main/java/com/gitblit/wicket/panels/FilterableRepositoryList.java @@ -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 + } +} \ No newline at end of file diff --git a/src/site/design.mkd b/src/site/design.mkd index 8392a9f..7171197 100644 --- a/src/site/design.mkd +++ b/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) -- Gitblit v1.9.1