--- app/assets/stylesheets/less/_page.less
+++ app/assets/stylesheets/less/_page.less
... | ... | @@ -6345,3 +6345,28 @@ |
6345 | 6345 |
.group-board { |
6346 | 6346 |
width: 100% !important; |
6347 | 6347 |
} |
6348 |
+ |
|
6349 |
+.board-labels { |
|
6350 |
+ width: 400px; |
|
6351 |
+ display: inline-block; |
|
6352 |
+ dt { |
|
6353 |
+ display: none; |
|
6354 |
+ } |
|
6355 |
+} |
|
6356 |
+ |
|
6357 |
+.post-list{ |
|
6358 |
+ .search-wrap { |
|
6359 |
+ & form { |
|
6360 |
+ width: 90%; |
|
6361 |
+ .search-bar { |
|
6362 |
+ height: 22px; |
|
6363 |
+ width: 400px; |
|
6364 |
+ display: inline-block; |
|
6365 |
+ margin-right: 10px; |
|
6366 |
+ } |
|
6367 |
+ .select2-container { |
|
6368 |
+ height: 32px; |
|
6369 |
+ } |
|
6370 |
+ } |
|
6371 |
+ } |
|
6372 |
+} |
+++ app/controllers/BoardApi.java
... | ... | @@ -0,0 +1,51 @@ |
1 | +/** | |
2 | + * Yobire, Project Hosting SW | |
3 | + * | |
4 | + * @author Suwon Chae | |
5 | + * Copyright 2016 the original author or authors. | |
6 | + */ | |
7 | + | |
8 | +package controllers; | |
9 | + | |
10 | +import com.fasterxml.jackson.databind.JsonNode; | |
11 | +import com.fasterxml.jackson.databind.node.ObjectNode; | |
12 | +import models.IssueLabel; | |
13 | +import models.Posting; | |
14 | +import models.Project; | |
15 | +import play.db.ebean.Transactional; | |
16 | +import play.libs.Json; | |
17 | +import play.mvc.Result; | |
18 | + | |
19 | +import java.util.HashSet; | |
20 | +import java.util.Set; | |
21 | + | |
22 | +import static play.libs.Json.toJson; | |
23 | + | |
24 | +public class BoardApi extends AbstractPostingApp { | |
25 | + | |
26 | + @Transactional | |
27 | + public static Result updatePostLabel(String owner, String projectName, Long number) { | |
28 | + JsonNode json = request().body().asJson(); | |
29 | + if(json == null) { | |
30 | + return badRequest("Expecting Json data"); | |
31 | + } | |
32 | + Project project = Project.findByOwnerAndProjectName(owner, projectName); | |
33 | + Posting posting = Posting.findByNumber(project, number); | |
34 | + Set<IssueLabel> labels = new HashSet<>(); | |
35 | + | |
36 | + for(JsonNode node: json){ | |
37 | + System.out.println("node: " + node); | |
38 | + Long labelId = Long.parseLong(node.asText()); | |
39 | + labels.add(IssueLabel.finder.byId(labelId)); | |
40 | + } | |
41 | + | |
42 | + posting.labels = labels; | |
43 | + posting.save(); | |
44 | + | |
45 | + ObjectNode result = Json.newObject(); | |
46 | + result.put("id", project.owner); | |
47 | + result.put("labels", toJson(posting.labels.size())); | |
48 | + return ok(result); | |
49 | + } | |
50 | + | |
51 | +} |
--- app/controllers/BoardApp.java
+++ app/controllers/BoardApp.java
... | ... | @@ -21,21 +21,17 @@ |
21 | 21 |
package controllers; |
22 | 22 |
|
23 | 23 |
import actions.NullProjectCheckAction; |
24 |
- |
|
25 | 24 |
import com.avaje.ebean.ExpressionList; |
26 | 25 |
import com.avaje.ebean.Page; |
27 |
- |
|
26 |
+import com.fasterxml.jackson.databind.node.ObjectNode; |
|
28 | 27 |
import controllers.annotation.AnonymousCheck; |
29 | 28 |
import controllers.annotation.IsAllowed; |
30 | 29 |
import controllers.annotation.IsCreatable; |
31 | 30 |
import models.*; |
32 | 31 |
import models.enumeration.Operation; |
33 | 32 |
import models.enumeration.ResourceType; |
34 |
- |
|
35 | 33 |
import org.apache.commons.collections.CollectionUtils; |
36 | 34 |
import org.apache.commons.lang3.StringUtils; |
37 |
-import com.fasterxml.jackson.databind.node.ObjectNode; |
|
38 |
- |
|
39 | 35 |
import play.data.Form; |
40 | 36 |
import play.db.ebean.Transactional; |
41 | 37 |
import play.libs.Json; |
... | ... | @@ -54,20 +50,25 @@ |
54 | 50 |
|
55 | 51 |
import javax.annotation.Nonnull; |
56 | 52 |
import java.io.IOException; |
57 |
-import java.util.ArrayList; |
|
58 |
-import java.util.List; |
|
59 |
-import java.util.Objects; |
|
53 |
+import java.util.*; |
|
60 | 54 |
|
61 | 55 |
import static com.avaje.ebean.Expr.icontains; |
62 | 56 |
|
63 | 57 |
public class BoardApp extends AbstractPostingApp { |
64 | 58 |
public static class SearchCondition extends AbstractPostingApp.SearchCondition { |
65 | 59 |
public List<String> projectNames; |
60 |
+ public String [] labelIds; |
|
61 |
+ public Set<Long> labelIdSet = new HashSet<>(); |
|
66 | 62 |
private ExpressionList<Posting> asExpressionList(Project project) { |
67 | 63 |
ExpressionList<Posting> el = Posting.finder.where().eq("project.id", project.id); |
68 | 64 |
|
69 | 65 |
if (filter != null) { |
70 | 66 |
el.or(icontains("title", filter), icontains("body", filter)); |
67 |
+ } |
|
68 |
+ |
|
69 |
+ if (CollectionUtils.isNotEmpty(labelIdSet)) { |
|
70 |
+ Set<IssueLabel> labels = IssueLabel.finder.where().idIn(new ArrayList<>(labelIdSet)).findSet(); |
|
71 |
+ el.in("id", Posting.finder.where().in("labels", labels).findIds()); |
|
71 | 72 |
} |
72 | 73 |
|
73 | 74 |
if (StringUtils.isNotBlank(orderBy)) { |
... | ... | @@ -155,6 +156,8 @@ |
155 | 156 |
if (searchCondition.orderBy.equals("id")) { |
156 | 157 |
searchCondition.orderBy = "createdDate"; |
157 | 158 |
} |
159 |
+ searchCondition.labelIdSet.addAll(LabelApp.getLabelIds(request())); |
|
160 |
+ searchCondition.labelIdSet.remove(null); |
|
158 | 161 |
|
159 | 162 |
ExpressionList<Posting> el = searchCondition.asExpressionList(project); |
160 | 163 |
el.eq("notice", false); |
--- app/models/IssueLabel.java
+++ app/models/IssueLabel.java
... | ... | @@ -67,6 +67,9 @@ |
67 | 67 |
@ManyToMany(mappedBy="labels", fetch = FetchType.EAGER) |
68 | 68 |
public Set<Issue> issues; |
69 | 69 |
|
70 |
+ @ManyToMany(mappedBy="labels", fetch = FetchType.EAGER) |
|
71 |
+ public Set<Posting> postings; |
|
72 |
+ |
|
70 | 73 |
public static List<IssueLabel> findByProject(Project project) { |
71 | 74 |
return finder.where() |
72 | 75 |
.eq("project.id", project.id) |
--- app/models/Posting.java
+++ app/models/Posting.java
... | ... | @@ -10,7 +10,9 @@ |
10 | 10 |
|
11 | 11 |
import javax.persistence.*; |
12 | 12 |
import java.util.Collections; |
13 |
+import java.util.HashSet; |
|
13 | 14 |
import java.util.List; |
15 |
+import java.util.Set; |
|
14 | 16 |
|
15 | 17 |
import static com.avaje.ebean.Expr.eq; |
16 | 18 |
|
... | ... | @@ -27,6 +29,19 @@ |
27 | 29 |
@OneToMany(cascade = CascadeType.ALL) |
28 | 30 |
public List<PostingComment> comments; |
29 | 31 |
|
32 |
+ @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.REMOVE) |
|
33 |
+ public Set<IssueLabel> labels; |
|
34 |
+ |
|
35 |
+ public Set<Long> getLabelIds() { |
|
36 |
+ Set<Long> labelIds = new HashSet<>(); |
|
37 |
+ |
|
38 |
+ for(IssueLabel label : this.labels){ |
|
39 |
+ labelIds.add(label.id); |
|
40 |
+ } |
|
41 |
+ |
|
42 |
+ return labelIds; |
|
43 |
+ } |
|
44 |
+ |
|
30 | 45 |
public Posting(Project project, User author, String title, String body) { |
31 | 46 |
super(project, author, title, body); |
32 | 47 |
} |
--- app/views/board/list.scala.html
+++ app/views/board/list.scala.html
... | ... | @@ -30,23 +30,29 @@ |
30 | 30 |
|
31 | 31 |
@makeFilterLink(fieldName:String, orderBy:String, orderDir:String, fieldText:String) = { |
32 | 32 |
@if(orderBy.equals(fieldName)) { |
33 |
- <a href="@urlToList?orderBy=@fieldName&orderDir=@if(orderDir.equals("desc")){asc}else{desc}" class="filter active"><i class="ico btn-gray-arrow @if(orderDir.equals("desc")){ down }"></i>@fieldText</a> |
|
33 |
+ <a href="@urlToList?filter=@param.filter&labelIds=@param.labelIds&orderBy=@fieldName&orderDir=@if(orderDir.equals("desc")){asc}else{desc}" class="filter active"><i class="ico btn-gray-arrow @if(orderDir.equals("desc")){ down }"></i>@fieldText</a> |
|
34 | 34 |
} else { |
35 |
- <a href="@urlToList?orderBy=@fieldName&orderDir=desc" class="filter"><i class="ico btn-gray-arrow down"></i>@fieldText</a> |
|
35 |
+ <a href="@urlToList?filter=@param.filter&labelIds=@param.labelIds&orderBy=@fieldName&orderDir=desc" class="filter"><i class="ico btn-gray-arrow down"></i>@fieldText</a> |
|
36 | 36 |
} |
37 | 37 |
} |
38 | 38 |
|
39 | 39 |
@projectLayout(title, project, utils.MenuType.BOARD) { |
40 | 40 |
@projectMenu(project, utils.MenuType.BOARD, "main-menu-only") |
41 |
+<link rel="stylesheet" href="@routes.IssueLabelApp.labelStyles(project.owner, project.name)" type="text/css" /> |
|
41 | 42 |
<div class="page-wrap-outer"> |
42 |
- <div class="project-page-wrap"> |
|
43 |
+ <div class="post-list project-page-wrap"> |
|
43 | 44 |
<div class="search-wrap underline"> |
44 |
- <form id="option_form" method="get" class="pull-left"> |
|
45 |
+ <form id="option_form" action="@routes.BoardApp.posts(project.owner, project.name)" method="get" class="pull-left"> |
|
45 | 46 |
<input type="hidden" name="orderBy" value="@param.orderBy"> |
46 | 47 |
<input type="hidden" name="orderDir" value="@param.orderDir"> |
47 | 48 |
<div class="search-bar"> |
48 | 49 |
<input name="filter" class="textbox" type="text" placeholder="@Messages("project.searchPlaceholder")" value="@param.filter"> |
49 | 50 |
<button type="submit" class="search-btn"><i class="yobicon-search"></i></button> |
51 |
+ </div> |
|
52 |
+ <div class="board-labels"> |
|
53 |
+ @if(!IssueLabel.findByProject(project).isEmpty){ |
|
54 |
+ @issue.partial_select_label(IssueLabel.findByProject(project), param.labelIdSet) |
|
55 |
+ } |
|
50 | 56 |
</div> |
51 | 57 |
</form> |
52 | 58 |
<div class="pull-right"> |
... | ... | @@ -107,6 +113,11 @@ |
107 | 113 |
"N": "@routes.BoardApp.newPostForm(project.owner, project.name)" |
108 | 114 |
}); |
109 | 115 |
} |
116 |
+ |
|
117 |
+ $('.board-labels select').on('change', function(e){ |
|
118 |
+ $("#option_form").submit(); |
|
119 |
+ }); |
|
110 | 120 |
}); |
111 | 121 |
</script> |
122 |
+@common.select2() |
|
112 | 123 |
} |
--- app/views/board/partial_list.scala.html
+++ app/views/board/partial_list.scala.html
... | ... | @@ -64,7 +64,10 @@ |
64 | 64 |
<span class="infos-item item-count-groups"> |
65 | 65 |
@countHtml("comments",routes.BoardApp.post(project.owner, project.name, post.getNumber).toString() + "#comments", post.numOfComments) |
66 | 66 |
</span> |
67 |
- } |
|
67 |
+ } |
|
68 |
+ @for(label <- post.labels) { |
|
69 |
+ <a href="#" class="label issue-label list-label active" data-category-id="@label.category.id" data-label-id="@label.id">@label.name</a> |
|
70 |
+ } |
|
68 | 71 |
</div> |
69 | 72 |
</li> |
70 | 73 |
} |
--- app/views/board/view.scala.html
+++ app/views/board/view.scala.html
... | ... | @@ -5,6 +5,7 @@ |
5 | 5 |
* http://yobi.io |
6 | 6 |
* |
7 | 7 |
* @author Ahn Hyeok Jun |
8 |
+* @author Suwon Chae |
|
8 | 9 |
* |
9 | 10 |
* Licensed under the Apache License, Version 2.0 (the "License"); |
10 | 11 |
* you may not use this file except in compliance with the License. |
... | ... | @@ -65,9 +66,18 @@ |
65 | 66 |
<strong class="name">@Messages("common.noAuthor")</strong> |
66 | 67 |
} |
67 | 68 |
</a> |
69 |
+ <div class="board-labels pull-right"> |
|
70 |
+ @if(!IssueLabel.findByProject(project).isEmpty){ |
|
71 |
+ @if(isAllowed(UserApp.currentUser(), post.asResource(), Operation.UPDATE)){ |
|
72 |
+ @issue.partial_select_label(IssueLabel.findByProject(project), post.getLabelIds) |
|
73 |
+ } else { |
|
74 |
+ @issue.partial_show_selected_label(post.labels.toList, "") |
|
75 |
+ } |
|
76 |
+ } |
|
77 |
+ </div> |
|
68 | 78 |
</div> |
69 |
- <div class="content markdown-wrap">@Html(Markdown.render(post.body, post.asResource().getProject()))</div> |
|
70 |
- <div class="attachments" id="attachments" data-attachments="@toJson(AttachmentApp.getFileList(ResourceType.BOARD_POST.toString(), post.id.toString()))"></div> |
|
79 |
+ <div class="content markdown-wrap">@Html(Markdown.render(post.body, post.asResource().getProject()))</div> |
|
80 |
+ <div class="attachments" id="attachments" data-attachments="@toJson(AttachmentApp.getFileList(ResourceType.BOARD_POST.toString(), post.id.toString()))"></div> |
|
71 | 81 |
</div> |
72 | 82 |
<div class="board-footer board-actrow"> |
73 | 83 |
<div class="pull-left"> |
... | ... | @@ -122,6 +132,7 @@ |
122 | 132 |
@common.markdown(project) |
123 | 133 |
@common.commentDeleteModal() |
124 | 134 |
|
135 |
+<link rel="stylesheet" type="text/css" media="screen" href="@routes.IssueLabelApp.labelStyles(project.owner, project.name)"> |
|
125 | 136 |
<link rel="stylesheet" type="text/css" media="screen" href="@routes.Assets.at("javascripts/lib/atjs/jquery.atwho.css")"> |
126 | 137 |
<script type="text/javascript" src="@routes.Assets.at("javascripts/lib/atjs/jquery.caret.min.js")"></script> |
127 | 138 |
<script type="text/javascript" src="@routes.Assets.at("javascripts/lib/atjs/jquery.atwho.js")"></script> |
... | ... | @@ -148,6 +159,19 @@ |
148 | 159 |
"target": "textarea[id^=editor-]", |
149 | 160 |
"url" : "@Html(routes.ProjectApp.mentionList(project.owner, project.name, post.getNumber, post.asResource().getType.resource()).toString())" |
150 | 161 |
}); |
162 |
+ |
|
163 |
+ $('.board-labels select').on('change', function(e){ |
|
164 |
+ var url = "@Html(routes.BoardApi.updatePostLabel(project.owner, project.name, post.getNumber).toString())"; |
|
165 |
+ $.ajax({ |
|
166 |
+ url: url, |
|
167 |
+ method: "POST", |
|
168 |
+ data: JSON.stringify(e.val), |
|
169 |
+ contentType: "application/json; charset=UTF-8" |
|
170 |
+ }).done(function(response) { |
|
171 |
+ }).error(function(response){ |
|
172 |
+ }); |
|
173 |
+ }); |
|
151 | 174 |
}); |
152 | 175 |
</script> |
176 |
+@common.select2() |
|
153 | 177 |
} |
--- app/views/issue/partial_select_label.scala.html
+++ app/views/issue/partial_select_label.scala.html
... | ... | @@ -30,7 +30,7 @@ |
30 | 30 |
<select name="labelIds" multiple="multiple" data-search="labelIds" |
31 | 31 |
data-toggle="select2" data-format="issuelabel" data-allow-clear="true" |
32 | 32 |
data-dropdown-css-class="issue-labels" data-container-css-class="issue-labels bordered fullsize" |
33 |
- data-placeholder="@Messages("label.select")" @additionalAttr> |
|
33 |
+ data-placeholder="@Messages("label.select")" @additionalAttr class="hide"> |
|
34 | 34 |
<option></option> |
35 | 35 |
@labels.groupBy(_.category).map { |
36 | 36 |
case (category, labels) => { |
+++ conf/evolutions/default/106.sql
... | ... | @@ -0,0 +1,14 @@ |
1 | +# --- !Ups | |
2 | +create table posting_issue_label ( | |
3 | + posting_id bigint not null, | |
4 | + issue_label_id bigint not null, | |
5 | + constraint pk_posting_issue_label primary key (posting_id, issue_label_id)) | |
6 | +; | |
7 | + | |
8 | +alter table posting_issue_label add constraint fk_posting_issue_label_issue_01 foreign key (posting_id) references posting (id) on delete restrict on update restrict; | |
9 | + | |
10 | +alter table posting_issue_label add constraint fk_posting_issue_label_issue_la_02 foreign key (issue_label_id) references issue_label (id) on delete restrict on update restrict; | |
11 | + | |
12 | +# --- !Downs | |
13 | +drop table if exists POSTING_ISSUE_LABEL; | |
14 | + |
--- conf/routes
+++ conf/routes
... | ... | @@ -21,6 +21,10 @@ |
21 | 21 |
# Search |
22 | 22 |
GET /search controllers.SearchApp.searchInAll() |
23 | 23 |
|
24 |
+GET /-_-api controllers.Application.index() |
|
25 |
+GET /-_-api/v1/ controllers.Application.index() |
|
26 |
+POST /-_-api/v1/owners/:owner/projects/:projectName/:number controllers.BoardApi.updatePostLabel(owner:String, projectName:String, number:Long) |
|
27 |
+ |
|
24 | 28 |
# Import |
25 | 29 |
GET /import controllers.ImportApp.importForm() |
26 | 30 |
POST /import controllers.ImportApp.newProject() |
--- public/javascripts/service/yobi.board.List.js
+++ public/javascripts/service/yobi.board.List.js
... | ... | @@ -53,6 +53,7 @@ |
53 | 53 |
htElement.welInputOrderBy = htElement.welForm.find("input[name=orderBy]"); |
54 | 54 |
htElement.welInputOrderDir = htElement.welForm.find("input[name=orderDir]"); |
55 | 55 |
htElement.welInputPageNum = htElement.welForm.find("input[name=pageNum]"); |
56 |
+ htElement.welIssueWrap = $(htOptions.welIssueWrap || '.post-list-wrap'); |
|
56 | 57 |
|
57 | 58 |
htElement.welPages = $(htOptions.sQueryPages || "#pagination a"); |
58 | 59 |
htElement.welPagination = $(htOptions.elPagination || '#pagination'); |
... | ... | @@ -63,6 +64,7 @@ |
63 | 64 |
*/ |
64 | 65 |
function _attachEvent() { |
65 | 66 |
htElement.welPages.click(_onClickPage); |
67 |
+ htElement.welIssueWrap.on("click", "a[data-label-id][data-category-id]", _onClickLabelOnList); |
|
66 | 68 |
} |
67 | 69 |
|
68 | 70 |
/** |
... | ... | @@ -74,6 +76,33 @@ |
74 | 76 |
return false; |
75 | 77 |
} |
76 | 78 |
|
79 |
+ /** |
|
80 |
+ * "click" event handler of labels on the list. |
|
81 |
+ * Add clicked label to search form condition. |
|
82 |
+ * |
|
83 |
+ * @param event |
|
84 |
+ * @private |
|
85 |
+ */ |
|
86 |
+ function _onClickLabelOnList(weEvt) { |
|
87 |
+ weEvt.preventDefault(); |
|
88 |
+ |
|
89 |
+ var link = $(this); |
|
90 |
+ var targetQuery = "[data-search=labelIds]"; |
|
91 |
+ var target = htElement.welForm.find(targetQuery); |
|
92 |
+ |
|
93 |
+ var labelId = link.data("labelId"); |
|
94 |
+ var newValue; |
|
95 |
+ |
|
96 |
+ if(target.prop("multiple")){ |
|
97 |
+ newValue = (target.val() || []); |
|
98 |
+ newValue.push(labelId); |
|
99 |
+ } else { |
|
100 |
+ newValue = labelId; |
|
101 |
+ } |
|
102 |
+ |
|
103 |
+ target.data("select2").val(newValue, true); // triggerChange=true |
|
104 |
+ console.log("labelId", labelId); |
|
105 |
+ } |
|
77 | 106 |
|
78 | 107 |
function _initPagination(htOptions){ |
79 | 108 |
yobi.Pagination.update(htElement.welPagination, htOptions.nTotalPages); |
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?