Hybris95
2014-04-22 e7f65dfe56b5b6325a7d7b42f877ae8d63fa4f71
Advanced "my tickets" page. Using filters and search.
2 files modified
453 ■■■■■ changed files
src/main/java/com/gitblit/wicket/pages/MyTicketsPage.html 83 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/MyTicketsPage.java 370 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/MyTicketsPage.html
@@ -7,18 +7,73 @@
<body>
    <wicket:extend>
        <div class="container">
            <table class="tickets">
                <thead>
                    <tr>
                        <th class="left">
                            <img style="vertical-align: middle;" src="git-black-16x16.png"/>
                            <wicket:message key="gb.repository">Repository</wicket:message>
                        </th>
                        <th class="hidden-phone" colspan='2'><span><wicket:message key="gb.ticket">Ticket</wicket:message></span></th>
                        <th class="hidden-tablet hidden-phone"><span><wicket:message key="gb.status">Status</wicket:message></span></th>
                        <th class="hidden-tablet hidden-phone right"><span><wicket:message key="gb.responsible">Responsible</wicket:message></span></th>
                    </tr>
                </thead>
            <!-- search tickets form -->
            <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=""/>
                        <button class="btn" style="border-radius: 0 14px 14px 0px;margin-left:-5px;" type="submit"><i class="icon-search"></i></button>
                    </div>
                </form>
            </div>
            <ul class="nav nav-tabs"></ul>
            <div class="tab-content">
                <div class="row" style="min-height:400px;" >
                    <div class="tab-pane active" id="tickets">
                        <!-- query controls -->
                        <div class="span3">
                            <div class="hidden-phone">
                                <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="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>
                            <table class="table tickets">
                <tbody>
                    <tr wicket:id="row">
                        <td class="left" style="padding-left:3px;">
@@ -37,6 +92,10 @@
                </tbody>
            </table>
        </div>
                    </div>
                </div>
            </div>
        </div>
    </wicket:extend>
</body>
</html>
src/main/java/com/gitblit/wicket/pages/MyTicketsPage.java
@@ -1,14 +1,24 @@
package com.gitblit.wicket.pages;
import java.io.Serializable;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.wicket.Component;
import org.apache.wicket.PageParameters;
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.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.Keys;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.TicketModel;
import com.gitblit.models.UserModel;
@@ -18,14 +28,20 @@
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.GitBlitWebApp;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.SessionlessForm;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.GravatarImage;
import com.gitblit.wicket.panels.LinkPanel;
public class MyTicketsPage extends RootPage {
    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 MyTicketsPage()
    {
@@ -43,15 +59,205 @@
            setResponsePage(getApplication().getHomePage());
            return;
        }
        String username = currentUser.getName();
        
        QueryBuilder qb = QueryBuilder
            .q(Lucene.createdby.matches(username))
            .or(Lucene.responsible.matches(username))
            .or(Lucene.watchedby.matches(username));
        final String username = currentUser.getName();
        final String[] statiiParam = (params == null) ? new String[0] : 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 search form
        TicketSearchForm searchForm = new TicketSearchForm("ticketSearchForm", searchParam);
        add(searchForm);
        searchForm.setTranslatedAttributes();
        // 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,
                        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)));
        // 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, openStatii, assignedToParam, sortBy, desc, 1)));
        add(new BookmarkablePageLink<Void>("closedTickets", MyTicketsPage.class, queryParameters(queryParam, milestoneParam, 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 = 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.responsible.name())) {
            // specify the responsible
            qb.and(Lucene.responsible.matches(assignedToParam));
        }
        if (!qb.containsField(Lucene.milestone.name())) {
            // specify the milestone
            qb.and(Lucene.milestone.matches(milestoneParam));
        }
        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 = qb.build();
        
        ITicketService tickets = GitBlitWebApp.get().tickets();
        List<QueryResult> results = tickets.queryFor(qb.build(), 0, 0, Lucene.updated.name(), true);
        List<QueryResult> results = tickets.queryFor(luceneQuery, 0, 0, Lucene.updated.name(), true);
        
        final ListDataProvider<QueryResult> dp = new ListDataProvider<QueryResult>(results);
        
@@ -99,6 +305,12 @@
                }
            }
        };
        // paging links
        int page = (params != null) ? Math.max(1, WicketUtils.getPage(params)) : 1;
        int pageSize = app().settings().getInteger(Keys.tickets.perPage, 25);
        int totalResults = results.size() == 0 ? 0 : results.get(0).totalResults;
        buildPager(queryParam, milestoneParam, statiiParam, assignedToParam, sortBy, desc, page, pageSize, results.size(), totalResults);
        
        add(dataView);
    }
@@ -197,4 +409,150 @@
        return "resolution" + (css.isEmpty() ? "" : " ") + css;
    }
    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 IModel<String> searchBoxModel;;
        public TicketSearchForm(String id, String text) {
            super(id, MyTicketsPage.this.getClass(), MyTicketsPage.this.getPageParameters());
            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"), ""));
            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("");
            params.add("s", searchString);
            String absoluteUrl = getCanonicalUrl(MyTicketsPage.class, params);
            getRequestCycle().setRequestTarget(new RedirectRequestTarget(absoluteUrl));
        }
    }
    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", TicketsPage.class, queryParameters(query, milestone, states, assignedTo, sort, desc, page - 1)).setEnabled(allowPrev).setVisible(showNav));
        add(new BookmarkablePageLink<Void>("nextLink", TicketsPage.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, TicketsPage.class, queryParameters(query, milestone, states, assignedTo, sort, desc, i));
                link.setRenderBodyOnly(true);
                if (i == page) {
                    WicketUtils.setCssClass(item, "active");
                }
                item.add(link);
            }
        };
        add(pagesView);
    }
}