From c9de84169e407fbf1963e7ad0e1f3f3e57cfae24 Mon Sep 17 00:00:00 2001 From: James Moger <james.moger@gitblit.com> Date: Thu, 01 May 2014 14:55:57 -0400 Subject: [PATCH] Merged #17 "CRUD Milestone Pages" --- src/main/java/com/gitblit/wicket/pages/TicketsPage.java | 106 ++++++++- src/main/java/com/gitblit/wicket/pages/EditMilestonePage.java | 189 +++++++++++++++++ src/main/java/com/gitblit/wicket/pages/NewMilestonePage.html | 37 +++ src/main/java/com/gitblit/tickets/QueryResult.java | 8 src/main/java/com/gitblit/tickets/ITicketService.java | 60 +++++ src/main/java/com/gitblit/wicket/GitBlitWebApp.java | 4 src/main/java/com/gitblit/wicket/GitBlitWebApp.properties | 8 src/main/java/com/gitblit/wicket/pages/EditMilestonePage.html | 39 +++ src/main/java/com/gitblit/wicket/pages/NewMilestonePage.java | 140 ++++++++++++ src/main/java/com/gitblit/tickets/TicketLabel.java | 7 src/main/java/com/gitblit/wicket/pages/TicketsPage.html | 43 +++ src/main/java/com/gitblit/tickets/TicketMilestone.java | 12 + 12 files changed, 631 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/gitblit/tickets/ITicketService.java b/src/main/java/com/gitblit/tickets/ITicketService.java index c2f3283..3261ca9 100644 --- a/src/main/java/com/gitblit/tickets/ITicketService.java +++ b/src/main/java/com/gitblit/tickets/ITicketService.java @@ -49,6 +49,7 @@ import com.gitblit.models.TicketModel.Patchset; import com.gitblit.models.TicketModel.Status; import com.gitblit.tickets.TicketIndexer.Lucene; +import com.gitblit.utils.DeepCopier; import com.gitblit.utils.DiffUtils; import com.gitblit.utils.DiffUtils.DiffStat; import com.gitblit.utils.StringUtils; @@ -556,9 +557,10 @@ public TicketMilestone getMilestone(RepositoryModel repository, String milestone) { for (TicketMilestone ms : getMilestones(repository)) { if (ms.name.equalsIgnoreCase(milestone)) { + TicketMilestone tm = DeepCopier.copy(ms); String q = QueryBuilder.q(Lucene.rid.matches(repository.getRID())).and(Lucene.milestone.matches(milestone)).build(); - ms.tickets = indexer.queryFor(q, 1, 0, Lucene.number.name(), true); - return ms; + tm.tickets = indexer.queryFor(q, 1, 0, Lucene.number.name(), true); + return tm; } } return null; @@ -639,6 +641,22 @@ * @since 1.4.0 */ public synchronized boolean renameMilestone(RepositoryModel repository, String oldName, String newName, String createdBy) { + return renameMilestone(repository, oldName, newName, createdBy, true); + } + + /** + * Renames a milestone. + * + * @param repository + * @param oldName + * @param newName + * @param createdBy + * @param notifyOpenTickets + * @return true if successful + * @since 1.6.0 + */ + public synchronized boolean renameMilestone(RepositoryModel repository, String oldName, + String newName, String createdBy, boolean notifyOpenTickets) { if (StringUtils.isEmpty(newName)) { throw new IllegalArgumentException("new milestone can not be empty!"); } @@ -651,7 +669,7 @@ config.setString(MILESTONE, newName, STATUS, milestone.status.name()); config.setString(MILESTONE, newName, COLOR, milestone.color); if (milestone.due != null) { - config.setString(MILESTONE, milestone.name, DUE, + config.setString(MILESTONE, newName, DUE, new SimpleDateFormat(DUE_DATE_PATTERN).format(milestone.due)); } config.save(); @@ -663,9 +681,13 @@ Change change = new Change(createdBy); change.setField(Field.milestone, newName); TicketModel ticket = updateTicket(repository, qr.number, change); - notifier.queueMailing(ticket); + if (notifyOpenTickets && ticket.isOpen()) { + notifier.queueMailing(ticket); + } } - notifier.sendAll(); + if (notifyOpenTickets) { + notifier.sendAll(); + } return true; } catch (IOException e) { @@ -688,11 +710,27 @@ * @since 1.4.0 */ public synchronized boolean deleteMilestone(RepositoryModel repository, String milestone, String createdBy) { + return deleteMilestone(repository, milestone, createdBy, true); + } + + /** + * Deletes a milestone. + * + * @param repository + * @param milestone + * @param createdBy + * @param notifyOpenTickets + * @return true if successful + * @since 1.6.0 + */ + public synchronized boolean deleteMilestone(RepositoryModel repository, String milestone, + String createdBy, boolean notifyOpenTickets) { if (StringUtils.isEmpty(milestone)) { throw new IllegalArgumentException("milestone can not be empty!"); } Repository db = null; try { + TicketMilestone tm = getMilestone(repository, milestone); db = repositoryManager.getRepository(repository.name); StoredConfig config = db.getConfig(); config.unsetSection(MILESTONE, milestone); @@ -700,6 +738,18 @@ milestonesCache.remove(repository.name); + TicketNotifier notifier = createNotifier(); + for (QueryResult qr : tm.tickets) { + Change change = new Change(createdBy); + change.setField(Field.milestone, ""); + TicketModel ticket = updateTicket(repository, qr.number, change); + if (notifyOpenTickets && ticket.isOpen()) { + notifier.queueMailing(ticket); + } + } + if (notifyOpenTickets) { + notifier.sendAll(); + } return true; } catch (IOException e) { log.error("failed to delete milestone " + milestone + " in " + repository, e); diff --git a/src/main/java/com/gitblit/tickets/QueryResult.java b/src/main/java/com/gitblit/tickets/QueryResult.java index 9f5d3a5..7a2b1ab 100644 --- a/src/main/java/com/gitblit/tickets/QueryResult.java +++ b/src/main/java/com/gitblit/tickets/QueryResult.java @@ -74,6 +74,14 @@ return type != null && Type.Proposal == type; } + public boolean isOpen() { + return !status.isClosed(); + } + + public boolean isClosed() { + return status.isClosed(); + } + public boolean isMerged() { return Status.Merged == status && !StringUtils.isEmpty(mergeSha); } diff --git a/src/main/java/com/gitblit/tickets/TicketLabel.java b/src/main/java/com/gitblit/tickets/TicketLabel.java index 686ce88..a7f0ebe 100644 --- a/src/main/java/com/gitblit/tickets/TicketLabel.java +++ b/src/main/java/com/gitblit/tickets/TicketLabel.java @@ -30,14 +30,17 @@ private static final long serialVersionUID = 1L; - public final String name; + public String name; public String color; public List<QueryResult> tickets; - public TicketLabel(String name) { + setName(name); + } + + public void setName(String name) { this.name = name; this.color = StringUtils.getColor(name); } diff --git a/src/main/java/com/gitblit/tickets/TicketMilestone.java b/src/main/java/com/gitblit/tickets/TicketMilestone.java index c6b4fcc..dacedda 100644 --- a/src/main/java/com/gitblit/tickets/TicketMilestone.java +++ b/src/main/java/com/gitblit/tickets/TicketMilestone.java @@ -38,6 +38,18 @@ status = Status.Open; } + public boolean isOpen() { + return status == Status.Open; + } + + public boolean isOverdue() { + return due == null ? false : System.currentTimeMillis() > due.getTime(); + } + + public void setDue(Date due) { + this.due = due; + } + public int getProgress() { int total = getTotalTickets(); if (total == 0) { diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.java b/src/main/java/com/gitblit/wicket/GitBlitWebApp.java index c4fdeda..d4c1bc4 100644 --- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.java +++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.java @@ -51,6 +51,7 @@ import com.gitblit.wicket.pages.ComparePage; import com.gitblit.wicket.pages.DocPage; import com.gitblit.wicket.pages.DocsPage; +import com.gitblit.wicket.pages.EditMilestonePage; import com.gitblit.wicket.pages.EditTicketPage; import com.gitblit.wicket.pages.ExportTicketPage; import com.gitblit.wicket.pages.FederationRegistrationPage; @@ -63,6 +64,7 @@ import com.gitblit.wicket.pages.LuceneSearchPage; import com.gitblit.wicket.pages.MetricsPage; import com.gitblit.wicket.pages.MyDashboardPage; +import com.gitblit.wicket.pages.NewMilestonePage; import com.gitblit.wicket.pages.NewTicketPage; import com.gitblit.wicket.pages.OverviewPage; import com.gitblit.wicket.pages.PatchPage; @@ -187,6 +189,8 @@ mount("/tickets/new", NewTicketPage.class, "r"); mount("/tickets/edit", EditTicketPage.class, "r", "h"); mount("/tickets/export", ExportTicketPage.class, "r", "h"); + mount("/milestones/new", NewMilestonePage.class, "r"); + mount("/milestones/edit", EditMilestonePage.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 aeb2d9e..1394890 100644 --- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties +++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties @@ -671,4 +671,10 @@ gb.ticketIsClosed = This ticket is closed. 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. \ No newline at end of file +gb.youDoNotHaveClonePermission = You are not permitted to clone this repository. +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 diff --git a/src/main/java/com/gitblit/wicket/pages/EditMilestonePage.html b/src/main/java/com/gitblit/wicket/pages/EditMilestonePage.html new file mode 100644 index 0000000..0897ebe --- /dev/null +++ b/src/main/java/com/gitblit/wicket/pages/EditMilestonePage.html @@ -0,0 +1,39 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd" + xml:lang="en" + lang="en"> + +<wicket:extend> +<body onload="document.getElementById('name').focus();"> + +<div class="container"> + <!-- page header --> + <div class="title" style="font-size: 22px; color: rgb(0, 32, 96);padding: 3px 0px 7px;"> + <span class="project"><wicket:message key="gb.editMilestone"></wicket:message></span> + </div> + + <form style="padding-top:5px;" wicket:id="editForm"> + <div class="row"> + <div class="span12"> + <!-- Edit Milestone Table --> + <table class="ticket"> + <tr><th><wicket:message key="gb.milestone"></wicket:message></th><td class="edit"><input class="input-large" type="text" wicket:id="name" id="name"></input></td></tr> + <tr><th><wicket:message key="gb.due"></wicket:message></th><td class="edit"><input class="input-large" type="text" wicket:id="due"></input> <span class="help-inline" wicket:id="dueFormat"></span></td></tr> + <tr><th><wicket:message key="gb.status"></wicket:message><span style="color:red;">*</span></th><td class="edit"><select class="input-large" wicket:id="status"></select></td></tr> + <tr><th></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="notify" /> <span class="help-inline"><wicket:message key="gb.notifyChangedOpenTickets"></wicket:message></span></label></td></tr> + </table> + </div> + </div> + + <div class="row"> + <div class="span12"> + <div class="form-actions"><input class="btn btn-appmenu" type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" /> <input class="btn" type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" /> <input class="btn btn-danger" type="submit" value="Delete" wicket:message="value:gb.delete" wicket:id="delete" /></div> + </div> + </div> + </form> +</div> +</body> + +</wicket:extend> +</html> \ No newline at end of file diff --git a/src/main/java/com/gitblit/wicket/pages/EditMilestonePage.java b/src/main/java/com/gitblit/wicket/pages/EditMilestonePage.java new file mode 100644 index 0000000..b844442 --- /dev/null +++ b/src/main/java/com/gitblit/wicket/pages/EditMilestonePage.java @@ -0,0 +1,189 @@ +/* + * 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.Arrays; +import java.util.Date; +import java.util.List; + +import org.apache.wicket.PageParameters; +import org.apache.wicket.RestartResponseException; +import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.ajax.markup.html.form.AjaxButton; +import org.apache.wicket.extensions.markup.html.form.DateTextField; +import org.apache.wicket.markup.html.basic.Label; +import org.apache.wicket.markup.html.form.Button; +import org.apache.wicket.markup.html.form.CheckBox; +import org.apache.wicket.markup.html.form.DropDownChoice; +import org.apache.wicket.markup.html.form.Form; +import org.apache.wicket.markup.html.form.TextField; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.Model; + +import com.gitblit.models.RepositoryModel; +import com.gitblit.models.TicketModel; +import com.gitblit.models.TicketModel.Status; +import com.gitblit.models.UserModel; +import com.gitblit.tickets.TicketMilestone; +import com.gitblit.utils.StringUtils; +import com.gitblit.wicket.GitBlitWebSession; +import com.gitblit.wicket.WicketUtils; + +/** + * Page for creating a new milestone. + * + * @author James Moger + * + */ +public class EditMilestonePage extends RepositoryPage { + + private final String oldName; + + private IModel<String> nameModel; + + private IModel<Date> dueModel; + + private IModel<Status> statusModel; + + private IModel<Boolean> notificationModel; + + public EditMilestonePage(PageParameters params) { + super(params); + + RepositoryModel model = getRepositoryModel(); + if (!app().tickets().isAcceptingTicketUpdates(model)) { + // ticket service is read-only + throw new RestartResponseException(TicketsPage.class, WicketUtils.newRepositoryParameter(repositoryName)); + } + + UserModel currentUser = GitBlitWebSession.get().getUser(); + if (currentUser == null) { + currentUser = UserModel.ANONYMOUS; + } + + if (!currentUser.isAuthenticated || !currentUser.canAdmin(model)) { + // administration prohibited + throw new RestartResponseException(TicketsPage.class, WicketUtils.newRepositoryParameter(repositoryName)); + } + + oldName = WicketUtils.getObject(params); + if (StringUtils.isEmpty(oldName)) { + // milestone not specified + throw new RestartResponseException(TicketsPage.class, WicketUtils.newRepositoryParameter(repositoryName)); + } + + TicketMilestone tm = app().tickets().getMilestone(getRepositoryModel(), oldName); + if (tm == null) { + // milestone does not exist + throw new RestartResponseException(TicketsPage.class, WicketUtils.newRepositoryParameter(repositoryName)); + } + + setStatelessHint(false); + setOutputMarkupId(true); + + Form<Void> form = new Form<Void>("editForm"); + add(form); + + nameModel = Model.of(tm.name); + dueModel = Model.of(tm.due); + statusModel = Model.of(tm.status); + notificationModel = Model.of(true); + + form.add(new TextField<String>("name", nameModel)); + form.add(new DateTextField("due", dueModel, "yyyy-MM-dd")); + form.add(new Label("dueFormat", "yyyy-MM-dd")); + form.add(new CheckBox("notify", notificationModel)); + + List<Status> statusChoices = Arrays.asList(Status.Open, Status.Closed); + form.add(new DropDownChoice<TicketModel.Status>("status", statusModel, statusChoices)); + + form.add(new AjaxButton("save") { + + private static final long serialVersionUID = 1L; + + @Override + protected void onSubmit(AjaxRequestTarget target, Form<?> form) { + String name = nameModel.getObject(); + if (StringUtils.isEmpty(name)) { + return; + } + + Date due = dueModel.getObject(); + Status status = statusModel.getObject(); + boolean rename = !name.equals(oldName); + boolean notify = notificationModel.getObject(); + + UserModel currentUser = GitBlitWebSession.get().getUser(); + String createdBy = currentUser.username; + + TicketMilestone tm = app().tickets().getMilestone(getRepositoryModel(), oldName); + tm.setName(name); + tm.setDue(due); + tm.status = status; + + boolean success = true; + if (rename) { + success = app().tickets().renameMilestone(getRepositoryModel(), oldName, name, createdBy, notify); + } + + if (success && app().tickets().updateMilestone(getRepositoryModel(), tm, createdBy)) { + setResponsePage(TicketsPage.class, WicketUtils.newRepositoryParameter(getRepositoryModel().name)); + } else { + // TODO error + } + } + }); + Button cancel = new Button("cancel") { + private static final long serialVersionUID = 1L; + + @Override + public void onSubmit() { + setResponsePage(TicketsPage.class, WicketUtils.newRepositoryParameter(repositoryName)); + } + }; + cancel.setDefaultFormProcessing(false); + form.add(cancel); + + Button delete = new Button("delete") { + private static final long serialVersionUID = 1L; + + @Override + public void onSubmit() { + UserModel currentUser = GitBlitWebSession.get().getUser(); + String createdBy = currentUser.username; + boolean notify = notificationModel.getObject(); + + if (app().tickets().deleteMilestone(getRepositoryModel(), oldName, createdBy, notify)) { + setResponsePage(TicketsPage.class, WicketUtils.newRepositoryParameter(repositoryName)); + } else { + // TODO error processing + } + } + }; + delete.setDefaultFormProcessing(false); + form.add(delete); + } + + @Override + protected String getPageName() { + return getString("gb.editMilestone"); + } + + @Override + protected Class<? extends BasePage> getRepoNavPageClass() { + return TicketsPage.class; + } +} diff --git a/src/main/java/com/gitblit/wicket/pages/NewMilestonePage.html b/src/main/java/com/gitblit/wicket/pages/NewMilestonePage.html new file mode 100644 index 0000000..2ba5d5c --- /dev/null +++ b/src/main/java/com/gitblit/wicket/pages/NewMilestonePage.html @@ -0,0 +1,37 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd" + xml:lang="en" + lang="en"> + +<wicket:extend> +<body onload="document.getElementById('name').focus();"> + +<div class="container"> + <!-- page header --> + <div class="title" style="font-size: 22px; color: rgb(0, 32, 96);padding: 3px 0px 7px;"> + <span class="project"><wicket:message key="gb.newMilestone"></wicket:message></span> + </div> + + <form style="padding-top:5px;" wicket:id="editForm"> + <div class="row"> + <div class="span12"> + <!-- New Milestone Table --> + <table class="ticket"> + <tr><th><wicket:message key="gb.milestone"></wicket:message></th><td class="edit"><input class="input-large" type="text" wicket:id="name" id="name"></input></td></tr> + <tr><th><wicket:message key="gb.due"></wicket:message></th><td class="edit"><input class="input-large" type="text" wicket:id="due"></input> <span class="help-inline" wicket:id="dueFormat"></span></td></tr> + </table> + </div> + </div> + + <div class="row"> + <div class="span12"> + <div class="form-actions"><input class="btn btn-appmenu" type="submit" value="Create" wicket:message="value:gb.create" wicket:id="create" /> <input class="btn" type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" /></div> + </div> + </div> + </form> +</div> +</body> + +</wicket:extend> +</html> \ No newline at end of file diff --git a/src/main/java/com/gitblit/wicket/pages/NewMilestonePage.java b/src/main/java/com/gitblit/wicket/pages/NewMilestonePage.java new file mode 100644 index 0000000..a9f76d3 --- /dev/null +++ b/src/main/java/com/gitblit/wicket/pages/NewMilestonePage.java @@ -0,0 +1,140 @@ +/* + * 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.Date; + +import org.apache.wicket.PageParameters; +import org.apache.wicket.RestartResponseException; +import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.ajax.markup.html.form.AjaxButton; +import org.apache.wicket.extensions.markup.html.form.DateTextField; +import org.apache.wicket.markup.html.basic.Label; +import org.apache.wicket.markup.html.form.Button; +import org.apache.wicket.markup.html.form.Form; +import org.apache.wicket.markup.html.form.TextField; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.Model; + +import com.gitblit.models.RepositoryModel; +import com.gitblit.models.UserModel; +import com.gitblit.tickets.TicketMilestone; +import com.gitblit.utils.StringUtils; +import com.gitblit.utils.TimeUtils; +import com.gitblit.wicket.GitBlitWebSession; +import com.gitblit.wicket.WicketUtils; + +/** + * Page for creating a new milestone. + * + * @author James Moger + * + */ +public class NewMilestonePage extends RepositoryPage { + + private IModel<String> nameModel; + + private IModel<Date> dueModel; + + public NewMilestonePage(PageParameters params) { + super(params); + + RepositoryModel model = getRepositoryModel(); + if (!app().tickets().isAcceptingTicketUpdates(model)) { + // ticket service is read-only + throw new RestartResponseException(TicketsPage.class, WicketUtils.newRepositoryParameter(repositoryName)); + } + + UserModel currentUser = GitBlitWebSession.get().getUser(); + if (currentUser == null) { + currentUser = UserModel.ANONYMOUS; + } + + if (!currentUser.isAuthenticated || !currentUser.canAdmin(model)) { + // administration prohibited + throw new RestartResponseException(TicketsPage.class, WicketUtils.newRepositoryParameter(repositoryName)); + } + + setStatelessHint(false); + setOutputMarkupId(true); + + Form<Void> form = new Form<Void>("editForm"); + add(form); + + nameModel = Model.of(""); + dueModel = Model.of(new Date(System.currentTimeMillis() + TimeUtils.ONEDAY)); + + form.add(new TextField<String>("name", nameModel)); + form.add(new DateTextField("due", dueModel, "yyyy-MM-dd")); + form.add(new Label("dueFormat", "yyyy-MM-dd")); + + form.add(new AjaxButton("create") { + + private static final long serialVersionUID = 1L; + + @Override + protected void onSubmit(AjaxRequestTarget target, Form<?> form) { + String name = nameModel.getObject(); + if (StringUtils.isEmpty(name)) { + // invalid name + return; + } + + TicketMilestone milestone = app().tickets().getMilestone(getRepositoryModel(), name); + if (milestone != null) { + // milestone already exists + return; + } + + Date due = dueModel.getObject(); + + UserModel currentUser = GitBlitWebSession.get().getUser(); + String createdBy = currentUser.username; + + milestone = app().tickets().createMilestone(getRepositoryModel(), name, createdBy); + if (milestone != null) { + milestone.due = due; + app().tickets().updateMilestone(getRepositoryModel(), milestone, createdBy); + throw new RestartResponseException(TicketsPage.class, WicketUtils.newRepositoryParameter(getRepositoryModel().name)); + } else { + // TODO error + } + } + }); + + Button cancel = new Button("cancel") { + private static final long serialVersionUID = 1L; + + @Override + public void onSubmit() { + setResponsePage(TicketsPage.class, WicketUtils.newRepositoryParameter(repositoryName)); + } + }; + cancel.setDefaultFormProcessing(false); + form.add(cancel); + + } + + @Override + protected String getPageName() { + return getString("gb.newMilestone"); + } + + @Override + protected Class<? extends BasePage> getRepoNavPageClass() { + return TicketsPage.class; + } +} diff --git a/src/main/java/com/gitblit/wicket/pages/TicketsPage.html b/src/main/java/com/gitblit/wicket/pages/TicketsPage.html index 7d13852..a40d312 100644 --- a/src/main/java/com/gitblit/wicket/pages/TicketsPage.html +++ b/src/main/java/com/gitblit/wicket/pages/TicketsPage.html @@ -139,14 +139,51 @@ </div> <div class="tab-pane" id="milestones"> <div class="row"> - <div class="span9" wicket:id="milestoneList" style="padding-bottom: 10px;"> - <h3><span wicket:id="milestoneName"></span> <small><span wicket:id="milestoneState"></span></small></h3> - <i style="color:#888;"class="fa fa-calendar"></i> <span wicket:id="milestoneDue"></span> + <span class="span12" style="padding-bottom:10px;" wicket:id="newMilestone"></span> + </div> + + <div class="row"> + <div class="span12"><h2><wicket:message key="gb.openMilestones"></wicket:message></h2></div> + <div class="span12"><hr/></div> + </div> + <div class="row"> + <div class="span4" wicket:id="openMilestonesList" style="padding-bottom: 20px;"> + <div wicket:id="entryPanel"></div> </div> </div> + + <div class="row"> + <div class="span12"><h2><wicket:message key="gb.closedMilestones"></wicket:message></h2></div> + <div class="span12"><hr/></div> + </div> + <div class="row"> + <div class="span4" wicket:id="closedMilestonesList" style="padding-bottom: 15px;"> + <div wicket:id="entryPanel"></div> + </div> + </div> + </div> </div> +<wicket:fragment wicket:id="milestoneListFragment"> + <h3><span wicket:id="milestoneName"></span> <small><span wicket:id="milestoneState"></span></small></h3> + <i style="color:#888;"class="fa fa-calendar"></i> <span wicket:id="milestoneDue"></span> <span wicket:id="editMilestone"></span> + <div wicket:id="milestonePanel"></div> +</wicket:fragment> + +<wicket:fragment wicket:id="openMilestoneFragment"> + <div style="clear:both;padding-bottom: 10px;"> + <div style="margin-bottom: 5px;" class="progress progress-success"> + <div class="bar" wicket:id="progress"></div> + </div> + <div class="milestoneOverview"> + <span wicket:id="openTickets" />, + <span wicket:id="closedTickets" />, + <span wicket:id="totalTickets" /> + </div> + </div> +</wicket:fragment> + <wicket:fragment wicket:id="noMilestoneFragment"> <table style="width: 100%;padding-bottom: 5px;"> <tbody> diff --git a/src/main/java/com/gitblit/wicket/pages/TicketsPage.java b/src/main/java/com/gitblit/wicket/pages/TicketsPage.java index ca509e2..5973d47 100644 --- a/src/main/java/com/gitblit/wicket/pages/TicketsPage.java +++ b/src/main/java/com/gitblit/wicket/pages/TicketsPage.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Set; import java.util.TreeSet; @@ -42,6 +43,7 @@ import com.gitblit.Constants.AccessPermission; import com.gitblit.Keys; import com.gitblit.models.RegistrantAccessPermission; +import com.gitblit.models.RepositoryModel; import com.gitblit.models.TicketModel; import com.gitblit.models.TicketModel.Status; import com.gitblit.models.UserModel; @@ -646,38 +648,120 @@ }; add(ticketsView); - List<TicketMilestone> allMilestones = app().tickets().getMilestones(getRepositoryModel()); - ListDataProvider<TicketMilestone> allMilestonesDp = new ListDataProvider<TicketMilestone>(allMilestones); - DataView<TicketMilestone> milestonesList = new DataView<TicketMilestone>("milestoneList", allMilestonesDp) { + // new milestone link + RepositoryModel repositoryModel = getRepositoryModel(); + final boolean acceptingUpdates = app().tickets().isAcceptingTicketUpdates(repositoryModel) + && user != null && user.canAdmin(getRepositoryModel()); + if (acceptingUpdates) { + add(new LinkPanel("newMilestone", null, getString("gb.newMilestone"), + NewMilestonePage.class, WicketUtils.newRepositoryParameter(repositoryName))); + } else { + add(new Label("newMilestone").setVisible(false)); + } + + // milestones list + List<TicketMilestone> openMilestones = new ArrayList<TicketMilestone>(); + List<TicketMilestone> closedMilestones = new ArrayList<TicketMilestone>(); + for (TicketMilestone milestone : app().tickets().getMilestones(repositoryModel)) { + if (milestone.isOpen()) { + openMilestones.add(milestone); + } else { + closedMilestones.add(milestone); + } + } + Collections.sort(openMilestones, new Comparator<TicketMilestone>() { + @Override + public int compare(TicketMilestone o1, TicketMilestone o2) { + return o2.due.compareTo(o1.due); + } + }); + + Collections.sort(closedMilestones, new Comparator<TicketMilestone>() { + @Override + public int compare(TicketMilestone o1, TicketMilestone o2) { + return o2.due.compareTo(o1.due); + } + }); + + DataView<TicketMilestone> openMilestonesList = milestoneList("openMilestonesList", openMilestones, acceptingUpdates); + add(openMilestonesList); + + DataView<TicketMilestone> closedMilestonesList = milestoneList("closedMilestonesList", closedMilestones, acceptingUpdates); + add(closedMilestonesList); + } + + protected DataView<TicketMilestone> milestoneList(String wicketId, List<TicketMilestone> milestones, final boolean acceptingUpdates) { + ListDataProvider<TicketMilestone> milestonesDp = new ListDataProvider<TicketMilestone>(milestones); + DataView<TicketMilestone> milestonesList = new DataView<TicketMilestone>(wicketId, milestonesDp) { private static final long serialVersionUID = 1L; @Override public void populateItem(final Item<TicketMilestone> item) { + Fragment entryPanel = new Fragment("entryPanel", "milestoneListFragment", this); + item.add(entryPanel); + final TicketMilestone tm = item.getModelObject(); - PageParameters params = queryParameters(null, tm.name, null, null, null, desc, 1); - item.add(new LinkPanel("milestoneName", null, tm.name, TicketsPage.class, params).setRenderBodyOnly(true)); + PageParameters params = queryParameters(null, tm.name, null, null, null, true, 1); + entryPanel.add(new LinkPanel("milestoneName", null, tm.name, TicketsPage.class, params).setRenderBodyOnly(true)); String css; + String status = tm.status.name(); switch (tm.status) { case Open: - css = "aui-lozenge aui-lozenge-subtle"; + if (tm.isOverdue()) { + css = "aui-lozenge aui-lozenge-subtle aui-lozenge-error"; + status = "overdue"; + } else { + css = "aui-lozenge aui-lozenge-subtle"; + } break; default: css = "aui-lozenge"; break; } - Label stateLabel = new Label("milestoneState", tm.status.name()); + Label stateLabel = new Label("milestoneState", status); WicketUtils.setCssClass(stateLabel, css); - item.add(stateLabel); + entryPanel.add(stateLabel); if (tm.due == null) { - item.add(new Label("milestoneDue", getString("gb.notSpecified"))); + entryPanel.add(new Label("milestoneDue", getString("gb.notSpecified"))); } else { - item.add(WicketUtils.createDatestampLabel("milestoneDue", tm.due, getTimeZone(), getTimeUtils())); + entryPanel.add(WicketUtils.createDatestampLabel("milestoneDue", tm.due, getTimeZone(), getTimeUtils())); + } + if (acceptingUpdates) { + entryPanel.add(new LinkPanel("editMilestone", null, getString("gb.edit"), EditMilestonePage.class, + WicketUtils.newObjectParameter(repositoryName, tm.name))); + } else { + entryPanel.add(new Label("editMilestone").setVisible(false)); + } + + if (tm.isOpen()) { + // re-load milestone with query results + TicketMilestone m = app().tickets().getMilestone(getRepositoryModel(), tm.name); + + Fragment milestonePanel = new Fragment("milestonePanel", "openMilestoneFragment", this); + Label label = new Label("progress"); + WicketUtils.setCssStyle(label, "width:" + tm.getProgress() + "%;"); + milestonePanel.add(label); + + milestonePanel.add(new LinkPanel("openTickets", null, + MessageFormat.format(getString("gb.nOpenTickets"), m.getOpenTickets()), + TicketsPage.class, + queryParameters(null, tm.name, 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))); + + milestonePanel.add(new Label("totalTickets", MessageFormat.format(getString("gb.nTotalTickets"), m.getTotalTickets()))); + entryPanel.add(milestonePanel); + } else { + entryPanel.add(new Label("milestonePanel").setVisible(false)); } } }; - add(milestonesList); + return milestonesList; } protected PageParameters queryParameters( -- Gitblit v1.9.1