--- app/assets/stylesheets/less/_common.less
+++ app/assets/stylesheets/less/_common.less
... | ... | @@ -114,6 +114,8 @@ |
114 | 114 |
filter:none; |
115 | 115 |
&.orange { background:@orange; .box-shadow(none); } // #f28149 |
116 | 116 |
&.blue { background:@blue; .box-shadow(none); } |
117 |
+ &.grey { background:@gray-9e; .box-shadow(none); } |
|
118 |
+ &.done { background:@light-green; .box-shadow(none); } |
|
117 | 119 |
&.open { background:@state-open; .box-shadow(none); } |
118 | 120 |
&.closed { background:@state-closed; .box-shadow(none); } |
119 | 121 |
} |
--- app/assets/stylesheets/less/_page.less
+++ app/assets/stylesheets/less/_page.less
... | ... | @@ -2134,43 +2134,7 @@ |
2134 | 2134 |
background: #fd6956 !important; |
2135 | 2135 |
} |
2136 | 2136 |
|
2137 |
- .state-label { |
|
2138 |
- font-size: 13px; |
|
2139 |
- padding: 5px 0 3px; |
|
2140 |
- display: inline-block; |
|
2141 |
- min-width: 13px; |
|
2142 |
- color: white; |
|
2143 |
- border-radius: 5px; |
|
2144 |
- text-align: center; |
|
2145 |
- line-height: 14px; |
|
2146 |
- margin-right: 3px; |
|
2147 |
- |
|
2148 |
- &.open { |
|
2149 |
- color: white; |
|
2150 |
- } |
|
2151 |
- &.closed { |
|
2152 |
- color: #2196F3; |
|
2153 |
- } |
|
2154 |
- } |
|
2155 |
- |
|
2156 |
- .issue-item { |
|
2157 |
- font-size: 16px; |
|
2158 |
- padding: 3px; |
|
2159 |
- .item-name { |
|
2160 |
- background-color: white; |
|
2161 |
- vertical-align: middle; |
|
2162 |
- } |
|
2163 |
- .number { |
|
2164 |
- font-family: menlo, Consolas, monospace, sans-serif; |
|
2165 |
- font-size: 12px; |
|
2166 |
- color: gray; |
|
2167 |
- min-width: 35px; |
|
2168 |
- padding-right: 5px; |
|
2169 |
- display: inline-block; |
|
2170 |
- text-align: right; |
|
2171 |
- margin-right: 3px; |
|
2172 |
- } |
|
2173 |
- } |
|
2137 |
+ #simple-issue-list; |
|
2174 | 2138 |
|
2175 | 2139 |
.infos { |
2176 | 2140 |
width: 100%; /*700px*; /*660px;*/ |
... | ... | @@ -3450,7 +3414,9 @@ |
3450 | 3414 |
.title { |
3451 | 3415 |
margin-bottom: 15px; |
3452 | 3416 |
font-size:18px; |
3453 |
- |
|
3417 |
+ border: none; |
|
3418 |
+ border-bottom: 1px solid #ddd; |
|
3419 |
+ border-radius: 0 !important; |
|
3454 | 3420 |
} |
3455 | 3421 |
.error { |
3456 | 3422 |
margin-bottom: 5px; |
... | ... | @@ -6658,3 +6624,101 @@ |
6658 | 6624 |
color: darkviolet; |
6659 | 6625 |
font-weight: bold; |
6660 | 6626 |
} |
6627 |
+ |
|
6628 |
+.width16px { |
|
6629 |
+ width: 16px; |
|
6630 |
+ display: inline-block; |
|
6631 |
+} |
|
6632 |
+ |
|
6633 |
+#simple-issue-list { |
|
6634 |
+ .state-label { |
|
6635 |
+ font-size: 13px; |
|
6636 |
+ padding: 5px 0 3px; |
|
6637 |
+ display: inline-block; |
|
6638 |
+ min-width: 13px; |
|
6639 |
+ color: white; |
|
6640 |
+ border-radius: 5px; |
|
6641 |
+ text-align: center; |
|
6642 |
+ line-height: 14px; |
|
6643 |
+ margin-right: 3px; |
|
6644 |
+ |
|
6645 |
+ &.open { |
|
6646 |
+ color: white; |
|
6647 |
+ } |
|
6648 |
+ &.closed { |
|
6649 |
+ color: #fd6956; |
|
6650 |
+ } |
|
6651 |
+ } |
|
6652 |
+ |
|
6653 |
+ .issue-item { |
|
6654 |
+ font-size: 16px; |
|
6655 |
+ padding: 3px; |
|
6656 |
+ .item-name { |
|
6657 |
+ background-color: white; |
|
6658 |
+ vertical-align: middle; |
|
6659 |
+ } |
|
6660 |
+ .number { |
|
6661 |
+ font-family: menlo, Consolas, monospace, sans-serif; |
|
6662 |
+ font-size: 12px; |
|
6663 |
+ color: gray; |
|
6664 |
+ min-width: 35px; |
|
6665 |
+ padding-right: 5px; |
|
6666 |
+ display: inline-block; |
|
6667 |
+ text-align: right; |
|
6668 |
+ margin-right: 3px; |
|
6669 |
+ } |
|
6670 |
+ } |
|
6671 |
+} |
|
6672 |
+ |
|
6673 |
+.subtasks { |
|
6674 |
+ #simple-issue-list; |
|
6675 |
+ margin-bottom: 15px; |
|
6676 |
+} |
|
6677 |
+ |
|
6678 |
+.subtask { |
|
6679 |
+ a { |
|
6680 |
+ border: 1px solid; |
|
6681 |
+ padding: 0 2px; |
|
6682 |
+ &:hover { |
|
6683 |
+ color: #51aacc; |
|
6684 |
+ text-decoration: none; |
|
6685 |
+ } |
|
6686 |
+ } |
|
6687 |
+ color: #9e9e9e; |
|
6688 |
+} |
|
6689 |
+ |
|
6690 |
+.infos, .parent-issue { |
|
6691 |
+ .upload-progress { |
|
6692 |
+ display: inline-block; |
|
6693 |
+ width: 30px; |
|
6694 |
+ vertical-align: middle; |
|
6695 |
+ overflow: hidden; |
|
6696 |
+ margin-top: 3px; |
|
6697 |
+ box-shadow: none; |
|
6698 |
+ } |
|
6699 |
+ .completion-ratio { |
|
6700 |
+ margin-right: 3px; |
|
6701 |
+ } |
|
6702 |
+ .open { |
|
6703 |
+ background-color: #8BC34A; |
|
6704 |
+ } |
|
6705 |
+ .closed { |
|
6706 |
+ background-color: #f68c52; |
|
6707 |
+ } |
|
6708 |
+ .parent-issue-state { |
|
6709 |
+ border-radius: 0; |
|
6710 |
+ color: white; |
|
6711 |
+ padding: 2px; |
|
6712 |
+ font-size: 12px; |
|
6713 |
+ line-height: 17px; |
|
6714 |
+ vertical-align: text-bottom; |
|
6715 |
+ } |
|
6716 |
+} |
|
6717 |
+ |
|
6718 |
+.txt-orange { |
|
6719 |
+ color: #f68c52; |
|
6720 |
+} |
|
6721 |
+ |
|
6722 |
+.txt-green { |
|
6723 |
+ color: @light-green; |
|
6724 |
+} |
--- app/assets/stylesheets/less/_variables.less
+++ app/assets/stylesheets/less/_variables.less
... | ... | @@ -22,11 +22,14 @@ |
22 | 22 |
@low-white: #efefef; |
23 | 23 |
@black : #000; |
24 | 24 |
@orange : #F36C22; |
25 |
+@orangeLighter : #f68c52; |
|
25 | 26 |
@purple : #8d008d; |
26 | 27 |
@blue : #5DBBE0; |
27 | 28 |
@blue2 : #51AACC; |
28 | 29 |
@darkgray: #878787; |
29 | 30 |
@green: #007fca; |
31 |
+@light-green: #8bc34a; |
|
32 |
+@light-cyan: #00BCD4; |
|
30 | 33 |
@darkmagenta: #8B008B; |
31 | 34 |
|
32 | 35 |
@gray-d4: #D4D4D4; |
... | ... | @@ -42,6 +45,7 @@ |
42 | 45 |
@gray-52: #5D5D5D; |
43 | 46 |
@gray-f2: #F2F2F2; |
44 | 47 |
@gray-d9: #D9D9D9; |
48 |
+@gray-9e: #9E9E9E; |
|
45 | 49 |
|
46 | 50 |
// new color scheme |
47 | 51 |
|
--- app/controllers/IssueApp.java
+++ app/controllers/IssueApp.java
... | ... | @@ -417,6 +417,9 @@ |
417 | 417 |
ErrorViews.RequestTextEntityTooLarge.render()); |
418 | 418 |
} |
419 | 419 |
|
420 |
+ if(StringUtils.isNotEmpty(newIssue.parentIssueId)){ |
|
421 |
+ newIssue.parent = Issue.finder.byId(Long.valueOf(newIssue.parentIssueId)); |
|
422 |
+ } |
|
420 | 423 |
newIssue.createdDate = JodaDateUtil.now(); |
421 | 424 |
newIssue.updatedDate = JodaDateUtil.now(); |
422 | 425 |
newIssue.setAuthor(UserApp.currentUser()); |
... | ... | @@ -511,6 +514,7 @@ |
511 | 514 |
Project project = Project.findByOwnerAndProjectName(ownerName, projectName); |
512 | 515 |
|
513 | 516 |
if (issueForm.hasErrors()) { |
517 |
+ flash(Constants.WARNING, issueForm.error("name").message()); |
|
514 | 518 |
return badRequest(edit.render("error.validation", issueForm, Issue.findByNumber(project, number), project)); |
515 | 519 |
} |
516 | 520 |
|
... | ... | @@ -521,6 +525,7 @@ |
521 | 525 |
issue.dueDate = JodaDateUtil.lastSecondOfDay(issue.dueDate); |
522 | 526 |
|
523 | 527 |
final Issue originalIssue = Issue.findByNumber(project, number); |
528 |
+ updateSubtaskRelation(issue, originalIssue); |
|
524 | 529 |
|
525 | 530 |
Call redirectTo = routes.IssueApp.issue(project.owner, project.name, number); |
526 | 531 |
|
... | ... | @@ -547,6 +552,16 @@ |
547 | 552 |
return editPosting(originalIssue, issue, issueForm, redirectTo, preUpdateHook); |
548 | 553 |
} |
549 | 554 |
|
555 |
+ private static void updateSubtaskRelation(Issue issue, Issue originalIssue) { |
|
556 |
+ if(StringUtils.isEmpty(issue.parentIssueId)){ |
|
557 |
+ issue.parent = null; |
|
558 |
+ } else { |
|
559 |
+ issue.parent = Issue.finder.byId(Long.valueOf(issue.parentIssueId)); |
|
560 |
+ } |
|
561 |
+ originalIssue.parent = issue.parent; |
|
562 |
+ originalIssue.update(); |
|
563 |
+ } |
|
564 |
+ |
|
550 | 565 |
private static void setAssignee(Form<Issue> issueForm, Issue issue, Project project) { |
551 | 566 |
String value = issueForm.field("assignee.user.id").value(); |
552 | 567 |
if (value != null) { |
--- app/models/Issue.java
+++ app/models/Issue.java
... | ... | @@ -7,6 +7,7 @@ |
7 | 7 |
package models; |
8 | 8 |
|
9 | 9 |
import com.avaje.ebean.Ebean; |
10 |
+import com.avaje.ebean.ExpressionList; |
|
10 | 11 |
import com.avaje.ebean.Page; |
11 | 12 |
import com.avaje.ebean.annotation.Formula; |
12 | 13 |
import controllers.routes; |
... | ... | @@ -93,6 +94,12 @@ |
93 | 94 |
super(project, author, title, body); |
94 | 95 |
this.state = State.OPEN; |
95 | 96 |
} |
97 |
+ |
|
98 |
+ @Transient |
|
99 |
+ public String parentIssueId; |
|
100 |
+ |
|
101 |
+ @OneToOne |
|
102 |
+ public Issue parent; |
|
96 | 103 |
|
97 | 104 |
public Issue() { |
98 | 105 |
super(); |
... | ... | @@ -421,6 +428,24 @@ |
421 | 428 |
.ge("createdDate", JodaDateUtil.before(days)).order().desc("createdDate").findList(); |
422 | 429 |
} |
423 | 430 |
|
431 |
+ public static List<Issue> findByProject(Project project, String filter) { |
|
432 |
+ ExpressionList<Issue> el = finder.where() |
|
433 |
+ .eq("project.id", project.id); |
|
434 |
+ if(StringUtils.isNotEmpty(filter)){ |
|
435 |
+ el.icontains("title", filter); |
|
436 |
+ } |
|
437 |
+ return el.order().desc("createdDate").findList(); |
|
438 |
+ } |
|
439 |
+ |
|
440 |
+ public static List<Issue> findByProject(Project project, String filter, int limit) { |
|
441 |
+ ExpressionList<Issue> el = finder.where() |
|
442 |
+ .eq("project.id", project.id); |
|
443 |
+ if(StringUtils.isNotEmpty(filter)){ |
|
444 |
+ el.icontains("title", filter); |
|
445 |
+ } |
|
446 |
+ return el.setMaxRows(10).order().desc("createdDate").findList(); |
|
447 |
+ } |
|
448 |
+ |
|
424 | 449 |
public static Page<Issue> findIssuesByState(int size, int pageNum, State state) { |
425 | 450 |
return finder.where().eq("state", state) |
426 | 451 |
.order().desc("createdDate") |
... | ... | @@ -581,6 +606,31 @@ |
581 | 606 |
.findRowCount(); |
582 | 607 |
} |
583 | 608 |
|
609 |
+ public static List<Issue> findByParentIssueId(Long parentIssueId){ |
|
610 |
+ return finder.where() |
|
611 |
+ .eq("parent.id", parentIssueId) |
|
612 |
+ .findList(); |
|
613 |
+ } |
|
584 | 614 |
|
615 |
+ public boolean hasChildIssue(){ |
|
616 |
+ List<Issue> issues = finder.where() |
|
617 |
+ .eq("parent.id", this.id) |
|
618 |
+ .setFirstRow(1) |
|
619 |
+ .setMaxRows(1).findList(); |
|
620 |
+ return issues.size() > 0; |
|
621 |
+ } |
|
585 | 622 |
|
623 |
+ public static List<Issue> findByParentIssueIdAndState(Long parentIssueId, State state){ |
|
624 |
+ return finder.where() |
|
625 |
+ .eq("parent.id", parentIssueId) |
|
626 |
+ .eq("state", state) |
|
627 |
+ .findList(); |
|
628 |
+ } |
|
629 |
+ |
|
630 |
+ public static int countByParentIssueIdAndState(Long parentIssueId, State state){ |
|
631 |
+ return finder.where() |
|
632 |
+ .eq("parent.id", parentIssueId) |
|
633 |
+ .eq("state", state) |
|
634 |
+ .findRowCount(); |
|
635 |
+ } |
|
586 | 636 |
} |
--- app/models/Posting.java
+++ app/models/Posting.java
... | ... | @@ -68,6 +68,9 @@ |
68 | 68 |
return comments.size(); |
69 | 69 |
} |
70 | 70 |
|
71 |
+ @OneToOne |
|
72 |
+ public Posting parent; |
|
73 |
+ |
|
71 | 74 |
public Posting() { |
72 | 75 |
super(); |
73 | 76 |
} |
--- app/models/User.java
+++ app/models/User.java
... | ... | @@ -1,23 +1,9 @@ |
1 | 1 |
/** |
2 |
- * Yobi, Project Hosting SW |
|
3 |
- * |
|
4 |
- * Copyright 2012 NAVER Corp. |
|
5 |
- * http://yobi.io |
|
6 |
- * |
|
7 |
- * @author Ahn Hyeok Jun |
|
8 |
- * |
|
9 |
- * Licensed under the Apache License, Version 2.0 (the "License"); |
|
10 |
- * you may not use this file except in compliance with the License. |
|
11 |
- * You may obtain a copy of the License at |
|
12 |
- * |
|
13 |
- * http://www.apache.org/licenses/LICENSE-2.0 |
|
14 |
- * |
|
15 |
- * Unless required by applicable law or agreed to in writing, software |
|
16 |
- * distributed under the License is distributed on an "AS IS" BASIS, |
|
17 |
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
18 |
- * See the License for the specific language governing permissions and |
|
19 |
- * limitations under the License. |
|
20 |
- */ |
|
2 |
+ * Yona, 21st Century Project Hosting SW |
|
3 |
+ * <p> |
|
4 |
+ * Copyright Yona & Yobi Authors & NAVER Corp. |
|
5 |
+ * https://yona.io |
|
6 |
+ **/ |
|
21 | 7 |
package models; |
22 | 8 |
|
23 | 9 |
import com.avaje.ebean.*; |
--- app/views/common/select2.scala.html
+++ app/views/common/select2.scala.html
... | ... | @@ -52,9 +52,14 @@ |
52 | 52 |
</div> |
53 | 53 |
</script> |
54 | 54 |
<script id="tplSelect2ProjectsWithoutAvatar" type="text/x-jquery-tmpl"> |
55 |
-<div class="usf-group" title="${name}> |
|
56 |
- <span class="avatar-wrap smaller"> </span> |
|
55 |
+<div class="usf-group" title="${name}"> |
|
56 |
+ <span class="width16px"></span> |
|
57 | 57 |
<span class="name">${name}</span> |
58 | 58 |
</div> |
59 | 59 |
</script> |
60 | 60 |
|
61 |
+<script id="tplSelect2FormatIssues" type="text/x-jquery-tmpl"> |
|
62 |
+<div title="${name}"> |
|
63 |
+ ${name} |
|
64 |
+</div> |
|
65 |
+</script> |
--- app/views/issue/create.scala.html
+++ app/views/issue/create.scala.html
... | ... | @@ -5,6 +5,7 @@ |
5 | 5 |
* https://yona.io |
6 | 6 |
**@ |
7 | 7 |
@import play.data.Form |
8 |
+@import org.apache.commons.lang3.StringUtils |
|
8 | 9 |
@(title:String, issueForm: Form[Issue], project:Project, issueTemplate:String = "") |
9 | 10 |
@import helper._ |
10 | 11 |
@import scala.collection.mutable.Map |
... | ... | @@ -13,6 +14,10 @@ |
13 | 14 |
@import utils.AccessControl._ |
14 | 15 |
@import utils.TemplateHelper._ |
15 | 16 |
@import controllers.UserApp |
17 |
+ |
|
18 |
+@parentIssueId = @{ |
|
19 |
+ play.mvc.Http.Context.current().request().getQueryString("parentIssueId") |
|
20 |
+} |
|
16 | 21 |
|
17 | 22 |
@projectLayout(Messages(title), project, utils.MenuType.ISSUE) { |
18 | 23 |
@projectMenu(project, utils.MenuType.ISSUE, "main-menu-only") |
... | ... | @@ -23,12 +28,9 @@ |
23 | 28 |
<div class="row-fluid"> |
24 | 29 |
<div class="span12"> |
25 | 30 |
<dl> |
26 |
- <dt> |
|
27 |
- <label for="title">@Messages("title")</label> |
|
28 |
- </dt> |
|
29 | 31 |
<dd> |
30 | 32 |
@defining(issueForm.errors().get("title")) { errors => |
31 |
- <input type="text" id="title" name="title" value="" class="text title zen-mode @if(errors != null) {error}" maxlength="250" tabindex="1"> |
|
33 |
+ <input type="text" id="title" name="title" placeholder="@Messages("title")" value="" class="text title zen-mode @if(errors != null) {error}" maxlength="250" tabindex="1"> |
|
32 | 34 |
@if(errors != null) { |
33 | 35 |
<div class="message"> |
34 | 36 |
@for(error <- errors) { |
... | ... | @@ -36,6 +38,30 @@ |
36 | 38 |
} |
37 | 39 |
</div> |
38 | 40 |
} |
41 |
+ <div class="subtitle-message">+ Sub task</div> |
|
42 |
+ <div class="subtask-wrap"> |
|
43 |
+ <div class="span3"> |
|
44 |
+ <select id="projects" name="targetProjectName" data-format="projects" data-placeholder="@Messages("organization.choose.projects")" data-toggle="select2" data-container-css-class="fullsize" > |
|
45 |
+ <option value="@project.name" data-avatar-url="@urlToProjectLogo(project)">@project.name</option> |
|
46 |
+ @for(project <- UserApp.currentUser().myProjects("")) { |
|
47 |
+ <option value="@project.name" data-avatar-url="@urlToProjectLogo(project)">@project.name</option> |
|
48 |
+ } |
|
49 |
+ </select> |
|
50 |
+ </div> |
|
51 |
+ <div class="span8"> |
|
52 |
+ <select id="parentId" name="parentIssueId" data-format="issues" data-placeholder="@Messages("organization.choose.projects")" data-toggle="select2" data-container-css-class="fullsize" > |
|
53 |
+ <option disabled selected value> -- select an issue -- </option> |
|
54 |
+ @if(StringUtils.isNotEmpty(parentIssueId)){ |
|
55 |
+ @defining(Issue.finder.byId(Long.parseLong(parentIssueId))) { issue => |
|
56 |
+ <option value="@parentIssueId" selected>#@issue.getNumber. @issue.title</option> |
|
57 |
+ } |
|
58 |
+ } |
|
59 |
+ @for(issue <- Issue.findByProject(project, "", 1)) { |
|
60 |
+ <option value="@issue.id" @if(issue.id+"" == parentIssueId){selected}>#@issue.getNumber. @issue.title</option> |
|
61 |
+ } |
|
62 |
+ </select> |
|
63 |
+ </div> |
|
64 |
+ </div> |
|
39 | 65 |
} |
40 | 66 |
</dd> |
41 | 67 |
</dl> |
... | ... | @@ -60,7 +86,7 @@ |
60 | 86 |
--><a href="javascript:history.back();" class="ybtn">@Messages("button.cancel")</a> |
61 | 87 |
</div> |
62 | 88 |
</div> |
63 |
- <div class="span3 span-hard-wrap"> |
|
89 |
+ <div class="span3 span-hard-wrap right-menu"> |
|
64 | 90 |
@if(isProjectResourceCreatable(UserApp.currentUser(), project, ResourceType.ISSUE_ASSIGNEE)) { |
65 | 91 |
<dl class="issue-option"> |
66 | 92 |
<dt>@Messages("issue.assignee")</dt> |
--- app/views/issue/edit.scala.html
+++ app/views/issue/edit.scala.html
... | ... | @@ -1,24 +1,11 @@ |
1 | 1 |
@** |
2 |
-* Yobi, Project Hosting SW |
|
2 |
+* Yona, 21st Century Project Hosting SW |
|
3 | 3 |
* |
4 |
-* Copyright 2012 NAVER Corp. |
|
5 |
-* http://yobi.io |
|
6 |
-* |
|
7 |
-* @author park3251 |
|
8 |
-* |
|
9 |
-* Licensed under the Apache License, Version 2.0 (the "License"); |
|
10 |
-* you may not use this file except in compliance with the License. |
|
11 |
-* You may obtain a copy of the License at |
|
12 |
-* |
|
13 |
-* http://www.apache.org/licenses/LICENSE-2.0 |
|
14 |
-* |
|
15 |
-* Unless required by applicable law or agreed to in writing, software |
|
16 |
-* distributed under the License is distributed on an "AS IS" BASIS, |
|
17 |
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
18 |
-* See the License for the specific language governing permissions and |
|
19 |
-* limitations under the License. |
|
4 |
+* Copyright Yona & Yobi Authors & NAVER Corp. |
|
5 |
+* https://yona.io |
|
20 | 6 |
**@ |
21 |
-@(title:String, issueForm: play.data.Form[Issue], issue:Issue, project:Project) |
|
7 |
+@import play.data.Form |
|
8 |
+@(title:String, issueForm: Form[Issue], issue:Issue, project:Project) |
|
22 | 9 |
@import helper._ |
23 | 10 |
@import scala.collection.mutable.Map |
24 | 11 |
@import models.enumeration.ResourceType |
... | ... | @@ -26,10 +13,16 @@ |
26 | 13 |
@import models.enumeration._ |
27 | 14 |
@import utils.AccessControl._ |
28 | 15 |
@import utils.TemplateHelper._ |
16 |
+@parentIssueId = @{ |
|
17 |
+ if(issue.parent != null) { |
|
18 |
+ issue.parent.id |
|
19 |
+ } else { |
|
20 |
+ null |
|
21 |
+ } |
|
22 |
+} |
|
29 | 23 |
|
30 | 24 |
@projectLayout(Messages(title), project, utils.MenuType.ISSUE) { |
31 | 25 |
@projectMenu(project, utils.MenuType.ISSUE, "main-menu-only") |
32 |
- |
|
33 | 26 |
<div class="page-wrap-outer"> |
34 | 27 |
<div class="project-page-wrap"> |
35 | 28 |
<div class="content-wrap frm-wrap"> |
... | ... | @@ -52,6 +45,7 @@ |
52 | 45 |
} |
53 | 46 |
</div> |
54 | 47 |
} |
48 |
+ @partial_select_subtask(project, issue, parentIssueId) |
|
55 | 49 |
} |
56 | 50 |
</dd> |
57 | 51 |
</dl> |
--- app/views/issue/list.scala.html
+++ app/views/issue/list.scala.html
... | ... | @@ -1,25 +1,13 @@ |
1 | 1 |
@** |
2 |
-* Yobi, Project Hosting SW |
|
2 |
+* Yona, 21st Century Project Hosting SW |
|
3 | 3 |
* |
4 |
-* Copyright 2012 NAVER Corp. |
|
5 |
-* http://yobi.io |
|
6 |
-* |
|
7 |
-* @author Tae |
|
8 |
-* |
|
9 |
-* Licensed under the Apache License, Version 2.0 (the "License"); |
|
10 |
-* you may not use this file except in compliance with the License. |
|
11 |
-* You may obtain a copy of the License at |
|
12 |
-* |
|
13 |
-* http://www.apache.org/licenses/LICENSE-2.0 |
|
14 |
-* |
|
15 |
-* Unless required by applicable law or agreed to in writing, software |
|
16 |
-* distributed under the License is distributed on an "AS IS" BASIS, |
|
17 |
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
18 |
-* See the License for the specific language governing permissions and |
|
19 |
-* limitations under the License. |
|
4 |
+* Copyright Yona & Yobi Authors & NAVER Corp. |
|
5 |
+* https://yona.io |
|
20 | 6 |
**@ |
21 |
-@(title: String, currentPage: com.avaje.ebean.Page[Issue], param: |
|
22 |
-models.support.SearchCondition, project:Project) |
|
7 |
+@import com.avaje.ebean.Page |
|
8 |
+@import models.support.SearchCondition |
|
9 |
+@(title: String, currentPage: Page[Issue], param: |
|
10 |
+SearchCondition, project:Project) |
|
23 | 11 |
|
24 | 12 |
@projectLayout(Messages(title), project, utils.MenuType.ISSUE){ |
25 | 13 |
@projectMenu(project, utils.MenuType.ISSUE, "main-menu-only") |
--- app/views/issue/my_partial_list.scala.html
+++ app/views/issue/my_partial_list.scala.html
... | ... | @@ -45,6 +45,8 @@ |
45 | 45 |
@agoOrDateString(issue.createdDate) |
46 | 46 |
</span> |
47 | 47 |
|
48 |
+ @partial_list_subtask(project, issue) |
|
49 |
+ |
|
48 | 50 |
<span class="infos-item project-name"> |
49 | 51 |
<a href="@routes.ProjectApp.project(project.owner,project.name)" class="title project" data-toggle="tooltip" data-placement="bottom" title="@Messages("project.name")"> |
50 | 52 |
@project.name |
... | ... | @@ -53,7 +55,7 @@ |
53 | 55 |
<span class="infos-item post-id">#@issue.getNumber</span> |
54 | 56 |
|
55 | 57 |
@if(issue.milestone != null && project != null && project.menuSetting != null && project.menuSetting.milestone) { |
56 |
- <span class="infos-item mileston-tag"> |
|
58 |
+ <span class="mileston-tag"> |
|
57 | 59 |
<a href="@routes.MilestoneApp.milestone(project.owner, project.name, issue.milestone.id)" data-toggle="tooltip" data-placement="bottom" title="@Messages("milestone")"> |
58 | 60 |
@issue.milestone.title |
59 | 61 |
</a> |
--- app/views/issue/partial_list.scala.html
+++ app/views/issue/partial_list.scala.html
... | ... | @@ -1,29 +1,17 @@ |
1 | 1 |
@** |
2 |
-* Yobi, Project Hosting SW |
|
2 |
+* Yona, 21st Century Project Hosting SW |
|
3 | 3 |
* |
4 |
-* Copyright 2013 NAVER Corp. |
|
5 |
-* http://yobi.io |
|
6 |
-* |
|
7 |
-* @author Suwon Chae |
|
8 |
-* |
|
9 |
-* Licensed under the Apache License, Version 2.0 (the "License"); |
|
10 |
-* you may not use this file except in compliance with the License. |
|
11 |
-* You may obtain a copy of the License at |
|
12 |
-* |
|
13 |
-* http://www.apache.org/licenses/LICENSE-2.0 |
|
14 |
-* |
|
15 |
-* Unless required by applicable law or agreed to in writing, software |
|
16 |
-* distributed under the License is distributed on an "AS IS" BASIS, |
|
17 |
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
18 |
-* See the License for the specific language governing permissions and |
|
19 |
-* limitations under the License. |
|
4 |
+* Copyright Yona & Yobi Authors & NAVER Corp. |
|
5 |
+* https://yona.io |
|
20 | 6 |
**@ |
21 |
-@(project:Project, issueList:Collection[Issue], searchCondition:models.support.SearchCondition, pageIndex:Int, totalPageCount:Int) |
|
7 |
+@import models.support.SearchCondition |
|
8 |
+@(project:Project, issueList:Collection[Issue], searchCondition:SearchCondition, pageIndex:Int, totalPageCount:Int) |
|
22 | 9 |
|
23 | 10 |
@import java.util |
24 | 11 |
@import utils.JodaDateUtil |
25 | 12 |
@import utils.TemplateHelper._ |
26 | 13 |
@import utils.AccessControl._ |
14 |
+@import models.enumeration.State |
|
27 | 15 |
|
28 | 16 |
|
29 | 17 |
@issueLabels(issue:Issue) = {@for(label <- issue.labels.toList.sortBy(r => (r.category.name, r.name))) {@label.category.name,@label.id,@label.name|}} |
... | ... | @@ -68,8 +56,10 @@ |
68 | 56 |
@agoOrDateString(issue.createdDate) |
69 | 57 |
</span> |
70 | 58 |
|
59 |
+ @partial_list_subtask(project, issue) |
|
60 |
+ |
|
71 | 61 |
@if(project.menuSetting.milestone && issue.milestone != null) { |
72 |
- <span class="infos-item mileston-tag"> |
|
62 |
+ <span class="mileston-tag"> |
|
73 | 63 |
<a href="@routes.MilestoneApp.milestone(project.owner, project.name, issue.milestone.id)" data-toggle="tooltip" data-placement="bottom" title="@Messages("milestone")"> |
74 | 64 |
@issue.milestone.title |
75 | 65 |
</a> |
+++ app/views/issue/partial_list_subtask.scala.html
... | ... | @@ -0,0 +1,31 @@ |
1 | +@** | |
2 | +* Yona, 21st Century Project Hosting SW | |
3 | +* | |
4 | +* Copyright Yona & Yobi Authors & NAVER Corp. | |
5 | +* https://yona.io | |
6 | +**@ | |
7 | +@(project:Project, issue:Issue) | |
8 | + | |
9 | +@import utils.TemplateHelper._ | |
10 | +@import models.enumeration.State | |
11 | + | |
12 | + | |
13 | +@if(issue.hasChildIssue) { | |
14 | + @defining(Issue.countByParentIssueIdAndState(issue.id, State.OPEN)) { openIssueCount => | |
15 | + @defining(Issue.countByParentIssueIdAndState(issue.id, State.CLOSED)) { closedIssueCount => | |
16 | + @defining(getPercent(closedIssueCount.toDouble, openIssueCount + closedIssueCount)) { percentage => | |
17 | + <div class="upload-progress"> | |
18 | + <div class="bar @if(percentage == 100) {done} else {grey}" style="width: @percentage%;" title="Subtask"></div> | |
19 | + </div> | |
20 | + <span class="completion-ratio @if(percentage == 100){txt-green}">@if(percentage != 100){@closedIssueCount/}@(closedIssueCount + openIssueCount)</span> | |
21 | + } | |
22 | + } | |
23 | + } | |
24 | +} | |
25 | +@if(issue.parent != null){ | |
26 | + <span class="infos-item subtask"> | |
27 | + @defining(Issue.finder.byId(issue.parent.id)) { parentIssue => | |
28 | + <a href="@routes.IssueApp.issue(project.owner, project.name, parentIssue.getNumber)">#@parentIssue.getNumber @parentIssue.title.take(5).trim()@if(parentIssue.title.size > 5){...}</a> | |
29 | + } | |
30 | + </span> | |
31 | +} |
+++ app/views/issue/partial_select_subtask.scala.html
... | ... | @@ -0,0 +1,34 @@ |
1 | +@** | |
2 | +* Yona, 21st Century Project Hosting SW | |
3 | +* | |
4 | +* Copyright Yona & Yobi Authors & NAVER Corp. | |
5 | +* https://yona.io | |
6 | +**@ | |
7 | +@(project:Project, issue:Issue, parentIssueId:Long) | |
8 | + | |
9 | +@import utils.TemplateHelper._ | |
10 | + | |
11 | +<div class="subtitle-message">+ Sub task</div> | |
12 | +<div class="subtask-wrap"> | |
13 | + <div class="span3"> | |
14 | + <select id="projects" name="targetProjectName" data-format="projects" data-placeholder="@Messages("organization.choose.projects")" data-toggle="select2" data-container-css-class="fullsize" > | |
15 | + <option value="@project.name" data-avatar-url="@urlToProjectLogo(project)">@project.name</option> | |
16 | + @for(project <- UserApp.currentUser().myProjects("")) { | |
17 | + <option value="@project.name" data-avatar-url="@urlToProjectLogo(project)">@project.name</option> | |
18 | + } | |
19 | + </select> | |
20 | + </div> | |
21 | + <div class="span8"> | |
22 | + <select id="parentId" name="parentIssueId" data-format="issues" data-placeholder="@Messages("organization.choose.projects")" data-toggle="select2" data-container-css-class="fullsize" > | |
23 | + <option value="" selected> -- select an issue -- </option> | |
24 | + @if(Option(parentIssueId).isDefined){ | |
25 | + @defining(Issue.finder.byId(parentIssueId)) { issue => | |
26 | + <option value="@issue.id" selected>#@issue.getNumber. @issue.title</option> | |
27 | + } | |
28 | + } | |
29 | + @for(issue <- Issue.findByProject(project, "", 1)) { | |
30 | + <option value="@issue.id" @if(issue.id == parentIssueId){selected}>#@issue.getNumber. @issue.title</option> | |
31 | + } | |
32 | + </select> | |
33 | + </div> | |
34 | +</div> |
--- app/views/issue/view.scala.html
+++ app/views/issue/view.scala.html
... | ... | @@ -14,6 +14,7 @@ |
14 | 14 |
@import utils.AccessControl._ |
15 | 15 |
@import play.libs.Json.toJson |
16 | 16 |
@import utils.Markdown |
17 |
+@import models.enumeration.State |
|
17 | 18 |
|
18 | 19 |
@getTitle(issue:Issue) = @{ "#" + issue.getNumber + " " + issue.title } |
19 | 20 |
|
... | ... | @@ -43,6 +44,53 @@ |
43 | 44 |
} |
44 | 45 |
|
45 | 46 |
@titleForOGTag = @{getTitle(issue) + " |:| " + issue.body.substring(0, Math.min(issue.body.length, 200))} |
47 |
+ |
|
48 |
+@parentIssueId = @{ |
|
49 |
+ if(issue.parent != null){ |
|
50 |
+ issue.parent.id |
|
51 |
+ } else { |
|
52 |
+ issue.id |
|
53 |
+ } |
|
54 |
+} |
|
55 |
+ |
|
56 |
+@showChildIssues(parentIssueId: Long) = { |
|
57 |
+@defining(Issue.findByParentIssueIdAndState(parentIssueId, State.OPEN)) { openChildIssues => |
|
58 |
+ @defining(Issue.findByParentIssueIdAndState(parentIssueId, State.CLOSED)) { closedChildIssues => |
|
59 |
+ @if(!openChildIssues.isEmpty || !closedChildIssues.isEmpty) { |
|
60 |
+ <div class="child-issues"> |
|
61 |
+ @defining(Issue.finder.byId(parentIssueId)) { parentIssue => |
|
62 |
+ <div class="issue-item parent-issue"><a href="@routes.IssueApp.issue(project.owner, project.name, parentIssue.getNumber)" class="@if(parentIssue.id == issue.id){bold}">#@parentIssue.getNumber @parentIssue.title @if(parentIssue.assignee != null) {- @parentIssue.assignee.user.name}</a> |
|
63 |
+ @defining(getPercent(closedChildIssues.size.toDouble, (openChildIssues.size + closedChildIssues.size).toDouble)) { percentage => |
|
64 |
+ <div class="upload-progress"> |
|
65 |
+ <div class="bar @if(percentage == 100){done} else {grey}" style="width: @percentage%;" title="Subtask"></div> |
|
66 |
+ </div> |
|
67 |
+ <span class="@if(percentage == 100){txt-green}">@if(percentage != 100){@closedChildIssues.size/}@(openChildIssues.size + closedChildIssues.size)</span> |
|
68 |
+ <span class="parent-issue-state @parentIssue.state.state">@Messages("issue.state." + parentIssue.state.state)</span> |
|
69 |
+ </div> |
|
70 |
+ } |
|
71 |
+ } |
|
72 |
+ <hr style="margin: 0"/> |
|
73 |
+ @for(childIssue <- openChildIssues) { |
|
74 |
+ <div class="issue-item @if(childIssue.id == issue.id){bold}"> |
|
75 |
+ <span class="state-label open"></span> |
|
76 |
+ <a href="@routes.IssueApp.issue(project.owner, project.name, childIssue.getNumber)"> |
|
77 |
+ <span class="item-name">@childIssue.title @if(childIssue.assignee != null) {- @childIssue.assignee.user.name}</span> |
|
78 |
+ </a> |
|
79 |
+ </div> |
|
80 |
+ } |
|
81 |
+ @for(childIssue <- closedChildIssues) { |
|
82 |
+ <div class="issue-item @if(childIssue.id == issue.id){bold}"> |
|
83 |
+ <span class="state-label closed"><i class=" yobicon-checkmark"></i></span> |
|
84 |
+ <a href="@routes.IssueApp.issue(project.owner, project.name, childIssue.getNumber)"> |
|
85 |
+ <span class="item-name">@childIssue.title @if(childIssue.assignee != null) {- @childIssue.assignee.user.name}</span> |
|
86 |
+ </a> |
|
87 |
+ </div> |
|
88 |
+ } |
|
89 |
+ </div> |
|
90 |
+ } |
|
91 |
+ } |
|
92 |
+} |
|
93 |
+} |
|
46 | 94 |
|
47 | 95 |
@conatinsCurrentUserInWatchers = @{issue.getWatchers(false).contains(UserApp.currentUser())} |
48 | 96 |
|
... | ... | @@ -155,6 +203,13 @@ |
155 | 203 |
} |
156 | 204 |
</div> |
157 | 205 |
<div class="watcher-list"></div> |
206 |
+ <div class="subtasks"> |
|
207 |
+ @if(issue.parent == null) { |
|
208 |
+ @showChildIssues(issue.id) |
|
209 |
+ } else { |
|
210 |
+ @showChildIssues(issue.parent.id) |
|
211 |
+ } |
|
212 |
+ </div> |
|
158 | 213 |
@** Comment **@ |
159 | 214 |
<div id="comments" class="board-comment-wrap"> |
160 | 215 |
<div id="timeline"> |
... | ... | @@ -176,7 +231,7 @@ |
176 | 231 |
<dl> |
177 | 232 |
@if(project.menuSetting.issue) { |
178 | 233 |
<dd class="project-btn-item"> |
179 |
- <a href="@routes.IssueApp.newIssueForm(project.owner, project.name)" class="ybtn ybtn-success" target="_blank">@Messages("button.newIssue")</a> |
|
234 |
+ <a href="@routes.IssueApp.newIssueForm(project.owner, project.name)?parentIssueId=@parentIssueId" class="ybtn ybtn-success" target="_blank">@Messages("button.newIssue")</a> |
|
180 | 235 |
</dd> |
181 | 236 |
} |
182 | 237 |
<dt>@Messages("issue.assignee")</dt> |
+++ conf/evolutions/default/13.sql
... | ... | @@ -0,0 +1,17 @@ |
1 | +# --- !Ups | |
2 | +ALTER TABLE issue ADD COLUMN parent_id bigint; | |
3 | +alter table issue add constraint fk_issue_parent_id_01 foreign key (parent_id) references issue (id) on delete set null; | |
4 | +CREATE INDEX ix_issue_parent_id ON issue (parent_id); | |
5 | + | |
6 | +ALTER TABLE posting ADD COLUMN parent_id bigint; | |
7 | +alter table posting add constraint fk_posting_parent_id_01 foreign key (parent_id) references posting (id) on delete set null; | |
8 | +CREATE INDEX ix_posting_parent_id ON posting (parent_id); | |
9 | + | |
10 | +# --- !Downs | |
11 | + | |
12 | + | |
13 | +ALTER TABLE issue DROP FOREIGN KEY fk_issue_parent_id_01; | |
14 | +ALTER TABLE posting DROP FOREIGN KEY fk_posting_parent_id_01; | |
15 | +ALTER TABLE issue DROP COLUMN parent_id; | |
16 | +ALTER TABLE posting DROP COLUMN parent_id; | |
17 | + |
--- public/javascripts/common/yobi.ui.Select2.js
+++ public/javascripts/common/yobi.ui.Select2.js
... | ... | @@ -61,6 +61,11 @@ |
61 | 61 |
}); |
62 | 62 |
} |
63 | 63 |
}, |
64 |
+ "issues": function(itemObject){ |
|
65 |
+ return $yobi.tmpl($("#tplSelect2FormatIssues").text(), { |
|
66 |
+ "name" : itemObject.text |
|
67 |
+ }); |
|
68 |
+ }, |
|
64 | 69 |
"user": function(itemObject){ |
65 | 70 |
var itemElement = $(itemObject.element); |
66 | 71 |
var avatarURL = itemElement.data("avatarUrl"); |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?