doortts doortts 2016-01-14
board: Add label feature to board
@5e7f61e26f65291077204dbe50dce35ea6ca7ceb
app/assets/stylesheets/less/_page.less
--- app/assets/stylesheets/less/_page.less
+++ app/assets/stylesheets/less/_page.less
@@ -6345,3 +6345,28 @@
 .group-board {
       width: 100% !important;
 }
+
+.board-labels {
+  width: 400px;
+  display: inline-block;
+  dt {
+    display: none;
+  }
+}
+
+.post-list{
+  .search-wrap {
+    & form {
+      width: 90%;
+      .search-bar {
+        height: 22px;
+        width: 400px;
+        display: inline-block;
+        margin-right: 10px;
+      }
+      .select2-container {
+        height: 32px;
+      }
+    }
+  }
+}
 
app/controllers/BoardApi.java (added)
+++ app/controllers/BoardApi.java
@@ -0,0 +1,51 @@
+/**
+ * Yobire, Project Hosting SW
+ *
+ * @author Suwon Chae
+ * Copyright 2016 the original author or authors.
+ */
+
+package controllers;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import models.IssueLabel;
+import models.Posting;
+import models.Project;
+import play.db.ebean.Transactional;
+import play.libs.Json;
+import play.mvc.Result;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import static play.libs.Json.toJson;
+
+public class BoardApi extends AbstractPostingApp {
+
+    @Transactional
+    public static Result updatePostLabel(String owner, String projectName, Long number) {
+        JsonNode json = request().body().asJson();
+        if(json == null) {
+            return badRequest("Expecting Json data");
+        }
+        Project project = Project.findByOwnerAndProjectName(owner, projectName);
+        Posting posting = Posting.findByNumber(project, number);
+        Set<IssueLabel> labels = new HashSet<>();
+
+        for(JsonNode node: json){
+            System.out.println("node: " + node);
+            Long labelId = Long.parseLong(node.asText());
+            labels.add(IssueLabel.finder.byId(labelId));
+        }
+
+        posting.labels = labels;
+        posting.save();
+
+        ObjectNode result = Json.newObject();
+        result.put("id", project.owner);
+        result.put("labels", toJson(posting.labels.size()));
+        return ok(result);
+    }
+
+}
app/controllers/BoardApp.java
--- app/controllers/BoardApp.java
+++ app/controllers/BoardApp.java
@@ -21,21 +21,17 @@
 package controllers;
 
 import actions.NullProjectCheckAction;
-
 import com.avaje.ebean.ExpressionList;
 import com.avaje.ebean.Page;
-
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import controllers.annotation.AnonymousCheck;
 import controllers.annotation.IsAllowed;
 import controllers.annotation.IsCreatable;
 import models.*;
 import models.enumeration.Operation;
 import models.enumeration.ResourceType;
-
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-
 import play.data.Form;
 import play.db.ebean.Transactional;
 import play.libs.Json;
@@ -54,20 +50,25 @@
 
 import javax.annotation.Nonnull;
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
+import java.util.*;
 
 import static com.avaje.ebean.Expr.icontains;
 
 public class BoardApp extends AbstractPostingApp {
     public static class SearchCondition extends AbstractPostingApp.SearchCondition {
         public List<String> projectNames;
+        public String [] labelIds;
+        public Set<Long> labelIdSet = new HashSet<>();
         private ExpressionList<Posting> asExpressionList(Project project) {
             ExpressionList<Posting> el = Posting.finder.where().eq("project.id", project.id);
 
             if (filter != null) {
                 el.or(icontains("title", filter), icontains("body", filter));
+            }
+
+            if (CollectionUtils.isNotEmpty(labelIdSet)) {
+                Set<IssueLabel> labels = IssueLabel.finder.where().idIn(new ArrayList<>(labelIdSet)).findSet();
+                el.in("id", Posting.finder.where().in("labels", labels).findIds());
             }
 
             if (StringUtils.isNotBlank(orderBy)) {
@@ -155,6 +156,8 @@
         if (searchCondition.orderBy.equals("id")) {
             searchCondition.orderBy = "createdDate";
         }
+        searchCondition.labelIdSet.addAll(LabelApp.getLabelIds(request()));
+        searchCondition.labelIdSet.remove(null);
 
         ExpressionList<Posting> el = searchCondition.asExpressionList(project);
         el.eq("notice", false);
app/models/IssueLabel.java
--- app/models/IssueLabel.java
+++ app/models/IssueLabel.java
@@ -67,6 +67,9 @@
     @ManyToMany(mappedBy="labels", fetch = FetchType.EAGER)
     public Set<Issue> issues;
 
+    @ManyToMany(mappedBy="labels", fetch = FetchType.EAGER)
+    public Set<Posting> postings;
+
     public static List<IssueLabel> findByProject(Project project) {
         return finder.where()
                 .eq("project.id", project.id)
app/models/Posting.java
--- app/models/Posting.java
+++ app/models/Posting.java
@@ -10,7 +10,9 @@
 
 import javax.persistence.*;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 import static com.avaje.ebean.Expr.eq;
 
@@ -27,6 +29,19 @@
     @OneToMany(cascade = CascadeType.ALL)
     public List<PostingComment> comments;
 
+    @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.REMOVE)
+    public Set<IssueLabel> labels;
+
+    public Set<Long> getLabelIds() {
+        Set<Long> labelIds = new HashSet<>();
+
+        for(IssueLabel label : this.labels){
+            labelIds.add(label.id);
+        }
+
+        return labelIds;
+    }
+
     public Posting(Project project, User author, String title, String body) {
         super(project, author, title, body);
     }
app/views/board/list.scala.html
--- app/views/board/list.scala.html
+++ app/views/board/list.scala.html
@@ -30,23 +30,29 @@
 
 @makeFilterLink(fieldName:String, orderBy:String, orderDir:String, fieldText:String) = {
 	@if(orderBy.equals(fieldName)) {
-		<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>
+		<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>
 	} else {
-	    <a href="@urlToList?orderBy=@fieldName&orderDir=desc" class="filter"><i class="ico btn-gray-arrow down"></i>@fieldText</a>
+	    <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>
 	}
 }
 
 @projectLayout(title, project, utils.MenuType.BOARD) {
 @projectMenu(project, utils.MenuType.BOARD, "main-menu-only")
+<link rel="stylesheet" href="@routes.IssueLabelApp.labelStyles(project.owner, project.name)" type="text/css" />
 <div class="page-wrap-outer">
-    <div class="project-page-wrap">
+    <div class="post-list project-page-wrap">
         <div class="search-wrap underline">
-            <form id="option_form" method="get" class="pull-left">
+            <form id="option_form" action="@routes.BoardApp.posts(project.owner, project.name)" method="get" class="pull-left">
                 <input type="hidden" name="orderBy"  value="@param.orderBy">
                 <input type="hidden" name="orderDir" value="@param.orderDir">
                 <div class="search-bar">
                     <input name="filter" class="textbox" type="text" placeholder="@Messages("project.searchPlaceholder")" value="@param.filter">
                     <button type="submit" class="search-btn"><i class="yobicon-search"></i></button>
+                </div>
+                <div class="board-labels">
+                @if(!IssueLabel.findByProject(project).isEmpty){
+                    @issue.partial_select_label(IssueLabel.findByProject(project), param.labelIdSet)
+                }
                 </div>
             </form>
             <div class="pull-right">
@@ -107,6 +113,11 @@
                 "N": "@routes.BoardApp.newPostForm(project.owner, project.name)"
             });
         }
+
+        $('.board-labels select').on('change', function(e){
+            $("#option_form").submit();
+        });
 	});
 </script>
+@common.select2()
 }
app/views/board/partial_list.scala.html
--- app/views/board/partial_list.scala.html
+++ app/views/board/partial_list.scala.html
@@ -64,7 +64,10 @@
         <span class="infos-item item-count-groups">
             @countHtml("comments",routes.BoardApp.post(project.owner, project.name, post.getNumber).toString() + "#comments", post.numOfComments)
         </span>
-        }       
+        }
+        @for(label <- post.labels) {
+            <a href="#" class="label issue-label list-label active" data-category-id="@label.category.id" data-label-id="@label.id">@label.name</a>
+        }
     </div>
 </li>
 }
app/views/board/view.scala.html
--- app/views/board/view.scala.html
+++ app/views/board/view.scala.html
@@ -5,6 +5,7 @@
 * http://yobi.io
 *
 * @author Ahn Hyeok Jun
+* @author Suwon Chae
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
@@ -65,9 +66,18 @@
               <strong class="name">@Messages("common.noAuthor")</strong>
             }
           </a>
+          <div class="board-labels pull-right">
+            @if(!IssueLabel.findByProject(project).isEmpty){
+                @if(isAllowed(UserApp.currentUser(), post.asResource(), Operation.UPDATE)){
+                    @issue.partial_select_label(IssueLabel.findByProject(project), post.getLabelIds)
+                } else {
+                    @issue.partial_show_selected_label(post.labels.toList, "")
+                }
+            }
+            </div>
         </div>
-    		<div class="content markdown-wrap">@Html(Markdown.render(post.body, post.asResource().getProject()))</div>
-            <div class="attachments" id="attachments" data-attachments="@toJson(AttachmentApp.getFileList(ResourceType.BOARD_POST.toString(), post.id.toString()))"></div>
+        <div class="content markdown-wrap">@Html(Markdown.render(post.body, post.asResource().getProject()))</div>
+        <div class="attachments" id="attachments" data-attachments="@toJson(AttachmentApp.getFileList(ResourceType.BOARD_POST.toString(), post.id.toString()))"></div>
     	</div>
     	<div class="board-footer board-actrow">
     	    <div class="pull-left">
@@ -122,6 +132,7 @@
 @common.markdown(project)
 @common.commentDeleteModal()
 
+<link rel="stylesheet" type="text/css" media="screen" href="@routes.IssueLabelApp.labelStyles(project.owner, project.name)">
 <link rel="stylesheet" type="text/css" media="screen" href="@routes.Assets.at("javascripts/lib/atjs/jquery.atwho.css")">
 <script type="text/javascript" src="@routes.Assets.at("javascripts/lib/atjs/jquery.caret.min.js")"></script>
 <script type="text/javascript" src="@routes.Assets.at("javascripts/lib/atjs/jquery.atwho.js")"></script>
@@ -148,6 +159,19 @@
             "target": "textarea[id^=editor-]",
             "url"   : "@Html(routes.ProjectApp.mentionList(project.owner, project.name, post.getNumber, post.asResource().getType.resource()).toString())"
         });
+
+        $('.board-labels select').on('change', function(e){
+            var url = "@Html(routes.BoardApi.updatePostLabel(project.owner, project.name, post.getNumber).toString())";
+            $.ajax({
+                url: url,
+                method: "POST",
+                data: JSON.stringify(e.val),
+                contentType: "application/json; charset=UTF-8"
+            }).done(function(response) {
+            }).error(function(response){
+            });
+        });
 	});
 </script>
+@common.select2()
 }
app/views/issue/partial_select_label.scala.html
--- app/views/issue/partial_select_label.scala.html
+++ app/views/issue/partial_select_label.scala.html
@@ -30,7 +30,7 @@
     <select name="labelIds" multiple="multiple" data-search="labelIds"
             data-toggle="select2" data-format="issuelabel" data-allow-clear="true"
             data-dropdown-css-class="issue-labels" data-container-css-class="issue-labels bordered fullsize"
-            data-placeholder="@Messages("label.select")" @additionalAttr>
+            data-placeholder="@Messages("label.select")" @additionalAttr class="hide">
       <option></option>
       @labels.groupBy(_.category).map {
         case (category, labels) => {
 
conf/evolutions/default/106.sql (added)
+++ conf/evolutions/default/106.sql
@@ -0,0 +1,14 @@
+# --- !Ups
+create table posting_issue_label (
+  posting_id                     bigint not null,
+  issue_label_id                 bigint not null,
+  constraint pk_posting_issue_label primary key (posting_id, issue_label_id))
+;
+
+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;
+
+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;
+
+# --- !Downs
+drop table if exists POSTING_ISSUE_LABEL;
+
conf/routes
--- conf/routes
+++ conf/routes
@@ -21,6 +21,10 @@
 # Search
 GET            /search                                                                controllers.SearchApp.searchInAll()
 
+GET            /-_-api                                                                controllers.Application.index()
+GET            /-_-api/v1/                                                            controllers.Application.index()
+POST           /-_-api/v1/owners/:owner/projects/:projectName/:number                 controllers.BoardApi.updatePostLabel(owner:String, projectName:String, number:Long)
+
 # Import
 GET            /import                                                                controllers.ImportApp.importForm()
 POST           /import                                                                controllers.ImportApp.newProject()
public/javascripts/service/yobi.board.List.js
--- public/javascripts/service/yobi.board.List.js
+++ public/javascripts/service/yobi.board.List.js
@@ -53,6 +53,7 @@
             htElement.welInputOrderBy = htElement.welForm.find("input[name=orderBy]");
             htElement.welInputOrderDir = htElement.welForm.find("input[name=orderDir]");
             htElement.welInputPageNum = htElement.welForm.find("input[name=pageNum]");
+            htElement.welIssueWrap = $(htOptions.welIssueWrap || '.post-list-wrap');
 
             htElement.welPages = $(htOptions.sQueryPages || "#pagination a");
             htElement.welPagination = $(htOptions.elPagination || '#pagination');
@@ -63,6 +64,7 @@
          */
         function _attachEvent() {
             htElement.welPages.click(_onClickPage);
+            htElement.welIssueWrap.on("click", "a[data-label-id][data-category-id]", _onClickLabelOnList);
         }
 
         /**
@@ -74,6 +76,33 @@
             return false;
         }
 
+        /**
+         * "click" event handler of labels on the list.
+         * Add clicked label to search form condition.
+         *
+         * @param event
+         * @private
+         */
+        function _onClickLabelOnList(weEvt) {
+            weEvt.preventDefault();
+
+            var link = $(this);
+            var targetQuery = "[data-search=labelIds]";
+            var target = htElement.welForm.find(targetQuery);
+
+            var labelId = link.data("labelId");
+            var newValue;
+
+            if(target.prop("multiple")){
+                newValue = (target.val() || []);
+                newValue.push(labelId);
+            } else {
+                newValue = labelId;
+            }
+
+            target.data("select2").val(newValue, true); // triggerChange=true
+            console.log("labelId", labelId);
+        }
 
         function _initPagination(htOptions){
             yobi.Pagination.update(htElement.welPagination, htOptions.nTotalPages);
Add a comment
List