package io.onedev.server.web.page.project.issues.milestones; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.annotation.Nullable; import org.apache.wicket.Component; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.behavior.AttributeAppender; import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator; import org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColumn; import org.apache.wicket.extensions.markup.html.repeater.data.table.DataTable; import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn; import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider; import org.apache.wicket.markup.head.CssHeaderItem; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.link.BookmarkablePageLink; import org.apache.wicket.markup.html.link.Link; import org.apache.wicket.markup.html.panel.Fragment; import org.apache.wicket.markup.repeater.Item; import org.apache.wicket.model.IModel; import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.model.Model; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.hibernate.criterion.Restrictions; import io.onedev.server.OneDev; import io.onedev.server.entitymanager.IssueManager; import io.onedev.server.entitymanager.MilestoneManager; import io.onedev.server.model.Milestone; import io.onedev.server.model.Project; import io.onedev.server.persistence.dao.Dao; import io.onedev.server.persistence.dao.EntityCriteria; import io.onedev.server.search.entity.issue.IssueQuery; import io.onedev.server.search.entity.issue.StateCriteria; import io.onedev.server.security.SecurityUtils; import io.onedev.server.util.MilestoneAndState; import io.onedev.server.util.MilestoneSort; import io.onedev.server.web.WebConstants; import io.onedev.server.web.WebSession; import io.onedev.server.web.component.datatable.DefaultDataTable; import io.onedev.server.web.component.datatable.LoadableDetachableDataProvider; import io.onedev.server.web.component.floating.FloatingPanel; import io.onedev.server.web.component.issue.statestats.StateStatsBar; import io.onedev.server.web.component.link.ActionablePageLink; import io.onedev.server.web.component.link.ViewStateAwarePageLink; import io.onedev.server.web.component.menu.MenuItem; import io.onedev.server.web.component.menu.MenuLink; import io.onedev.server.web.component.milestone.MilestoneDueLabel; import io.onedev.server.web.page.project.issues.ProjectIssuesPage; import io.onedev.server.web.util.PagingHistorySupport; @SuppressWarnings("serial") public class MilestoneListPage extends ProjectIssuesPage { private static final String PARAM_STATE = "state"; private static final String PARAM_SORT = "sort"; private static final String PARAM_PAGE = "page"; private final boolean closed; private final MilestoneSort sort; private final IModel<Collection<MilestoneAndState>> milestoneAndStatesModel = new LoadableDetachableModel<Collection<MilestoneAndState>>() { @Override protected Collection<MilestoneAndState> load() { List<Milestone> milestones = new ArrayList<>(); for (Component row: (WebMarkupContainer)milestonesTable.get("body").get("rows")) { Milestone milestone = (Milestone) row.getDefaultModelObject(); milestones.add(milestone); } return OneDev.getInstance(IssueManager.class).queryMilestoneAndStates(getProject(), milestones); } }; private DataTable<Milestone, Void> milestonesTable; private EntityCriteria<Milestone> getCriteria(boolean closed) { EntityCriteria<Milestone> criteria = EntityCriteria.of(Milestone.class); criteria.add(Restrictions.eq("project", getProject())); criteria.add(Restrictions.eq("closed", closed)); return criteria; } public MilestoneListPage(PageParameters params) { super(params); String state = params.get(PARAM_STATE).toString(); if (state == null) closed = false; else closed = state.toLowerCase().equals("closed"); String sortString = params.get(PARAM_SORT).toString(); if (sortString != null) sort = MilestoneSort.valueOf(sortString.toUpperCase()); else sort = MilestoneSort.CLOSEST_DUE_DATE; } @Override protected void onInitialize() { super.onInitialize(); Link<Void> openLink = new BookmarkablePageLink<Void>("open", MilestoneListPage.class, paramsOf(getProject(), false, sort)); openLink.setOutputMarkupId(true); if (!closed) openLink.add(AttributeAppender.append("class", "active")); add(openLink); Link<Void> closeLink = new BookmarkablePageLink<Void>("closed", MilestoneListPage.class, paramsOf(getProject(), true, sort)); closeLink.setOutputMarkupId(true); if (closed) closeLink.add(AttributeAppender.append("class", "active")); add(closeLink); add(new MenuLink("sort") { @Override protected List<MenuItem> getMenuItems(FloatingPanel dropdown) { List<MenuItem> menuItems = new ArrayList<>(); for (MilestoneSort sort: MilestoneSort.values()) { menuItems.add(new MenuItem() { @Override public String getLabel() { return sort.toString(); } @Override public WebMarkupContainer newLink(String id) { PageParameters params = paramsOf(getProject(), closed, sort); Link<Void> link = new BookmarkablePageLink<Void>(id, MilestoneListPage.class, params); link.add(AttributeAppender.append("class", "milestone-sort")); if (sort == MilestoneListPage.this.sort) link.add(AttributeAppender.append("class", "active")); return link; } }); } return menuItems; } }); add(new BookmarkablePageLink<Void>("newMilestone", NewMilestonePage.class, NewMilestonePage.paramsOf(getProject())) { @Override protected void onConfigure() { super.onConfigure(); setVisible(SecurityUtils.canManageIssues(getProject())); } }); List<IColumn<Milestone, Void>> columns = new ArrayList<>(); columns.add(new AbstractColumn<Milestone, Void>(Model.of("Name")) { @Override public String getCssClass() { return "name"; } @Override public void populateItem(Item<ICellPopulator<Milestone>> cellItem, String componentId, IModel<Milestone> rowModel) { Milestone milestone = rowModel.getObject(); Fragment fragment = new Fragment(componentId, "nameFrag", MilestoneListPage.this); WebMarkupContainer link = new ActionablePageLink<Void>("link", MilestoneDetailPage.class, MilestoneDetailPage.paramsOf(milestone, null)) { @Override protected void doBeforeNav(AjaxRequestTarget target) { String redirectUrlAfterDelete = RequestCycle.get().urlFor( MilestoneListPage.class, getPageParameters()).toString(); WebSession.get().setRedirectUrlAfterDelete(Milestone.class, redirectUrlAfterDelete); } }; link.add(new Label("label", milestone.getName())); fragment.add(link); cellItem.add(fragment); } }); columns.add(new AbstractColumn<Milestone, Void>(Model.of("Due Date")) { @Override public String getCssClass() { return "due-date"; } @Override public void populateItem(Item<ICellPopulator<Milestone>> cellItem, String componentId, IModel<Milestone> rowModel) { cellItem.add(new MilestoneDueLabel(componentId, rowModel)); } }); columns.add(new AbstractColumn<Milestone, Void>(Model.of("Issue Stats")) { @Override public String getCssClass() { return "issue-stats"; } @Override public void populateItem(Item<ICellPopulator<Milestone>> cellItem, String componentId, IModel<Milestone> rowModel) { Fragment fragment = new Fragment(componentId, "issueStatsFrag", MilestoneListPage.this) { @Override protected void onBeforeRender() { /* * Create StateStatsBar here as it requires to access the milestoneAndStatsModel which can * only be calculated correctly after the milestone table is initialized */ addOrReplace(new StateStatsBar("content", new LoadableDetachableModel<Map<String, Integer>>() { @Override protected Map<String, Integer> load() { Map<String, Integer> stateStats = new HashMap<>(); for (MilestoneAndState milestoneAndState: milestoneAndStatesModel.getObject()) { if (milestoneAndState.getMilestoneId().equals(rowModel.getObject().getId())) { Integer count = stateStats.get(milestoneAndState.getIssueState()); if (count != null) count ++; else count = 1; stateStats.put(milestoneAndState.getIssueState(), count); } } return stateStats; } }) { @Override protected Link<Void> newStateLink(String componentId, String state) { String query = new IssueQuery(new StateCriteria(state)).toString(); PageParameters params = MilestoneDetailPage.paramsOf(rowModel.getObject(), query); return new ViewStateAwarePageLink<Void>(componentId, MilestoneDetailPage.class, params); } }); super.onBeforeRender(); } }; cellItem.add(fragment); } }); if (SecurityUtils.canManageIssues(getProject())) { columns.add(new AbstractColumn<Milestone, Void>(Model.of("Actions")) { @Override public String getCssClass() { return "expanded actions"; } @Override public void populateItem(Item<ICellPopulator<Milestone>> cellItem, String componentId, IModel<Milestone> rowModel) { cellItem.add(new MilestoneActionsPanel(componentId, rowModel, false) { @Override protected void onUpdated(AjaxRequestTarget target) { target.add(milestonesTable); } @Override protected void onDeleted(AjaxRequestTarget target) { target.add(milestonesTable); } }); } }); } SortableDataProvider<Milestone, Void> dataProvider = new LoadableDetachableDataProvider<Milestone, Void>() { @Override public Iterator<? extends Milestone> iterator(long first, long count) { EntityCriteria<Milestone> criteria = getCriteria(closed); criteria.addOrder(sort.getOrder(closed)); return OneDev.getInstance(Dao.class).query(criteria, (int)first, (int)count).iterator(); } @Override public long calcSize() { return OneDev.getInstance(Dao.class).count(getCriteria(closed)); } @Override public IModel<Milestone> model(Milestone object) { Long id = object.getId(); return new LoadableDetachableModel<Milestone>() { @Override protected Milestone load() { return OneDev.getInstance(MilestoneManager.class).load(id); } }; } }; PagingHistorySupport pagingHistorySupport = new PagingHistorySupport() { @Override public PageParameters newPageParameters(int currentPage) { PageParameters params = paramsOf(getProject(), closed, sort); params.add(PARAM_PAGE, currentPage+1); return params; } @Override public int getCurrentPage() { return getPageParameters().get(PARAM_PAGE).toInt(1)-1; } }; add(milestonesTable = new DefaultDataTable<Milestone, Void>("milestones", columns, dataProvider, WebConstants.PAGE_SIZE, pagingHistorySupport)); milestonesTable.setOutputMarkupId(true); } @Override protected void onDetach() { milestoneAndStatesModel.detach(); super.onDetach(); } @Override public void renderHead(IHeaderResponse response) { super.renderHead(response); response.render(CssHeaderItem.forReference(new MilestonesResourceReference())); } public static PageParameters paramsOf(Project project, boolean closed, @Nullable MilestoneSort sort) { PageParameters params = paramsOf(project); if (closed) params.add(PARAM_STATE, "closed"); else params.add(PARAM_STATE, "open"); if (sort != null) params.add(PARAM_SORT, sort.name().toLowerCase()); return params; } }