From 66e4a930dcd8287b35f256eb13c8df9808efcf55 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Thu, 01 May 2014 19:58:44 -0400
Subject: [PATCH] Merged #15 "My Tickets page"

---
 src/main/java/com/gitblit/wicket/pages/RepositoryPage.java    |    3 
 src/main/java/com/gitblit/wicket/pages/TicketsPage.java       |  316 ----------
 src/main/java/com/gitblit/wicket/panels/TicketSearchForm.java |   78 ++
 src/main/java/com/gitblit/wicket/panels/UserTitlePanel.html   |   16 
 src/main/java/com/gitblit/wicket/GitBlitWebApp.java           |    2 
 src/main/java/com/gitblit/wicket/pages/MyTicketsPage.html     |   84 +++
 src/main/java/com/gitblit/wicket/pages/TicketPage.java        |   21 
 src/main/java/com/gitblit/wicket/GitBlitWebApp.properties     |    4 
 src/main/java/com/gitblit/wicket/pages/TicketsPage.html       |   46 -
 src/main/java/com/gitblit/wicket/panels/TicketListPanel.java  |  243 ++++++++
 src/main/java/com/gitblit/wicket/pages/RootPage.java          |   15 
 /dev/null                                                     |  126 ----
 src/main/java/com/gitblit/wicket/SessionlessForm.java         |   15 
 src/main/java/com/gitblit/wicket/panels/TicketListPanel.html  |   55 +
 src/main/java/com/gitblit/wicket/TicketsUI.java               |  211 +++++++
 src/main/java/com/gitblit/wicket/GitBlitWebApp_fr.properties  |    2 
 src/main/java/com/gitblit/wicket/WicketUtils.java             |    4 
 src/main/java/com/gitblit/wicket/pages/RepositoryPage.html    |    2 
 src/main/java/com/gitblit/wicket/pages/MyTicketsPage.java     |  392 ++++++++++++++
 src/main/java/com/gitblit/wicket/panels/UserTitlePanel.java   |   32 +
 20 files changed, 1,176 insertions(+), 491 deletions(-)

diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.java b/src/main/java/com/gitblit/wicket/GitBlitWebApp.java
index d4c1bc4..9f002d2 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.java
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.java
@@ -81,6 +81,7 @@
 import com.gitblit.wicket.pages.TreePage;
 import com.gitblit.wicket.pages.UserPage;
 import com.gitblit.wicket.pages.UsersPage;
+import com.gitblit.wicket.pages.MyTicketsPage;
 
 public class GitBlitWebApp extends WebApplication {
 
@@ -191,6 +192,7 @@
 		mount("/tickets/export", ExportTicketPage.class, "r", "h");
 		mount("/milestones/new", NewMilestonePage.class, "r");
 		mount("/milestones/edit", EditMilestonePage.class, "r", "h");
+		mount("/mytickets", MyTicketsPage.class, "r", "h");
 
 		// setup the markup document urls
 		mount("/docs", DocsPage.class, "r");
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
index 1394890..0ed2ed5 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
@@ -672,9 +672,11 @@
 gb.mergeToDescription = default integration branch for merging ticket patchsets
 gb.anonymousCanNotPropose = Anonymous users can not propose patchsets.
 gb.youDoNotHaveClonePermission = You are not permitted to clone this repository.
+gb.myTickets = my tickets
+gb.yourAssignedTickets = assigned to you
 gb.newMilestone = new milestone
 gb.editMilestone = edit milestone
 gb.notifyChangedOpenTickets = send notification for changed open tickets
 gb.overdue = overdue
 gb.openMilestones = open milestones
-gb.closedMilestones = closed milestones
\ No newline at end of file
+gb.closedMilestones = closed milestones
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_fr.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_fr.properties
index 8a725cf..a6d6184 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp_fr.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_fr.properties
@@ -670,3 +670,5 @@
 gb.serverDoesNotAcceptPatchsets = Ce serveur n'accepte pas de patchsets.
 gb.ticketIsClosed = Ce ticket est clos.
 gb.mergeToDescription = default integration branch for merging ticket patchsets
+gb.myTickets = mes tickets
+gb.yourAssignedTickets = dont vous �tes responsable
diff --git a/src/main/java/com/gitblit/wicket/SessionlessForm.java b/src/main/java/com/gitblit/wicket/SessionlessForm.java
index d228a2e..6f79071 100644
--- a/src/main/java/com/gitblit/wicket/SessionlessForm.java
+++ b/src/main/java/com/gitblit/wicket/SessionlessForm.java
@@ -22,6 +22,7 @@
 import org.apache.wicket.markup.ComponentTag;
 import org.apache.wicket.markup.MarkupStream;
 import org.apache.wicket.markup.html.form.StatelessForm;
+import org.apache.wicket.protocol.http.RequestUtils;
 import org.apache.wicket.protocol.http.WicketURLDecoder;
 import org.apache.wicket.protocol.http.request.WebRequestCodingStrategy;
 import org.apache.wicket.util.string.AppendingStringBuffer;
@@ -53,9 +54,9 @@
 
 	private static final String HIDDEN_DIV_START = "<div style=\"width:0px;height:0px;position:absolute;left:-100px;top:-100px;overflow:hidden\">";
 
-	private final Class<? extends BasePage> pageClass;
+	protected final Class<? extends BasePage> pageClass;
 
-	private final PageParameters pageParameters;
+	protected final PageParameters pageParameters;
 
 	private final Logger log = LoggerFactory.getLogger(SessionlessForm.class);
 
@@ -145,4 +146,14 @@
 		String un = WicketURLDecoder.QUERY_INSTANCE.decode(s);
 		return Strings.escapeMarkup(un).toString();
 	}
+
+	protected String getAbsoluteUrl() {
+		return getAbsoluteUrl(pageClass, pageParameters);
+	}
+
+	protected String getAbsoluteUrl(Class<? extends BasePage> pageClass, PageParameters pageParameters) {
+		String relativeUrl = urlFor(pageClass, pageParameters).toString();
+		String absoluteUrl = RequestUtils.toAbsolutePath(relativeUrl);
+		return absoluteUrl;
+	}
 }
diff --git a/src/main/java/com/gitblit/wicket/TicketsUI.java b/src/main/java/com/gitblit/wicket/TicketsUI.java
new file mode 100644
index 0000000..a243a7b
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/TicketsUI.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2014 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;
+
+import java.io.Serializable;
+import java.text.MessageFormat;
+
+import org.apache.wicket.markup.html.basic.Label;
+
+import com.gitblit.models.TicketModel;
+import com.gitblit.models.TicketModel.Status;
+import com.gitblit.models.TicketModel.Type;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Common tickets ui methods and classes.
+ *
+ * @author James Moger
+ *
+ */
+public class TicketsUI {
+
+	public static final String [] openStatii = new String [] { Status.New.name().toLowerCase(), Status.Open.name().toLowerCase() };
+
+	public static final String [] closedStatii = new String [] { "!" + Status.New.name().toLowerCase(), "!" + Status.Open.name().toLowerCase() };
+
+	public static Label getStateIcon(String wicketId, TicketModel ticket) {
+		return getStateIcon(wicketId, ticket.type, ticket.status);
+	}
+
+	public static Label getStateIcon(String wicketId, Type type, Status state) {
+		Label label = new Label(wicketId);
+		if (type == null) {
+			type = Type.defaultType;
+		}
+		switch (type) {
+		case Proposal:
+			WicketUtils.setCssClass(label, "fa fa-code-fork");
+			break;
+		case Bug:
+			WicketUtils.setCssClass(label, "fa fa-bug");
+			break;
+		case Enhancement:
+			WicketUtils.setCssClass(label, "fa fa-magic");
+			break;
+		case Question:
+			WicketUtils.setCssClass(label, "fa fa-question");
+			break;
+		default:
+			// standard ticket
+			WicketUtils.setCssClass(label, "fa fa-ticket");
+		}
+		WicketUtils.setHtmlTooltip(label, getTypeState(type, state));
+		return label;
+	}
+
+	public static String getTypeState(Type type, Status state) {
+		return state.toString() + " " + type.toString();
+	}
+
+	public static String getLozengeClass(Status status, boolean subtle) {
+		if (status == null) {
+			status = Status.New;
+		}
+		String css = "";
+		switch (status) {
+		case Declined:
+		case Duplicate:
+		case Invalid:
+		case Wontfix:
+		case Abandoned:
+			css = "aui-lozenge-error";
+			break;
+		case Fixed:
+		case Merged:
+		case Resolved:
+			css = "aui-lozenge-success";
+			break;
+		case New:
+			css = "aui-lozenge-complete";
+			break;
+		case On_Hold:
+			css = "aui-lozenge-current";
+			break;
+		default:
+			css = "";
+			break;
+		}
+
+		return "aui-lozenge" + (subtle ? " aui-lozenge-subtle": "") + (css.isEmpty() ? "" : " ") + css;
+	}
+
+	public static String getStatusClass(Status status) {
+		String css = "";
+		switch (status) {
+		case Declined:
+		case Duplicate:
+		case Invalid:
+		case Wontfix:
+		case Abandoned:
+			css = "resolution-error";
+			break;
+		case Fixed:
+		case Merged:
+		case Resolved:
+			css = "resolution-success";
+			break;
+		case New:
+			css = "resolution-complete";
+			break;
+		case On_Hold:
+			css = "resolution-current";
+			break;
+		default:
+			css = "";
+			break;
+		}
+
+		return "resolution" + (css.isEmpty() ? "" : " ") + css;
+	}
+
+	public static class TicketSort implements Serializable {
+
+		private static final long serialVersionUID = 1L;
+
+		public final String name;
+		public final String sortBy;
+		public final boolean desc;
+
+		public TicketSort(String name, String sortBy, boolean desc) {
+			this.name = name;
+			this.sortBy = sortBy;
+			this.desc = desc;
+		}
+	}
+
+	public static class Indicator implements Serializable {
+
+		private static final long serialVersionUID = 1L;
+
+		public final String css;
+		public final int count;
+		public final String tooltip;
+
+		public Indicator(String css, String tooltip) {
+			this.css = css;
+			this.tooltip = tooltip;
+			this.count = 0;
+		}
+
+		public Indicator(String css, int count, String pattern) {
+			this.css = css;
+			this.count = count;
+			this.tooltip = StringUtils.isEmpty(pattern) ? "" : MessageFormat.format(pattern, count);
+		}
+
+		public String getTooltip() {
+			return tooltip;
+		}
+	}
+
+	public static class TicketQuery implements Serializable, Comparable<TicketQuery> {
+
+		private static final long serialVersionUID = 1L;
+
+		public final String name;
+		public final String query;
+		public String color;
+
+		public TicketQuery(String name, String query) {
+			this.name = name;
+			this.query = query;
+		}
+
+		public TicketQuery color(String value) {
+			this.color = value;
+			return this;
+		}
+
+		@Override
+		public boolean equals(Object o) {
+			if (o instanceof TicketQuery) {
+				return ((TicketQuery) o).query.equals(query);
+			}
+			return false;
+		}
+
+		@Override
+		public int hashCode() {
+			return query.hashCode();
+		}
+
+		@Override
+		public int compareTo(TicketQuery o) {
+			return query.compareTo(o.query);
+		}
+	}
+}
diff --git a/src/main/java/com/gitblit/wicket/WicketUtils.java b/src/main/java/com/gitblit/wicket/WicketUtils.java
index 2a34ca8..10b2146 100644
--- a/src/main/java/com/gitblit/wicket/WicketUtils.java
+++ b/src/main/java/com/gitblit/wicket/WicketUtils.java
@@ -300,7 +300,9 @@
 
 	public static PageParameters newRepositoryParameter(String repositoryName) {
 		Map<String, String> parameterMap = new HashMap<String, String>();
-		parameterMap.put("r", repositoryName);
+		if (!StringUtils.isEmpty(repositoryName)) {
+			parameterMap.put("r", repositoryName);
+		}
 		return new PageParameters(parameterMap);
 	}
 
diff --git a/src/main/java/com/gitblit/wicket/pages/MyTicketsPage.html b/src/main/java/com/gitblit/wicket/pages/MyTicketsPage.html
new file mode 100644
index 0000000..b0bc194
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/MyTicketsPage.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"  
+      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"  
+      xml:lang="en"  
+      lang="en"> 
+
+<body>
+	<wicket:extend>
+		<div class="container">
+			<div class="row" style="padding-top:15px;min-height:500px;" >
+				<div class="tab-pane active" id="tickets">
+					<!-- query controls -->
+					<div class="span3">
+						<div class="hidden-phone">
+							<div wicket:id="userTitlePanel"></div>
+							
+							<!-- search tickets form -->
+							<form class="form-search" style="margin: 10px 0px;" wicket:id="ticketSearchForm">
+								<div class="input-append">
+									<input type="text" class="input-medium search-query" style="border-radius: 14px 0 0 14px; padding-left: 14px;" id="ticketSearchBox" wicket:id="ticketSearchBox" value=""/>
+									<button class="btn" style="border-radius: 0 14px 14px 0px;margin-left:-5px;" type="submit"><i class="icon-search"></i></button>
+								</div>
+							</form>
+							
+							<!--  query list -->						
+							<ul class="nav nav-list">
+								<li class="nav-header"><wicket:message key="gb.queries"></wicket:message></li>
+								<li><a wicket:id="changesQuery"><i class="fa fa-code-fork"></i> <wicket:message key="gb.proposalTickets"></wicket:message></a></li>
+								<li><a wicket:id="bugsQuery"><i class="fa fa-bug"></i> <wicket:message key="gb.bugTickets"></wicket:message></a></li>
+								<li><a wicket:id="enhancementsQuery"><i class="fa fa-magic"></i> <wicket:message key="gb.enhancementTickets"></wicket:message></a></li>
+								<li><a wicket:id="tasksQuery"><i class="fa fa-ticket"></i> <wicket:message key="gb.taskTickets"></wicket:message></a></li>
+								<li><a wicket:id="questionsQuery"><i class="fa fa-question"></i> <wicket:message key="gb.questionTickets"></wicket:message></a></li>
+								<li wicket:id="userDivider" class="divider"></li>
+								<li><a wicket:id="createdQuery"><i class="fa fa-user"></i> <wicket:message key="gb.yourCreatedTickets"></wicket:message></a></li>
+								<li><a wicket:id="responsibleQuery"><i class="fa fa-user"></i> <wicket:message key="gb.yourAssignedTickets"></wicket:message></a></li>
+								<li><a wicket:id="watchedQuery"><i class="fa fa-eye"></i> <wicket:message key="gb.yourWatchedTickets"></wicket:message></a></li>
+								<li><a wicket:id="mentionsQuery"><i class="fa fa-comment"></i> <wicket:message key="gb.mentionsMeTickets"></wicket:message></a></li>
+								<li class="divider"></li>
+								<li><a wicket:id="resetQuery"><i class="fa fa-bolt"></i> <wicket:message key="gb.reset"></wicket:message></a></li>
+							</ul>
+
+						</div>
+					</div>
+					
+					<!-- tickets -->
+					<div class="span9">					
+						<div class="btn-toolbar" style="margin-top: 0px;">
+							<div class="btn-group">
+								<a class="btn dropdown-toggle" data-toggle="dropdown" href="#"><wicket:message key="gb.status"></wicket:message>: <span style="font-weight:bold;" wicket:id="selectedStatii"></span> <span class="caret"></span></a>
+								<ul class="dropdown-menu">
+									<li><a wicket:id="openTickets"><wicket:message key="gb.open"></wicket:message></a></li>
+									<li><a wicket:id="closedTickets"><wicket:message key="gb.closed"></wicket:message></a></li>
+									<li><a wicket:id="allTickets"><wicket:message key="gb.all"></wicket:message></a></li>
+									<li class="divider"></li>
+									<li wicket:id="statii"><span wicket:id="statusLink"></span></li>
+								</ul>
+							</div>
+							<div class="btn-group">
+								<a class="btn dropdown-toggle" data-toggle="dropdown" href="#"><i class="fa fa-sort"></i> <wicket:message key="gb.sort"></wicket:message>: <span style="font-weight:bold;" wicket:id="currentSort"></span> <span class="caret"></span></a>
+								<ul class="dropdown-menu">
+									<li wicket:id="sort"><span wicket:id="sortLink"></span></li>
+								</ul>
+							</div>
+							
+							<div class="btn-group pull-right">
+								<div class="pagination pagination-right pagination-small">
+									<ul>
+										<li><a wicket:id="prevLink"><i class="fa fa-angle-double-left"></i></a></li>
+										<li wicket:id="pageLink"><span wicket:id="page"></span></li>
+										<li><a wicket:id="nextLink"><i class="fa fa-angle-double-right"></i></a></li>
+									</ul>
+								</div>
+							</div>
+						</div>
+						
+						<div wicket:id="ticketList"></div>					
+					</div>
+				</div>
+			</div>
+		</div>
+				
+</wicket:extend>
+</body>
+</html>
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/MyTicketsPage.java b/src/main/java/com/gitblit/wicket/pages/MyTicketsPage.java
new file mode 100644
index 0000000..c207d56
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/MyTicketsPage.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright 2014 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.pages;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+
+import com.gitblit.Keys;
+import com.gitblit.models.TicketModel;
+import com.gitblit.models.TicketModel.Status;
+import com.gitblit.models.UserModel;
+import com.gitblit.tickets.QueryBuilder;
+import com.gitblit.tickets.QueryResult;
+import com.gitblit.tickets.TicketIndexer.Lucene;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.TicketsUI;
+import com.gitblit.wicket.TicketsUI.TicketSort;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.LinkPanel;
+import com.gitblit.wicket.panels.TicketListPanel;
+import com.gitblit.wicket.panels.TicketSearchForm;
+import com.gitblit.wicket.panels.UserTitlePanel;
+
+/**
+ * My Tickets page
+ *
+ * @author Christian Buisson
+ * @author James Moger
+ */
+public class MyTicketsPage extends RootPage {
+
+	public MyTicketsPage() {
+		this(null);
+	}
+
+	public MyTicketsPage(PageParameters params)	{
+		super(params);
+		setupPage("", getString("gb.myTickets"));
+
+		UserModel currentUser = GitBlitWebSession.get().getUser();
+		if (currentUser == null || UserModel.ANONYMOUS.equals(currentUser)) {
+			setRedirect(true);
+			setResponsePage(getApplication().getHomePage());
+			return;
+		}
+
+		final String username = currentUser.getName();
+		final String[] statiiParam = (params == null) ? TicketsUI.openStatii : params.getStringArray(Lucene.status.name());
+		final String assignedToParam = (params == null) ? "" : params.getString(Lucene.responsible.name(), null);
+		final String milestoneParam = (params == null) ? "" : params.getString(Lucene.milestone.name(), null);
+		final String queryParam = (params == null || StringUtils.isEmpty(params.getString("q", null))) ? "watchedby:" + username : params.getString("q", null);
+		final String searchParam = (params == null) ? "" : params.getString("s", null);
+		final String sortBy = (params == null) ? "" : Lucene.fromString(params.getString("sort", Lucene.created.name())).name();
+		final boolean desc = (params == null) ? true : !"asc".equals(params.getString("direction", "desc"));
+
+		// add the user title panel
+		add(new UserTitlePanel("userTitlePanel", currentUser, getString("gb.myTickets")));
+
+		// add search form
+		add(new TicketSearchForm("ticketSearchForm", null, searchParam, getClass(), params));
+
+		// standard queries
+		add(new BookmarkablePageLink<Void>("changesQuery", MyTicketsPage.class,
+				queryParameters(
+						Lucene.type.matches(TicketModel.Type.Proposal.name()),
+						milestoneParam,
+						statiiParam,
+						assignedToParam,
+						sortBy,
+						desc,
+						1)));
+
+		add(new BookmarkablePageLink<Void>("bugsQuery", MyTicketsPage.class,
+				queryParameters(
+						Lucene.type.matches(TicketModel.Type.Bug.name()),
+						milestoneParam,
+						statiiParam,
+						assignedToParam,
+						sortBy,
+						desc,
+						1)));
+
+		add(new BookmarkablePageLink<Void>("enhancementsQuery", MyTicketsPage.class,
+				queryParameters(
+						Lucene.type.matches(TicketModel.Type.Enhancement.name()),
+						milestoneParam,
+						statiiParam,
+						assignedToParam,
+						sortBy,
+						desc,
+						1)));
+
+		add(new BookmarkablePageLink<Void>("tasksQuery", MyTicketsPage.class,
+				queryParameters(
+						Lucene.type.matches(TicketModel.Type.Task.name()),
+						milestoneParam,
+						statiiParam,
+						assignedToParam,
+						sortBy,
+						desc,
+						1)));
+
+		add(new BookmarkablePageLink<Void>("questionsQuery", MyTicketsPage.class,
+				queryParameters(
+						Lucene.type.matches(TicketModel.Type.Question.name()),
+						milestoneParam,
+						statiiParam,
+						assignedToParam,
+						sortBy,
+						desc,
+						1)));
+
+		add(new BookmarkablePageLink<Void>("resetQuery", MyTicketsPage.class,
+				queryParameters(
+						null,
+						milestoneParam,
+						TicketsUI.openStatii,
+						null,
+						null,
+						true,
+						1)));
+
+		add(new Label("userDivider"));
+		add(new BookmarkablePageLink<Void>("createdQuery", MyTicketsPage.class,
+				queryParameters(
+						Lucene.createdby.matches(username),
+						milestoneParam,
+						statiiParam,
+						assignedToParam,
+						sortBy,
+						desc,
+						1)));
+
+		add(new BookmarkablePageLink<Void>("watchedQuery", MyTicketsPage.class,
+				queryParameters(
+						Lucene.watchedby.matches(username),
+						milestoneParam,
+						statiiParam,
+						assignedToParam,
+						sortBy,
+						desc,
+						1)));
+		add(new BookmarkablePageLink<Void>("mentionsQuery", MyTicketsPage.class,
+				queryParameters(
+						Lucene.mentions.matches(username),
+						milestoneParam,
+						statiiParam,
+						assignedToParam,
+						sortBy,
+						desc,
+						1)));
+		add(new BookmarkablePageLink<Void>("responsibleQuery", MyTicketsPage.class,
+				queryParameters(
+						Lucene.responsible.matches(username),
+						milestoneParam,
+						statiiParam,
+						assignedToParam,
+						sortBy,
+						desc,
+						1)));
+
+		// states
+		if (ArrayUtils.isEmpty(statiiParam)) {
+			add(new Label("selectedStatii", getString("gb.all")));
+		} else {
+			add(new Label("selectedStatii", StringUtils.flattenStrings(Arrays.asList(statiiParam), ",")));
+		}
+		add(new BookmarkablePageLink<Void>("openTickets", MyTicketsPage.class, queryParameters(queryParam, milestoneParam, TicketsUI.openStatii, assignedToParam, sortBy, desc, 1)));
+		add(new BookmarkablePageLink<Void>("closedTickets", MyTicketsPage.class, queryParameters(queryParam, milestoneParam, TicketsUI.closedStatii, assignedToParam, sortBy, desc, 1)));
+		add(new BookmarkablePageLink<Void>("allTickets", MyTicketsPage.class, queryParameters(queryParam, milestoneParam, null, assignedToParam, sortBy, desc, 1)));
+
+		// by status
+		List<Status> statii = new ArrayList<Status>(Arrays.asList(Status.values()));
+		statii.remove(Status.Closed);
+		ListDataProvider<Status> resolutionsDp = new ListDataProvider<Status>(statii);
+		DataView<Status> statiiLinks = new DataView<Status>("statii", resolutionsDp) {
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			public void populateItem(final Item<Status> item) {
+				final Status status = item.getModelObject();
+				PageParameters p = queryParameters(queryParam, milestoneParam, new String [] { status.name().toLowerCase() }, assignedToParam, sortBy, desc, 1);
+				String css = TicketsUI.getStatusClass(status);
+				item.add(new LinkPanel("statusLink", css, status.toString(), MyTicketsPage.class, p).setRenderBodyOnly(true));
+			}
+		};
+		add(statiiLinks);
+
+		List<TicketSort> sortChoices = new ArrayList<TicketSort>();
+		sortChoices.add(new TicketSort(getString("gb.sortNewest"), Lucene.created.name(), true));
+		sortChoices.add(new TicketSort(getString("gb.sortOldest"), Lucene.created.name(), false));
+		sortChoices.add(new TicketSort(getString("gb.sortMostRecentlyUpdated"), Lucene.updated.name(), true));
+		sortChoices.add(new TicketSort(getString("gb.sortLeastRecentlyUpdated"), Lucene.updated.name(), false));
+		sortChoices.add(new TicketSort(getString("gb.sortMostComments"), Lucene.comments.name(), true));
+		sortChoices.add(new TicketSort(getString("gb.sortLeastComments"), Lucene.comments.name(), false));
+		sortChoices.add(new TicketSort(getString("gb.sortMostPatchsetRevisions"), Lucene.patchsets.name(), true));
+		sortChoices.add(new TicketSort(getString("gb.sortLeastPatchsetRevisions"), Lucene.patchsets.name(), false));
+		sortChoices.add(new TicketSort(getString("gb.sortMostVotes"), Lucene.votes.name(), true));
+		sortChoices.add(new TicketSort(getString("gb.sortLeastVotes"), Lucene.votes.name(), false));
+
+		TicketSort currentSort = sortChoices.get(0);
+		for (TicketSort ts : sortChoices) {
+			if (ts.sortBy.equals(sortBy) && desc == ts.desc) {
+				currentSort = ts;
+				break;
+			}
+		}
+		add(new Label("currentSort", currentSort.name));
+
+		ListDataProvider<TicketSort> sortChoicesDp = new ListDataProvider<TicketSort>(sortChoices);
+		DataView<TicketSort> sortMenu = new DataView<TicketSort>("sort", sortChoicesDp) {
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			public void populateItem(final Item<TicketSort> item) {
+				final TicketSort ts = item.getModelObject();
+				PageParameters params = queryParameters(queryParam, milestoneParam, statiiParam, assignedToParam, ts.sortBy, ts.desc, 1);
+				item.add(new LinkPanel("sortLink", null, ts.name, MyTicketsPage.class, params).setRenderBodyOnly(true));
+			}
+		};
+		add(sortMenu);
+
+		// Build Query here
+		QueryBuilder qb = new QueryBuilder(queryParam);
+		if (!qb.containsField(Lucene.status.name()) && !ArrayUtils.isEmpty(statiiParam)) {
+			// specify the states
+			boolean not = false;
+			QueryBuilder q = new QueryBuilder();
+			for (String state : statiiParam) {
+				if (state.charAt(0) == '!') {
+					not = true;
+					q.and(Lucene.status.doesNotMatch(state.substring(1)));
+				} else {
+					q.or(Lucene.status.matches(state));
+				}
+			}
+			if (not) {
+				qb.and(q.toString());
+			} else {
+				qb.and(q.toSubquery().toString());
+			}
+		}
+
+		final String luceneQuery;
+		if (qb.containsField(Lucene.createdby.name())
+				|| qb.containsField(Lucene.responsible.name())
+				|| qb.containsField(Lucene.watchedby.name())) {
+			// focused "my tickets" query
+			luceneQuery = qb.build();
+		} else {
+			// general "my tickets" query
+			QueryBuilder myQuery = new QueryBuilder();
+			myQuery.or(Lucene.createdby.matches(username));
+			myQuery.or(Lucene.responsible.matches(username));
+			myQuery.or(Lucene.watchedby.matches(username));
+			myQuery.and(qb.toSubquery().toString());
+			luceneQuery = myQuery.build();
+		}
+
+		// paging links
+		int page = (params != null) ? Math.max(1, WicketUtils.getPage(params)) : 1;
+		int pageSize = app().settings().getInteger(Keys.tickets.perPage, 25);
+
+		List<QueryResult> results;
+		if(StringUtils.isEmpty(searchParam)) {
+			results = app().tickets().queryFor(luceneQuery, page, pageSize, sortBy, desc);
+		} else {
+			results = app().tickets().searchFor(null, searchParam, page, pageSize);
+		}
+
+		int totalResults = results.size() == 0 ? 0 : results.get(0).totalResults;
+		buildPager(queryParam, milestoneParam, statiiParam, assignedToParam, sortBy, desc, page, pageSize, results.size(), totalResults);
+
+		final boolean showSwatch = app().settings().getBoolean(Keys.web.repositoryListSwatches, true);
+		add(new TicketListPanel("ticketList", results, showSwatch, true));
+	}
+
+	protected PageParameters queryParameters(
+			String query,
+			String milestone,
+			String[] states,
+			String assignedTo,
+			String sort,
+			boolean descending,
+			int page) {
+
+		PageParameters params = WicketUtils.newRepositoryParameter("");
+		if (!StringUtils.isEmpty(query)) {
+			params.add("q", query);
+		}
+		if (!StringUtils.isEmpty(milestone)) {
+			params.add(Lucene.milestone.name(), milestone);
+		}
+		if (!ArrayUtils.isEmpty(states)) {
+			for (String state : states) {
+				params.add(Lucene.status.name(), state);
+			}
+		}
+		if (!StringUtils.isEmpty(assignedTo)) {
+			params.add(Lucene.responsible.name(), assignedTo);
+		}
+		if (!StringUtils.isEmpty(sort)) {
+			params.add("sort", sort);
+		}
+		if (!descending) {
+			params.add("direction", "asc");
+		}
+		if (page > 1) {
+			params.add("pg", "" + page);
+		}
+		return params;
+	}
+
+	protected void buildPager(
+			final String query,
+			final String milestone,
+			final String [] states,
+			final String assignedTo,
+			final String sort,
+			final boolean desc,
+			final int page,
+			int pageSize,
+			int count,
+			int total) {
+
+		boolean showNav = total > (2 * pageSize);
+		boolean allowPrev = page > 1;
+		boolean allowNext = (pageSize * (page - 1) + count) < total;
+		add(new BookmarkablePageLink<Void>("prevLink", MyTicketsPage.class, queryParameters(query, milestone, states, assignedTo, sort, desc, page - 1)).setEnabled(allowPrev).setVisible(showNav));
+		add(new BookmarkablePageLink<Void>("nextLink", MyTicketsPage.class, queryParameters(query, milestone, states, assignedTo, sort, desc, page + 1)).setEnabled(allowNext).setVisible(showNav));
+
+		if (total <= pageSize) {
+			add(new Label("pageLink").setVisible(false));
+			return;
+		}
+
+		// determine page numbers to display
+		int pages = count == 0 ? 0 : ((total / pageSize) + (total % pageSize == 0 ? 0 : 1));
+		// preferred number of pagelinks
+		int segments = 5;
+		if (pages < segments) {
+			// not enough data for preferred number of page links
+			segments = pages;
+		}
+		int minpage = Math.min(Math.max(1, page - 2), pages - (segments - 1));
+		int maxpage = Math.min(pages, minpage + (segments - 1));
+		List<Integer> sequence = new ArrayList<Integer>();
+		for (int i = minpage; i <= maxpage; i++) {
+			sequence.add(i);
+		}
+
+		ListDataProvider<Integer> pagesDp = new ListDataProvider<Integer>(sequence);
+		DataView<Integer> pagesView = new DataView<Integer>("pageLink", pagesDp) {
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			public void populateItem(final Item<Integer> item) {
+				final Integer i = item.getModelObject();
+				LinkPanel link = new LinkPanel("page", null, "" + i, MyTicketsPage.class, queryParameters(query, milestone, states, assignedTo, sort, desc, i));
+				link.setRenderBodyOnly(true);
+				if (i == page) {
+					WicketUtils.setCssClass(item, "active");
+				}
+				item.add(link);
+			}
+		};
+		add(pagesView);
+	}
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.html b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.html
index cb4f1b6..22544bc 100644
--- a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.html
+++ b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.html
@@ -17,7 +17,7 @@
 								<form class="form-search" style="margin: 0px;" wicket:id="searchForm">
 									<div class="input-append">
 										<select class="span2" style="border-radius: 4px;" wicket:id="searchType"/>
-										<input type="text" class="search-query" style="width: 170px;border-radius: 14px 0 0 14px; padding-left: 14px;" id="searchBox" wicket:id="searchBox" value=""/>
+										<input type="text" class="input-medium search-query" style="border-radius: 14px 0 0 14px; padding-left: 14px;" id="searchBox" wicket:id="searchBox" value=""/>
 										<button class="btn" style="border-radius: 0 14px 14px 0px;margin-left:-5px;" type="submit"><i class="icon-search"></i></button>
 									</div>
 								</form>
diff --git a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
index 2b97bc1..5ea99fd 100644
--- a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
@@ -69,6 +69,7 @@
 import com.gitblit.wicket.PageRegistration;
 import com.gitblit.wicket.PageRegistration.OtherPageLink;
 import com.gitblit.wicket.SessionlessForm;
+import com.gitblit.wicket.TicketsUI;
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.panels.LinkPanel;
 import com.gitblit.wicket.panels.NavigationPanel;
@@ -204,7 +205,7 @@
 		pages.put("tree", new PageRegistration("gb.tree", TreePage.class, params));
 		if (app().tickets().isReady() && (app().tickets().isAcceptingNewTickets(getRepositoryModel()) || app().tickets().hasTickets(getRepositoryModel()))) {
 			PageParameters tParams = new PageParameters(params);
-			for (String state : TicketsPage.openStatii) {
+			for (String state : TicketsUI.openStatii) {
 				tParams.add(Lucene.status.name(), state);
 			}
 			pages.put("tickets", new PageRegistration("gb.tickets", TicketsPage.class, tParams));
diff --git a/src/main/java/com/gitblit/wicket/pages/RootPage.java b/src/main/java/com/gitblit/wicket/pages/RootPage.java
index 5ccc3a4..c59c189 100644
--- a/src/main/java/com/gitblit/wicket/pages/RootPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/RootPage.java
@@ -134,6 +134,8 @@
 		boolean authenticateView = app().settings().getBoolean(Keys.web.authenticateViewPages, false);
 		boolean authenticateAdmin = app().settings().getBoolean(Keys.web.authenticateAdminPages, true);
 		boolean allowAdmin = app().settings().getBoolean(Keys.web.allowAdministration, true);
+		boolean allowLucene = app().settings().getBoolean(Keys.web.allowLuceneIndexing, true);
+		boolean isLoggedIn = GitBlitWebSession.get().isLoggedIn();
 
 		if (authenticateAdmin) {
 			showAdmin = allowAdmin && GitBlitWebSession.get().canAdmin();
@@ -151,7 +153,7 @@
 		}
 
 		if (authenticateView || authenticateAdmin) {
-			if (GitBlitWebSession.get().isLoggedIn()) {
+			if (isLoggedIn) {
 				UserMenu userFragment = new UserMenu("userPanel", "userMenuFragment", RootPage.this);
 				add(userFragment);
 			} else {
@@ -167,13 +169,16 @@
 
 		// navigation links
 		List<PageRegistration> pages = new ArrayList<PageRegistration>();
-		if (!authenticateView || (authenticateView && GitBlitWebSession.get().isLoggedIn())) {
-			pages.add(new PageRegistration(GitBlitWebSession.get().isLoggedIn() ? "gb.myDashboard" : "gb.dashboard", MyDashboardPage.class,
+		if (!authenticateView || (authenticateView && isLoggedIn)) {
+			pages.add(new PageRegistration(isLoggedIn ? "gb.myDashboard" : "gb.dashboard", MyDashboardPage.class,
 					getRootPageParameters()));
+			if (isLoggedIn && app().tickets().isReady()) {
+				pages.add(new PageRegistration("gb.myTickets", MyTicketsPage.class));
+			}
 			pages.add(new PageRegistration("gb.repositories", RepositoriesPage.class,
 					getRootPageParameters()));
 			pages.add(new PageRegistration("gb.activity", ActivityPage.class, getRootPageParameters()));
-			if (app().settings().getBoolean(Keys.web.allowLuceneIndexing, true)) {
+			if (allowLucene) {
 				pages.add(new PageRegistration("gb.search", LuceneSearchPage.class));
 			}
 			if (showAdmin) {
@@ -183,7 +188,7 @@
 				pages.add(new PageRegistration("gb.federation", FederationPage.class));
 			}
 
-			if (!authenticateView || (authenticateView && GitBlitWebSession.get().isLoggedIn())) {
+			if (!authenticateView || (authenticateView && isLoggedIn)) {
 				addDropDownMenus(pages);
 			}
 		}
diff --git a/src/main/java/com/gitblit/wicket/pages/TicketBasePage.java b/src/main/java/com/gitblit/wicket/pages/TicketBasePage.java
deleted file mode 100644
index 60fa638..0000000
--- a/src/main/java/com/gitblit/wicket/pages/TicketBasePage.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * 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.pages;
-
-import org.apache.wicket.PageParameters;
-import org.apache.wicket.markup.html.basic.Label;
-
-import com.gitblit.models.TicketModel;
-import com.gitblit.models.TicketModel.Status;
-import com.gitblit.models.TicketModel.Type;
-import com.gitblit.wicket.WicketUtils;
-
-public abstract class TicketBasePage extends RepositoryPage {
-
-	public TicketBasePage(PageParameters params) {
-		super(params);
-	}
-
-	protected Label getStateIcon(String wicketId, TicketModel ticket) {
-		return getStateIcon(wicketId, ticket.type, ticket.status);
-	}
-
-	protected Label getStateIcon(String wicketId, Type type, Status state) {
-		Label label = new Label(wicketId);
-		if (type == null) {
-			type = Type.defaultType;
-		}
-		switch (type) {
-		case Proposal:
-			WicketUtils.setCssClass(label, "fa fa-code-fork");
-			break;
-		case Bug:
-			WicketUtils.setCssClass(label, "fa fa-bug");
-			break;
-		case Enhancement:
-			WicketUtils.setCssClass(label, "fa fa-magic");
-			break;
-		case Question:
-			WicketUtils.setCssClass(label, "fa fa-question");
-			break;
-		default:
-			// standard ticket
-			WicketUtils.setCssClass(label, "fa fa-ticket");
-		}
-		WicketUtils.setHtmlTooltip(label, getTypeState(type, state));
-		return label;
-	}
-
-	protected String getTypeState(Type type, Status state) {
-		return state.toString() + " " + type.toString();
-	}
-
-	protected String getLozengeClass(Status status, boolean subtle) {
-		if (status == null) {
-			status = Status.New;
-		}
-		String css = "";
-		switch (status) {
-		case Declined:
-		case Duplicate:
-		case Invalid:
-		case Wontfix:
-		case Abandoned:
-			css = "aui-lozenge-error";
-			break;
-		case Fixed:
-		case Merged:
-		case Resolved:
-			css = "aui-lozenge-success";
-			break;
-		case New:
-			css = "aui-lozenge-complete";
-			break;
-		case On_Hold:
-			css = "aui-lozenge-current";
-			break;
-		default:
-			css = "";
-			break;
-		}
-
-		return "aui-lozenge" + (subtle ? " aui-lozenge-subtle": "") + (css.isEmpty() ? "" : " ") + css;
-	}
-
-	protected String getStatusClass(Status status) {
-		String css = "";
-		switch (status) {
-		case Declined:
-		case Duplicate:
-		case Invalid:
-		case Wontfix:
-		case Abandoned:
-			css = "resolution-error";
-			break;
-		case Fixed:
-		case Merged:
-		case Resolved:
-			css = "resolution-success";
-			break;
-		case New:
-			css = "resolution-complete";
-			break;
-		case On_Hold:
-			css = "resolution-current";
-			break;
-		default:
-			css = "";
-			break;
-		}
-
-		return "resolution" + (css.isEmpty() ? "" : " ") + css;
-	}
-}
diff --git a/src/main/java/com/gitblit/wicket/pages/TicketPage.java b/src/main/java/com/gitblit/wicket/pages/TicketPage.java
index 659acad..c066f24 100644
--- a/src/main/java/com/gitblit/wicket/pages/TicketPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/TicketPage.java
@@ -86,6 +86,7 @@
 import com.gitblit.utils.StringUtils;
 import com.gitblit.utils.TimeUtils;
 import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.TicketsUI;
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.panels.BasePanel.JavascriptTextPrompt;
 import com.gitblit.wicket.panels.CommentPanel;
@@ -102,7 +103,7 @@
  * @author James Moger
  *
  */
-public class TicketPage extends TicketBasePage {
+public class TicketPage extends RepositoryPage {
 
 	static final String NIL = "<nil>";
 
@@ -154,7 +155,7 @@
 		String href = urlFor(TicketsPage.class, params).toString();
 		add(new ExternalLink("ticketNumber", href, "#" + ticket.number));
 		Label headerStatus = new Label("headerStatus", ticket.status.toString());
-		WicketUtils.setCssClass(headerStatus, getLozengeClass(ticket.status, false));
+		WicketUtils.setCssClass(headerStatus, TicketsUI.getLozengeClass(ticket.status, false));
 		add(headerStatus);
 		add(new Label("ticketTitle", ticket.title));
 		if (currentPatchset == null) {
@@ -317,10 +318,10 @@
 		 * LARGE STATUS INDICATOR WITH ICON (DISCUSSION TAB->SIDE BAR)
 		 */
 		Fragment ticketStatus = new Fragment("ticketStatus", "ticketStatusFragment", this);
-		Label ticketIcon = getStateIcon("ticketIcon", ticket);
+		Label ticketIcon = TicketsUI.getStateIcon("ticketIcon", ticket);
 		ticketStatus.add(ticketIcon);
 		ticketStatus.add(new Label("ticketStatus", ticket.status.toString()));
-		WicketUtils.setCssClass(ticketStatus, getLozengeClass(ticket.status, false));
+		WicketUtils.setCssClass(ticketStatus, TicketsUI.getLozengeClass(ticket.status, false));
 		add(ticketStatus);
 
 
@@ -370,7 +371,7 @@
 								setResponsePage(TicketsPage.class, getPageParameters());
 							}
 						};
-						String css = getStatusClass(item.getModel().getObject());
+						String css = TicketsUI.getStatusClass(item.getModel().getObject());
 						WicketUtils.setCssClass(link, css);
 						item.add(link);
 					}
@@ -665,7 +666,7 @@
 						 */
 						Fragment frag = new Fragment("entry", "statusFragment", this);
 						Label status = new Label("statusChange", entry.getStatus().toString());
-						String css = getLozengeClass(entry.getStatus(), false);
+						String css = TicketsUI.getLozengeClass(entry.getStatus(), false);
 						WicketUtils.setCssClass(status, css);
 						for (IBehavior b : status.getBehaviors()) {
 							if (b instanceof SimpleAttributeModifier) {
@@ -936,7 +937,7 @@
 							case status:
 								// special handling for status
 								Status status = event.getStatus();
-								String css = getLozengeClass(status, true);
+								String css = TicketsUI.getLozengeClass(status, true);
 								value = String.format("<span class=\"%1$s\">%2$s</span>", css, status.toString());
 								break;
 							default:
@@ -1525,14 +1526,14 @@
 		switch (type) {
 			case Rebase:
 			case Rebase_Squash:
-				typeCss = getLozengeClass(Status.Declined, false);
+				typeCss = TicketsUI.getLozengeClass(Status.Declined, false);
 				break;
 			case Squash:
 			case Amend:
-				typeCss = getLozengeClass(Status.On_Hold, false);
+				typeCss = TicketsUI.getLozengeClass(Status.On_Hold, false);
 				break;
 			case Proposal:
-				typeCss = getLozengeClass(Status.New, false);
+				typeCss = TicketsUI.getLozengeClass(Status.New, false);
 				break;
 			case FastForward:
 			default:
diff --git a/src/main/java/com/gitblit/wicket/pages/TicketsPage.html b/src/main/java/com/gitblit/wicket/pages/TicketsPage.html
index a40d312..40060f3 100644
--- a/src/main/java/com/gitblit/wicket/pages/TicketsPage.html
+++ b/src/main/java/com/gitblit/wicket/pages/TicketsPage.html
@@ -11,7 +11,7 @@
 	<div class="hidden-phone pull-right">
 		<form class="form-search" style="margin: 0px;" wicket:id="ticketSearchForm">
 			<div class="input-append">
-				<input type="text" class="search-query" style="width: 170px;border-radius: 14px 0 0 14px; padding-left: 14px;" id="ticketSearchBox" wicket:id="ticketSearchBox" value=""/>
+				<input type="text" class="input-medium search-query" style="border-radius: 14px 0 0 14px; padding-left: 14px;" id="ticketSearchBox" wicket:id="ticketSearchBox" value=""/>
 				<button class="btn" style="border-radius: 0 14px 14px 0px;margin-left:-5px;" type="submit"><i class="icon-search"></i></button>
 			</div>
 		</form>
@@ -88,42 +88,7 @@
 				</div>
 			</div>
 		
-		
-			<table class="table tickets">			
-			<tbody>
-       		<tr wicket:id="ticket">
-       			<td class="ticket-list-icon">
-       				<i wicket:id="state"></i>
-       			</td>
-        		<td>
-        			<span wicket:id="title">[title]</span> <span wicket:id="labels" style="font-weight: normal;color:white;"><span class="label" wicket:id="label"></span></span>
-        			<div class="ticket-list-details">
-        				<span style="padding-right: 10px;" class="hidden-phone">
-        					<wicket:message key="gb.createdBy"></wicket:message>
-        					<span style="padding: 0px 2px" wicket:id="createdBy">[createdBy]</span> <span class="date" wicket:id="createDate">[create date]</span>
-        				</span>
-        				<span wicket:id="indicators" style="white-space:nowrap;"><i wicket:id="icon"></i> <span style="padding-right:10px;" wicket:id="count"></span></span>
-        			</div>
-        			<div class="hidden-phone" wicket:id="updated"></div>
-        		</td>
-        		<td class="ticket-list-state">
-       				<span class="badge badge-info" wicket:id="votes"></span>
-        		</td>
-        		<td class="hidden-phone ticket-list-state">
-       				<i wicket:message="title:gb.watching" style="color:#888;" class="fa fa-eye" wicket:id="watching"></i>
-        		</td>
-        		<td class="ticket-list-state">
-       				<div wicket:id="status"></div>
-        		</td>
-        		<td class="indicators">
-        			<div>        	 		        	 		
-        	 			<b>#<span wicket:id="id">[id]</span></b>
-        	 		</div>
-        			<div wicket:id="responsible"></div>
-        		</td>
-       		</tr>
-    		</tbody>
-			</table>
+			<div wicket:id="ticketList"></div>
 		
 			<div class="btn-group pull-right">
 					<div class="pagination pagination-right pagination-small">
@@ -238,13 +203,6 @@
 		<li class="nav-header"><wicket:message key="gb.topicsAndLabels"></wicket:message></li>
 		<li class="dynamicQuery" wicket:id="dynamicQuery"><span><span wicket:id="swatch"></span> <span wicket:id="link"></span></span><span class="pull-right"><i style="font-size: 18px;" wicket:id="checked"></i></span></li>
 	</ul>
-</wicket:fragment>
-
-<wicket:fragment wicket:id="updatedFragment">
-	<div class="ticket-list-details">
-		<wicket:message key="gb.updatedBy"></wicket:message>
-		<span style="padding: 0px 2px" wicket:id="updatedBy">[updatedBy]</span> <span class="date" wicket:id="updateDate">[update date]</span>
-	</div>
 </wicket:fragment>
 
 </wicket:extend>
diff --git a/src/main/java/com/gitblit/wicket/pages/TicketsPage.java b/src/main/java/com/gitblit/wicket/pages/TicketsPage.java
index 5973d47..d88ccb6 100644
--- a/src/main/java/com/gitblit/wicket/pages/TicketsPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/TicketsPage.java
@@ -15,7 +15,6 @@
  */
 package com.gitblit.wicket.pages;
 
-import java.io.Serializable;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -29,17 +28,12 @@
 import org.apache.wicket.PageParameters;
 import org.apache.wicket.behavior.SimpleAttributeModifier;
 import org.apache.wicket.markup.html.basic.Label;
-import org.apache.wicket.markup.html.form.TextField;
 import org.apache.wicket.markup.html.link.BookmarkablePageLink;
 import org.apache.wicket.markup.html.panel.Fragment;
 import org.apache.wicket.markup.repeater.Item;
 import org.apache.wicket.markup.repeater.data.DataView;
 import org.apache.wicket.markup.repeater.data.ListDataProvider;
-import org.apache.wicket.model.IModel;
-import org.apache.wicket.model.Model;
-import org.apache.wicket.request.target.basic.RedirectRequestTarget;
 
-import com.gitblit.Constants;
 import com.gitblit.Constants.AccessPermission;
 import com.gitblit.Keys;
 import com.gitblit.models.RegistrantAccessPermission;
@@ -56,18 +50,17 @@
 import com.gitblit.utils.ArrayUtils;
 import com.gitblit.utils.StringUtils;
 import com.gitblit.wicket.GitBlitWebSession;
-import com.gitblit.wicket.SessionlessForm;
+import com.gitblit.wicket.TicketsUI;
+import com.gitblit.wicket.TicketsUI.TicketQuery;
+import com.gitblit.wicket.TicketsUI.TicketSort;
 import com.gitblit.wicket.WicketUtils;
-import com.gitblit.wicket.panels.GravatarImage;
 import com.gitblit.wicket.panels.LinkPanel;
+import com.gitblit.wicket.panels.TicketListPanel;
+import com.gitblit.wicket.panels.TicketSearchForm;
 
-public class TicketsPage extends TicketBasePage {
+public class TicketsPage extends RepositoryPage {
 
 	final TicketResponsible any;
-
-	public static final String [] openStatii = new String [] { Status.New.name().toLowerCase(), Status.Open.name().toLowerCase() };
-
-	public static final String [] closedStatii = new String [] { "!" + Status.New.name().toLowerCase(), "!" + Status.Open.name().toLowerCase() };
 
 	public TicketsPage(PageParameters params) {
 		super(params);
@@ -102,11 +95,8 @@
 		final String sortBy = Lucene.fromString(params.getString("sort", Lucene.created.name())).name();
 		final boolean desc = !"asc".equals(params.getString("direction", "desc"));
 
-
 		// add search form
-		TicketSearchForm searchForm = new TicketSearchForm("ticketSearchForm", repositoryName, searchParam);
-		add(searchForm);
-		searchForm.setTranslatedAttributes();
+		add(new TicketSearchForm("ticketSearchForm", repositoryName, searchParam, getClass(), params));
 
 		final String activeQuery;
 		if (!StringUtils.isEmpty(searchParam)) {
@@ -192,12 +182,12 @@
 			milestonePanel.add(new LinkPanel("openTickets", null,
 					MessageFormat.format(getString("gb.nOpenTickets"), currentMilestone.getOpenTickets()),
 					TicketsPage.class,
-					queryParameters(null, currentMilestone.name, openStatii, null, sortBy, desc, 1)));
+					queryParameters(null, currentMilestone.name, TicketsUI.openStatii, null, sortBy, desc, 1)));
 
 			milestonePanel.add(new LinkPanel("closedTickets", null,
 					MessageFormat.format(getString("gb.nClosedTickets"), currentMilestone.getClosedTickets()),
 					TicketsPage.class,
-					queryParameters(null, currentMilestone.name, closedStatii, null, sortBy, desc, 1)));
+					queryParameters(null, currentMilestone.name, TicketsUI.closedStatii, null, sortBy, desc, 1)));
 
 			milestonePanel.add(new Label("totalTickets", MessageFormat.format(getString("gb.nTotalTickets"), currentMilestone.getTotalTickets())));
 			add(milestonePanel);
@@ -287,7 +277,7 @@
 				queryParameters(
 						null,
 						milestoneParam,
-						openStatii,
+						TicketsUI.openStatii,
 						null,
 						null,
 						true,
@@ -397,8 +387,8 @@
 		} else {
 			add(new Label("selectedStatii", StringUtils.flattenStrings(Arrays.asList(statiiParam), ",")));
 		}
-		add(new BookmarkablePageLink<Void>("openTickets", TicketsPage.class, queryParameters(queryParam, milestoneParam, openStatii, assignedToParam, sortBy, desc, 1)));
-		add(new BookmarkablePageLink<Void>("closedTickets", TicketsPage.class, queryParameters(queryParam, milestoneParam, closedStatii, assignedToParam, sortBy, desc, 1)));
+		add(new BookmarkablePageLink<Void>("openTickets", TicketsPage.class, queryParameters(queryParam, milestoneParam, TicketsUI.openStatii, assignedToParam, sortBy, desc, 1)));
+		add(new BookmarkablePageLink<Void>("closedTickets", TicketsPage.class, queryParameters(queryParam, milestoneParam, TicketsUI.closedStatii, assignedToParam, sortBy, desc, 1)));
 		add(new BookmarkablePageLink<Void>("allTickets", TicketsPage.class, queryParameters(queryParam, milestoneParam, null, assignedToParam, sortBy, desc, 1)));
 
 		// by status
@@ -412,7 +402,7 @@
 			public void populateItem(final Item<Status> item) {
 				final Status status = item.getModelObject();
 				PageParameters p = queryParameters(queryParam, milestoneParam, new String [] { status.name().toLowerCase() }, assignedToParam, sortBy, desc, 1);
-				String css = getStatusClass(status);
+				String css = TicketsUI.getStatusClass(status);
 				item.add(new LinkPanel("statusLink", css, status.toString(), TicketsPage.class, p).setRenderBodyOnly(true));
 			}
 		};
@@ -491,162 +481,7 @@
 		// paging links
 		buildPager(queryParam, milestoneParam, statiiParam, assignedToParam, sortBy, desc, page, pageSize, results.size(), totalResults);
 
-		ListDataProvider<QueryResult> resultsDataProvider = new ListDataProvider<QueryResult>(results);
-		DataView<QueryResult> ticketsView = new DataView<QueryResult>("ticket", resultsDataProvider) {
-			private static final long serialVersionUID = 1L;
-
-			@Override
-			public void populateItem(final Item<QueryResult> item) {
-				final QueryResult ticket = item.getModelObject();
-				item.add(getStateIcon("state", ticket.type, ticket.status));
-				item.add(new Label("id", "" + ticket.number));
-				UserModel creator = app().users().getUserModel(ticket.createdBy);
-				if (creator != null) {
-					item.add(new LinkPanel("createdBy", null, creator.getDisplayName(),
-						UserPage.class, WicketUtils.newUsernameParameter(ticket.createdBy)));
-				} else {
-					item.add(new Label("createdBy", ticket.createdBy));
-				}
-				item.add(WicketUtils.createDateLabel("createDate", ticket.createdAt, GitBlitWebSession
-						.get().getTimezone(), getTimeUtils(), false));
-
-				if (ticket.updatedAt == null) {
-					item.add(new Label("updated").setVisible(false));
-				} else {
-					Fragment updated = new Fragment("updated", "updatedFragment", this);
-					UserModel updater = app().users().getUserModel(ticket.updatedBy);
-					if (updater != null) {
-						updated.add(new LinkPanel("updatedBy", null, updater.getDisplayName(),
-								UserPage.class, WicketUtils.newUsernameParameter(ticket.updatedBy)));
-					} else {
-						updated.add(new Label("updatedBy", ticket.updatedBy));
-					}
-					updated.add(WicketUtils.createDateLabel("updateDate", ticket.updatedAt, GitBlitWebSession
-							.get().getTimezone(), getTimeUtils(), false));
-					item.add(updated);
-				}
-
-				item.add(new LinkPanel("title", "list subject", StringUtils.trimString(
-						ticket.title, Constants.LEN_SHORTLOG), TicketsPage.class, newTicketParameter(ticket)));
-
-				ListDataProvider<String> labelsProvider = new ListDataProvider<String>(ticket.getLabels());
-				DataView<String> labelsView = new DataView<String>("labels", labelsProvider) {
-					private static final long serialVersionUID = 1L;
-
-					@Override
-					public void populateItem(final Item<String> labelItem) {
-						String content = bugtraqProcessor().processPlainCommitMessage(getRepository(), repositoryName, labelItem.getModelObject());
-						Label label = new Label("label", content);
-						label.setEscapeModelStrings(false);
-						TicketLabel tLabel = app().tickets().getLabel(getRepositoryModel(), labelItem.getModelObject());
-						String background = MessageFormat.format("background-color:{0};", tLabel.color);
-						label.add(new SimpleAttributeModifier("style", background));
-						labelItem.add(label);
-					}
-				};
-				item.add(labelsView);
-
-				if (StringUtils.isEmpty(ticket.responsible)) {
-					item.add(new Label("responsible").setVisible(false));
-				} else {
-					UserModel responsible = app().users().getUserModel(ticket.responsible);
-					if (responsible == null) {
-						responsible = new UserModel(ticket.responsible);
-					}
-					GravatarImage avatar = new GravatarImage("responsible", responsible.getDisplayName(),
-							responsible.emailAddress, null, 16, true);
-					avatar.setTooltip(getString("gb.responsible") + ": " + responsible.getDisplayName());
-					item.add(avatar);
-				}
-
-				// votes indicator
-				Label v = new Label("votes", "" + ticket.votesCount);
-				WicketUtils.setHtmlTooltip(v, getString("gb.votes"));
-				item.add(v.setVisible(ticket.votesCount > 0));
-
-				// watching indicator
-				item.add(new Label("watching").setVisible(ticket.isWatching(GitBlitWebSession.get().getUsername())));
-
-				// status indicator
-				String css = getLozengeClass(ticket.status, true);
-				Label l = new Label("status", ticket.status.toString());
-				WicketUtils.setCssClass(l, css);
-				item.add(l);
-
-				// add the ticket indicators/icons
-				List<Indicator> indicators = new ArrayList<Indicator>();
-
-				// comments
-				if (ticket.commentsCount > 0) {
-					int count = ticket.commentsCount;
-					String pattern = "gb.nComments";
-					if (count == 1) {
-						pattern = "gb.oneComment";
-					}
-					indicators.add(new Indicator("fa fa-comment", count, pattern));
-				}
-
-				// participants
-				if (!ArrayUtils.isEmpty(ticket.participants)) {
-					int count = ticket.participants.size();
-					if (count > 1) {
-						String pattern = "gb.nParticipants";
-						indicators.add(new Indicator("fa fa-user", count, pattern));
-					}
-				}
-
-				// attachments
-				if (!ArrayUtils.isEmpty(ticket.attachments)) {
-					int count = ticket.attachments.size();
-					String pattern = "gb.nAttachments";
-					if (count == 1) {
-						pattern = "gb.oneAttachment";
-					}
-					indicators.add(new Indicator("fa fa-file", count, pattern));
-				}
-
-				// patchset revisions
-				if (ticket.patchset != null) {
-					int count = ticket.patchset.commits;
-					String pattern = "gb.nCommits";
-					if (count == 1) {
-						pattern = "gb.oneCommit";
-					}
-					indicators.add(new Indicator("fa fa-code", count, pattern));
-				}
-
-				// milestone
-				if (!StringUtils.isEmpty(ticket.milestone)) {
-					indicators.add(new Indicator("fa fa-bullseye", ticket.milestone));
-				}
-
-				ListDataProvider<Indicator> indicatorsDp = new ListDataProvider<Indicator>(indicators);
-				DataView<Indicator> indicatorsView = new DataView<Indicator>("indicators", indicatorsDp) {
-					private static final long serialVersionUID = 1L;
-
-					@Override
-					public void populateItem(final Item<Indicator> item) {
-						Indicator indicator = item.getModelObject();
-						String tooltip = indicator.getTooltip();
-
-						Label icon = new Label("icon");
-						WicketUtils.setCssClass(icon, indicator.css);
-						item.add(icon);
-
-						if (indicator.count > 0) {
-							Label count = new Label("count", "" + indicator.count);
-							item.add(count.setVisible(!StringUtils.isEmpty(tooltip)));
-						} else {
-							item.add(new Label("count").setVisible(false));
-						}
-
-						WicketUtils.setHtmlTooltip(item, tooltip);
-					}
-				};
-				item.add(indicatorsView);
-			}
-		};
-		add(ticketsView);
+		add(new TicketListPanel("ticketList", results, false, false));
 
 		// new milestone link
 		RepositoryModel repositoryModel = getRepositoryModel();
@@ -747,12 +582,12 @@
 					milestonePanel.add(new LinkPanel("openTickets", null,
 							MessageFormat.format(getString("gb.nOpenTickets"), m.getOpenTickets()),
 							TicketsPage.class,
-							queryParameters(null, tm.name, openStatii, null, null, true, 1)));
+							queryParameters(null, tm.name, TicketsUI.openStatii, null, null, true, 1)));
 
 					milestonePanel.add(new LinkPanel("closedTickets", null,
 							MessageFormat.format(getString("gb.nClosedTickets"), m.getClosedTickets()),
 							TicketsPage.class,
-							queryParameters(null, tm.name, closedStatii, null, null, true, 1)));
+							queryParameters(null, tm.name, TicketsUI.closedStatii, null, null, true, 1)));
 
 					milestonePanel.add(new Label("totalTickets", MessageFormat.format(getString("gb.nTotalTickets"), m.getTotalTickets())));
 					entryPanel.add(milestonePanel);
@@ -863,124 +698,5 @@
 			}
 		};
 		add(pagesView);
-	}
-
-	private class Indicator implements Serializable {
-
-		private static final long serialVersionUID = 1L;
-
-		final String css;
-		final int count;
-		final String tooltip;
-
-		Indicator(String css, String tooltip) {
-			this.css = css;
-			this.tooltip = tooltip;
-			this.count = 0;
-		}
-
-		Indicator(String css, int count, String pattern) {
-			this.css = css;
-			this.count = count;
-			this.tooltip = StringUtils.isEmpty(pattern) ? "" : MessageFormat.format(getString(pattern), count);
-		}
-
-		String getTooltip() {
-			return tooltip;
-		}
-	}
-
-	private class TicketQuery implements Serializable, Comparable<TicketQuery> {
-
-		private static final long serialVersionUID = 1L;
-
-		final String name;
-		final String query;
-		String color;
-
-		TicketQuery(String name, String query) {
-			this.name = name;
-			this.query = query;
-		}
-
-		TicketQuery color(String value) {
-			this.color = value;
-			return this;
-		}
-
-		@Override
-		public boolean equals(Object o) {
-			if (o instanceof TicketQuery) {
-				return ((TicketQuery) o).query.equals(query);
-			}
-			return false;
-		}
-
-		@Override
-		public int hashCode() {
-			return query.hashCode();
-		}
-
-		@Override
-		public int compareTo(TicketQuery o) {
-			return query.compareTo(o.query);
-		}
-	}
-
-	private class TicketSort implements Serializable {
-
-		private static final long serialVersionUID = 1L;
-
-		final String name;
-		final String sortBy;
-		final boolean desc;
-
-		TicketSort(String name, String sortBy, boolean desc) {
-			this.name = name;
-			this.sortBy = sortBy;
-			this.desc = desc;
-		}
-	}
-
-	private class TicketSearchForm extends SessionlessForm<Void> implements Serializable {
-		private static final long serialVersionUID = 1L;
-
-		private final String repositoryName;
-
-		private final IModel<String> searchBoxModel;;
-
-		public TicketSearchForm(String id, String repositoryName, String text) {
-			super(id, TicketsPage.this.getClass(), TicketsPage.this.getPageParameters());
-
-			this.repositoryName = repositoryName;
-			this.searchBoxModel = new Model<String>(text == null ? "" : text);
-
-			TextField<String> searchBox = new TextField<String>("ticketSearchBox", searchBoxModel);
-			add(searchBox);
-		}
-
-		void setTranslatedAttributes() {
-			WicketUtils.setHtmlTooltip(get("ticketSearchBox"),
-					MessageFormat.format(getString("gb.searchTicketsTooltip"), repositoryName));
-			WicketUtils.setInputPlaceholder(get("ticketSearchBox"), getString("gb.searchTickets"));
-		}
-
-		@Override
-		public void onSubmit() {
-			String searchString = searchBoxModel.getObject();
-			if (StringUtils.isEmpty(searchString)) {
-				// redirect to self to avoid wicket page update bug
-				String absoluteUrl = getCanonicalUrl();
-				getRequestCycle().setRequestTarget(new RedirectRequestTarget(absoluteUrl));
-				return;
-			}
-
-			// use an absolute url to workaround Wicket-Tomcat problems with
-			// mounted url parameters (issue-111)
-			PageParameters params = WicketUtils.newRepositoryParameter(repositoryName);
-			params.add("s", searchString);
-			String absoluteUrl = getCanonicalUrl(TicketsPage.class, params);
-			getRequestCycle().setRequestTarget(new RedirectRequestTarget(absoluteUrl));
-		}
 	}
 }
diff --git a/src/main/java/com/gitblit/wicket/panels/TicketListPanel.html b/src/main/java/com/gitblit/wicket/panels/TicketListPanel.html
new file mode 100644
index 0000000..6e6d209
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/TicketListPanel.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"  
+      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"  
+      xml:lang="en"  
+      lang="en"> 
+
+<body>
+<wicket:panel>
+<table class="table tickets">	
+	<tbody>
+		<tr wicket:id="row">
+       		<td class="ticket-list-icon">
+       			<i wicket:id="state"></i>
+       		</td>
+        	<td>
+        		<span wicket:id="title">[title]</span> <span wicket:id="labels" style="font-weight: normal;color:white;"><span class="label" wicket:id="label"></span></span>
+        		<div class="ticket-list-details">
+        			<span style="padding-right: 10px;" class="hidden-phone">
+        				<wicket:message key="gb.createdBy"></wicket:message>
+        				<span style="padding: 0px 2px" wicket:id="createdBy">[createdBy]</span> <span class="date" wicket:id="createDate">[create date]</span>
+        			</span>
+        			<span wicket:id="indicators" style="white-space:nowrap;"><i wicket:id="icon"></i> <span style="padding-right:10px;" wicket:id="count"></span></span>
+        		</div>
+        		<div class="hidden-phone" wicket:id="updated"></div>
+        		<div class="ticket-list-details"><span class="activitySwatch" wicket:id="repositoryLink">[repository link]</span></div>
+        	</td>
+        	<td class="ticket-list-state">
+       			<span class="badge badge-info" wicket:id="votes"></span>
+        	</td>
+        	<td class="hidden-phone ticket-list-state">
+       			<i wicket:message="title:gb.watching" style="color:#888;" class="fa fa-eye" wicket:id="watching"></i>
+        	</td>
+        	<td class="ticket-list-state">
+       			<div wicket:id="status"></div>
+        	</td>
+        	<td class="indicators">
+        		<div>        	 		        	 		
+        				<b>#<span wicket:id="id">[id]</span></b>
+        			</div>
+        		<div wicket:id="responsible"></div>
+        	</td>
+       	</tr>
+    </tbody>
+</table>
+
+<wicket:fragment wicket:id="updatedFragment">
+	<div class="ticket-list-details">
+		<wicket:message key="gb.updatedBy"></wicket:message>
+		<span style="padding: 0px 2px" wicket:id="updatedBy">[updatedBy]</span> <span class="date" wicket:id="updateDate">[update date]</span>
+	</div>
+</wicket:fragment>
+
+</wicket:panel>
+</body>
+</html>
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/TicketListPanel.java b/src/main/java/com/gitblit/wicket/panels/TicketListPanel.java
new file mode 100644
index 0000000..fc0431f
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/TicketListPanel.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2014 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.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.behavior.SimpleAttributeModifier;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.eclipse.jgit.lib.Repository;
+
+import com.gitblit.Constants;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.tickets.QueryResult;
+import com.gitblit.tickets.TicketLabel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.BugtraqProcessor;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.TicketsUI;
+import com.gitblit.wicket.TicketsUI.Indicator;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.SummaryPage;
+import com.gitblit.wicket.pages.TicketsPage;
+import com.gitblit.wicket.pages.UserPage;
+
+/**
+ *
+ * The ticket list panel lists tickets in a table.
+ *
+ * @author James Moger
+ *
+ */
+public class TicketListPanel extends BasePanel {
+
+	private static final long serialVersionUID = 1L;
+
+	public TicketListPanel(String wicketId, List<QueryResult> list, final boolean showSwatch, final boolean showRepository) {
+		super(wicketId);
+
+		final ListDataProvider<QueryResult> dp = new ListDataProvider<QueryResult>(list);
+		DataView<QueryResult> dataView = new DataView<QueryResult>("row", dp) {
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			protected void populateItem(Item<QueryResult> item) {
+				final QueryResult ticket = item.getModelObject();
+				final RepositoryModel repository = app().repositories().getRepositoryModel(ticket.repository);
+
+				if (showSwatch) {
+					// set repository color
+					String color = StringUtils.getColor(StringUtils.stripDotGit(repository.name));
+					WicketUtils.setCssStyle(item, MessageFormat.format("border-left: 2px solid {0};", color));
+				}
+
+				PageParameters rp = WicketUtils.newRepositoryParameter(ticket.repository);
+				PageParameters tp = WicketUtils.newObjectParameter(ticket.repository, "" + ticket.number);
+
+				if (showRepository) {
+					String name = StringUtils.stripDotGit(ticket.repository);
+					LinkPanel link = new LinkPanel("repositoryLink", null, name, SummaryPage.class, rp);
+					WicketUtils.setCssBackground(link, name);
+					item.add(link);
+				} else {
+					item.add(new Label("repositoryLink").setVisible(false));
+				}
+
+				item.add(TicketsUI.getStateIcon("state", ticket.type, ticket.status));
+				item.add(new Label("id", "" + ticket.number));
+				UserModel creator = app().users().getUserModel(ticket.createdBy);
+				if (creator != null) {
+					item.add(new LinkPanel("createdBy", null, creator.getDisplayName(),
+							UserPage.class, WicketUtils.newUsernameParameter(ticket.createdBy)));
+				} else {
+					item.add(new Label("createdBy", ticket.createdBy));
+				}
+				item.add(WicketUtils.createDateLabel("createDate", ticket.createdAt, GitBlitWebSession
+						.get().getTimezone(), getTimeUtils(), false));
+
+				if (ticket.updatedAt == null) {
+					item.add(new Label("updated").setVisible(false));
+				} else {
+					Fragment updated = new Fragment("updated", "updatedFragment", this);
+					UserModel updater = app().users().getUserModel(ticket.updatedBy);
+					if (updater != null) {
+						updated.add(new LinkPanel("updatedBy", null, updater.getDisplayName(),
+								UserPage.class, WicketUtils.newUsernameParameter(ticket.updatedBy)));
+					} else {
+						updated.add(new Label("updatedBy", ticket.updatedBy));
+					}
+					updated.add(WicketUtils.createDateLabel("updateDate", ticket.updatedAt, GitBlitWebSession
+							.get().getTimezone(), getTimeUtils(), false));
+					item.add(updated);
+				}
+
+				item.add(new LinkPanel("title", "list subject", StringUtils.trimString(
+						ticket.title, Constants.LEN_SHORTLOG), TicketsPage.class, tp));
+
+				ListDataProvider<String> labelsProvider = new ListDataProvider<String>(ticket.getLabels());
+				DataView<String> labelsView = new DataView<String>("labels", labelsProvider) {
+					private static final long serialVersionUID = 1L;
+
+					@Override
+					public void populateItem(final Item<String> labelItem) {
+						BugtraqProcessor btp  = new BugtraqProcessor(app().settings());
+						Repository db = app().repositories().getRepository(repository.name);
+						String content = btp.processPlainCommitMessage(db, repository.name, labelItem.getModelObject());
+						db.close();
+						Label label = new Label("label", content);
+						label.setEscapeModelStrings(false);
+						TicketLabel tLabel = app().tickets().getLabel(repository, labelItem.getModelObject());
+						String background = MessageFormat.format("background-color:{0};", tLabel.color);
+						label.add(new SimpleAttributeModifier("style", background));
+						labelItem.add(label);
+					}
+				};
+				item.add(labelsView);
+
+				if (StringUtils.isEmpty(ticket.responsible)) {
+					item.add(new Label("responsible").setVisible(false));
+				} else {
+					UserModel responsible = app().users().getUserModel(ticket.responsible);
+					if (responsible == null) {
+						responsible = new UserModel(ticket.responsible);
+					}
+					GravatarImage avatar = new GravatarImage("responsible", responsible.getDisplayName(),
+							responsible.emailAddress, null, 16, true);
+					avatar.setTooltip(getString("gb.responsible") + ": " + responsible.getDisplayName());
+					item.add(avatar);
+				}
+
+				// votes indicator
+				Label v = new Label("votes", "" + ticket.votesCount);
+				WicketUtils.setHtmlTooltip(v, getString("gb.votes"));
+				item.add(v.setVisible(ticket.votesCount > 0));
+
+				// watching indicator
+				item.add(new Label("watching").setVisible(ticket.isWatching(GitBlitWebSession.get().getUsername())));
+
+				// status indicator
+				String css = TicketsUI.getLozengeClass(ticket.status, true);
+				Label l = new Label("status", ticket.status.toString());
+				WicketUtils.setCssClass(l, css);
+				item.add(l);
+
+				// add the ticket indicators/icons
+				List<Indicator> indicators = new ArrayList<Indicator>();
+
+				// comments
+				if (ticket.commentsCount > 0) {
+					int count = ticket.commentsCount;
+					String pattern = getString("gb.nComments");
+					if (count == 1) {
+						pattern = getString("gb.oneComment");
+					}
+					indicators.add(new Indicator("fa fa-comment", count, pattern));
+				}
+
+				// participants
+				if (!ArrayUtils.isEmpty(ticket.participants)) {
+					int count = ticket.participants.size();
+					if (count > 1) {
+						String pattern = getString("gb.nParticipants");
+						indicators.add(new Indicator("fa fa-user", count, pattern));
+					}
+				}
+
+				// attachments
+				if (!ArrayUtils.isEmpty(ticket.attachments)) {
+					int count = ticket.attachments.size();
+					String pattern = getString("gb.nAttachments");
+					if (count == 1) {
+						pattern = getString("gb.oneAttachment");
+					}
+					indicators.add(new Indicator("fa fa-file", count, pattern));
+				}
+
+				// patchset revisions
+				if (ticket.patchset != null) {
+					int count = ticket.patchset.commits;
+					String pattern = getString("gb.nCommits");
+					if (count == 1) {
+						pattern = getString("gb.oneCommit");
+					}
+					indicators.add(new Indicator("fa fa-code", count, pattern));
+				}
+
+				// milestone
+				if (!StringUtils.isEmpty(ticket.milestone)) {
+					indicators.add(new Indicator("fa fa-bullseye", ticket.milestone));
+				}
+
+				ListDataProvider<Indicator> indicatorsDp = new ListDataProvider<Indicator>(indicators);
+				DataView<Indicator> indicatorsView = new DataView<Indicator>("indicators", indicatorsDp) {
+					private static final long serialVersionUID = 1L;
+
+					@Override
+					public void populateItem(final Item<Indicator> item) {
+						Indicator indicator = item.getModelObject();
+						String tooltip = indicator.getTooltip();
+
+						Label icon = new Label("icon");
+						WicketUtils.setCssClass(icon, indicator.css);
+						item.add(icon);
+
+						if (indicator.count > 0) {
+							Label count = new Label("count", "" + indicator.count);
+							item.add(count.setVisible(!StringUtils.isEmpty(tooltip)));
+						} else {
+							item.add(new Label("count").setVisible(false));
+						}
+
+						WicketUtils.setHtmlTooltip(item, tooltip);
+					}
+				};
+				item.add(indicatorsView);
+			}
+		};
+
+		add(dataView);
+	}
+}
+
diff --git a/src/main/java/com/gitblit/wicket/panels/TicketSearchForm.java b/src/main/java/com/gitblit/wicket/panels/TicketSearchForm.java
new file mode 100644
index 0000000..21bf1ba
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/TicketSearchForm.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2014 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.MessageFormat;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.form.TextField;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.request.target.basic.RedirectRequestTarget;
+
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.SessionlessForm;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.BasePage;
+
+public class TicketSearchForm extends SessionlessForm<Void> implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	private final String repositoryName;
+
+	private final IModel<String> searchBoxModel;
+
+	public TicketSearchForm(String id, String repositoryName, String text,
+			Class<? extends BasePage> pageClass, PageParameters params) {
+
+		super(id, pageClass, params);
+
+		this.repositoryName = repositoryName;
+		this.searchBoxModel = new Model<String>(text == null ? "" : text);
+
+		TextField<String> searchBox = new TextField<String>("ticketSearchBox", searchBoxModel);
+		add(searchBox);
+	}
+
+	@Override
+	protected
+	void onInitialize() {
+		super.onInitialize();
+		WicketUtils.setHtmlTooltip(get("ticketSearchBox"),
+				MessageFormat.format(getString("gb.searchTicketsTooltip"), ""));
+		WicketUtils.setInputPlaceholder(get("ticketSearchBox"), getString("gb.searchTickets"));
+	}
+
+	@Override
+	public void onSubmit() {
+		String searchString = searchBoxModel.getObject();
+		if (StringUtils.isEmpty(searchString)) {
+			// redirect to self to avoid wicket page update bug
+			String absoluteUrl = getAbsoluteUrl();
+			getRequestCycle().setRequestTarget(new RedirectRequestTarget(absoluteUrl));
+			return;
+		}
+
+		// use an absolute url to workaround Wicket-Tomcat problems with
+		// mounted url parameters (issue-111)
+		PageParameters params = WicketUtils.newRepositoryParameter(repositoryName);
+		params.add("s", searchString);
+		String absoluteUrl = getAbsoluteUrl(pageClass, params);
+		getRequestCycle().setRequestTarget(new RedirectRequestTarget(absoluteUrl));
+	}
+}
diff --git a/src/main/java/com/gitblit/wicket/panels/UserTitlePanel.html b/src/main/java/com/gitblit/wicket/panels/UserTitlePanel.html
new file mode 100644
index 0000000..432c880
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/UserTitlePanel.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"  
+      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"  
+      xml:lang="en"  
+      lang="en"> 
+
+<body>
+<wicket:panel>
+<div style="display:inline-block;vertical-align:top;padding: 0px 2px 2px;"><img wicket:id="userGravatar"></img></div>
+	<div style="display:inline-block;">
+	<div style="font-size:1.5em;" wicket:id="userDisplayName"></div>
+	<div style="color:#888;font-size:1.2em;padding-top:4px;"><span wicket:id="userTitle"></span></div>
+</div>
+</wicket:panel>
+</body>
+</html>
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/UserTitlePanel.java b/src/main/java/com/gitblit/wicket/panels/UserTitlePanel.java
new file mode 100644
index 0000000..2bf5ee7
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/UserTitlePanel.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2014 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 org.apache.wicket.markup.html.basic.Label;
+
+import com.gitblit.models.UserModel;
+
+public class UserTitlePanel extends BasePanel {
+
+	private static final long serialVersionUID = 1L;
+
+	public UserTitlePanel(String wicketId, UserModel user, String title) {
+		super(wicketId);
+		add(new GravatarImage("userGravatar", user, "gravatar", 36, false));
+		add(new Label("userDisplayName", user.getDisplayName()));
+		add(new Label("userTitle", title));
+	}
+}

--
Gitblit v1.9.1