title-prefix: Support auto-completion title header
Title header and lable auto completion is triggered by '[' character in the issue/posting title input box. TL:DR --- Title prefix header is loved feature of Yona. It is very conveneint for expressing category of the posting. But somtimes user mistyped or made duplication of title header which is nearly the same but not exactly same. And, more Yona users adopt title header, more they omit selecting issue labels. It's a bad thing for all of us. Now, Yona supports auto-completion of title header prefix and issue labels. This is a trial to recude this kind of problem. I hope it will be going well.
@c25850cd871c118101013538d10e322d2ab5d79e
--- app/controllers/AbstractPostingApp.java
+++ app/controllers/AbstractPostingApp.java
... | ... | @@ -1,23 +1,9 @@ |
1 | 1 |
/** |
2 |
- * Yobi, Project Hosting SW |
|
3 |
- * |
|
4 |
- * Copyright 2013 NAVER Corp. |
|
5 |
- * http://yobi.io |
|
6 |
- * |
|
7 |
- * @author Yi EungJun |
|
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. & NAVER LABS Corp. |
|
5 |
+ * https://yona.io |
|
6 |
+ **/ |
|
21 | 7 |
package controllers; |
22 | 8 |
|
23 | 9 |
import com.fasterxml.jackson.databind.JsonNode; |
... | ... | @@ -129,6 +115,9 @@ |
129 | 115 |
posting.update(); |
130 | 116 |
posting.updateProperties(); |
131 | 117 |
|
118 |
+ TitleHead.saveTitleHeadKeyword(posting.project, posting.title); |
|
119 |
+ TitleHead.deleteTitleHeadKeyword(original.project, original.title); |
|
120 |
+ |
|
132 | 121 |
// Attach the files in the current user's temporary storage. |
133 | 122 |
attachUploadFilesToPost(original.asResource()); |
134 | 123 |
|
--- app/controllers/api/ProjectApi.java
+++ app/controllers/api/ProjectApi.java
... | ... | @@ -23,6 +23,7 @@ |
23 | 23 |
import play.i18n.Messages; |
24 | 24 |
import play.libs.Json; |
25 | 25 |
import play.mvc.Controller; |
26 |
+import play.mvc.Http; |
|
26 | 27 |
import play.mvc.Result; |
27 | 28 |
import playRepository.RepositoryService; |
28 | 29 |
import utils.AccessControl; |
... | ... | @@ -485,4 +486,61 @@ |
485 | 486 |
|
486 | 487 |
return createdUserNode; |
487 | 488 |
} |
489 |
+ |
|
490 |
+ @Transactional |
|
491 |
+ @IsAllowed(Operation.READ) |
|
492 |
+ public static Result titleHeads(String owner, String projectName, String query) { |
|
493 |
+ if (!request().accepts("application/json")) { |
|
494 |
+ return status(Http.Status.NOT_ACCEPTABLE); |
|
495 |
+ } |
|
496 |
+ |
|
497 |
+ Project project = Project.findByOwnerAndProjectName(owner, projectName); |
|
498 |
+ |
|
499 |
+ List<ObjectNode> searched = new ArrayList<>(); |
|
500 |
+ searched.addAll(getherTitleHeads(project, query)); |
|
501 |
+ searched.addAll(getherProjectLabels(project)); |
|
502 |
+ |
|
503 |
+ ObjectNode result = Json.newObject(); |
|
504 |
+ result.put("result", toJson(searched)); |
|
505 |
+ return ok(result); |
|
506 |
+ } |
|
507 |
+ |
|
508 |
+ private static List<ObjectNode> getherProjectLabels(Project project) { |
|
509 |
+ List<ObjectNode> searched = new ArrayList<>(); |
|
510 |
+ for (IssueLabel label : project.issueLabels) { |
|
511 |
+ searched.add(getIssueLabelNode(label)); |
|
512 |
+ } |
|
513 |
+ return searched; |
|
514 |
+ } |
|
515 |
+ |
|
516 |
+ private static List<ObjectNode> getherTitleHeads(Project project, String query) { |
|
517 |
+ List<ObjectNode> searched = new ArrayList<>(); |
|
518 |
+ List<TitleHead> titleHeads = TitleHead.findByProject(project, query); |
|
519 |
+ for (TitleHead titleHead : titleHeads) { |
|
520 |
+ searched.add(getTitleHeadNode(titleHead)); |
|
521 |
+ } |
|
522 |
+ return searched; |
|
523 |
+ } |
|
524 |
+ |
|
525 |
+ private static ObjectNode getTitleHeadNode(TitleHead titleHead) { |
|
526 |
+ ObjectNode titleHeadNode = Json.newObject(); |
|
527 |
+ titleHeadNode.put("name", titleHead.headKeyword); |
|
528 |
+ titleHeadNode.put("frequency", titleHead.frequency); |
|
529 |
+ titleHeadNode.put("category", ""); |
|
530 |
+ titleHeadNode.put("searchText", titleHead.headKeyword); |
|
531 |
+ return titleHeadNode; |
|
532 |
+ } |
|
533 |
+ |
|
534 |
+ private static ObjectNode getIssueLabelNode(IssueLabel label) { |
|
535 |
+ ObjectNode labelNode = Json.newObject(); |
|
536 |
+ labelNode.put("name", label.name); |
|
537 |
+ labelNode.put("frequency", 0); |
|
538 |
+ labelNode.put("category", label.category.name); |
|
539 |
+ labelNode.put("categoryId", label.category.id); |
|
540 |
+ labelNode.put("id", label.id); |
|
541 |
+ labelNode.put("labelColor", label.color); |
|
542 |
+ labelNode.put("isExclusive", label.category.isExclusive); |
|
543 |
+ labelNode.put("searchText", label.name + "/" + label.category.name); |
|
544 |
+ return labelNode; |
|
545 |
+ } |
|
488 | 546 |
} |
--- app/models/AbstractPosting.java
+++ app/models/AbstractPosting.java
... | ... | @@ -1,8 +1,9 @@ |
1 | 1 |
/** |
2 |
- * Yona, Project Hosting SW |
|
3 |
- * |
|
4 |
- * Copyright 2016 the original author or authors. |
|
5 |
- */ |
|
2 |
+ * Yona, 21st Century Project Hosting SW |
|
3 |
+ * <p> |
|
4 |
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. |
|
5 |
+ * https://yona.io |
|
6 |
+ **/ |
|
6 | 7 |
package models; |
7 | 8 |
|
8 | 9 |
import models.enumeration.ResourceType; |
... | ... | @@ -113,6 +114,7 @@ |
113 | 114 |
|
114 | 115 |
try { |
115 | 116 |
super.save(); |
117 |
+ TitleHead.saveTitleHeadKeyword(project, title); |
|
116 | 118 |
updateMention(); |
117 | 119 |
} catch (PersistenceException e) { |
118 | 120 |
Long oldNumber = number; |
... | ... | @@ -220,6 +222,7 @@ |
220 | 222 |
for (Comment comment: getComments()) { |
221 | 223 |
comment.delete(); |
222 | 224 |
} |
225 |
+ TitleHead.deleteTitleHeadKeyword(project, title); |
|
223 | 226 |
Attachment.deleteAll(asResource()); |
224 | 227 |
NotificationEvent.deleteBy(this.asResource()); |
225 | 228 |
super.delete(); |
+++ app/models/TitleHead.java
... | ... | @@ -0,0 +1,108 @@ |
1 | +/** | |
2 | + * Yona, 21st Century Project Hosting SW | |
3 | + * <p> | |
4 | + * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. | |
5 | + * https://yona.io | |
6 | + **/ | |
7 | +package models; | |
8 | + | |
9 | +import play.db.ebean.Model; | |
10 | +import utils.TemplateHelper; | |
11 | + | |
12 | +import javax.persistence.*; | |
13 | +import java.util.List; | |
14 | + | |
15 | +@Entity | |
16 | +public class TitleHead extends Model { | |
17 | + | |
18 | + private static final long serialVersionUID = 5194690128303455482L; | |
19 | + | |
20 | + public static final Finder<Long, TitleHead> finder = new Finder<>(Long.class, TitleHead.class); | |
21 | + | |
22 | + @Id | |
23 | + public Long id; | |
24 | + | |
25 | + @ManyToOne | |
26 | + public Project project; | |
27 | + | |
28 | + public String headKeyword; | |
29 | + | |
30 | + public int frequency; | |
31 | + | |
32 | + public static List<TitleHead> findByProject(Project project, String query) { | |
33 | + return finder.where() | |
34 | + .eq("project.id", project.id) | |
35 | + .ilike("headKeyword", "%" +query + "%") | |
36 | + .findList(); | |
37 | + } | |
38 | + | |
39 | + public static TitleHead findByHeadKeyword(Project project, String headKeyword) { | |
40 | + List<TitleHead> founds = finder.where() | |
41 | + .eq("project.id", project.id) | |
42 | + .eq("headKeyword", headKeyword).findList(); | |
43 | + if (founds.size() > 0) { | |
44 | + return founds.get(0); | |
45 | + } else { | |
46 | + return null; | |
47 | + } | |
48 | + } | |
49 | + | |
50 | + public static void newHeadKeyword(Project project, String headKeyword) { | |
51 | + TitleHead found = findByHeadKeyword(project, headKeyword); | |
52 | + if (found != null) { | |
53 | + found.frequency++; | |
54 | + found.update(); | |
55 | + } else { | |
56 | + TitleHead titleHeadKeyword = new TitleHead(); | |
57 | + titleHeadKeyword.project = project; | |
58 | + titleHeadKeyword.headKeyword = headKeyword; | |
59 | + titleHeadKeyword.frequency = 1; | |
60 | + titleHeadKeyword.save(); | |
61 | + } | |
62 | + } | |
63 | + | |
64 | + public static void reduceHeadKeyword(Project project, String headKeyword) { | |
65 | + TitleHead found = findByHeadKeyword(project, headKeyword); | |
66 | + if (found != null) { | |
67 | + found.frequency--; | |
68 | + if (found.frequency == 0) { | |
69 | + found.delete(); | |
70 | + } else { | |
71 | + found.update(); | |
72 | + } | |
73 | + } | |
74 | + } | |
75 | + | |
76 | + public static void saveTitleHeadKeyword(Project project, String title) { | |
77 | + String[] headKeywords = TemplateHelper.extractHeaderWordsInBrackets(title); | |
78 | + for (String headKeyword : headKeywords) { | |
79 | + String trimmed = headKeyword.trim(); | |
80 | + | |
81 | + if (isSurroundedByBracket(trimmed)) { | |
82 | + newHeadKeyword(project, removeBracket(trimmed)); | |
83 | + } | |
84 | + } | |
85 | + } | |
86 | + | |
87 | + private static String removeBracket(String trimmed) { | |
88 | + return trimmed.substring(1, trimmed.length() - 1); | |
89 | + } | |
90 | + | |
91 | + private static boolean isSurroundedByBracket(String trimmed) { | |
92 | + return trimmed.indexOf("[") == 0 | |
93 | + && trimmed.indexOf("]") == trimmed.length() - 1 | |
94 | + && trimmed.length() > 2; | |
95 | + } | |
96 | + | |
97 | + public static void deleteTitleHeadKeyword(Project project, String title) { | |
98 | + String[] headKeywords = TemplateHelper.extractHeaderWordsInBrackets(title); | |
99 | + for (String headKeyword : headKeywords) { | |
100 | + String trimmed = headKeyword.trim(); | |
101 | + | |
102 | + if (isSurroundedByBracket(trimmed)) { | |
103 | + reduceHeadKeyword(project, removeBracket(trimmed)); | |
104 | + } | |
105 | + } | |
106 | + } | |
107 | +} | |
108 | + |
--- app/utils/TemplateHelper.scala
+++ app/utils/TemplateHelper.scala
... | ... | @@ -636,7 +636,7 @@ |
636 | 636 |
StringUtils.isEmpty(title.replace(findHeaderWords(title),"").trim) |
637 | 637 |
} |
638 | 638 |
|
639 |
- private def extractHeaderWordsInBrackets(title: String): Array[String] = { |
|
639 |
+ def extractHeaderWordsInBrackets(title: String): Array[String] = { |
|
640 | 640 |
return title.split("(=\\[)|(?<=\\])") |
641 | 641 |
} |
642 | 642 |
|
--- app/views/board/create.scala.html
+++ app/views/board/create.scala.html
... | ... | @@ -121,6 +121,7 @@ |
121 | 121 |
<link rel="stylesheet" type="text/css" media="screen" href="@routes.Assets.at("javascripts/lib/atjs/jquery.atwho.css")"> |
122 | 122 |
<script type="text/javascript" src="@routes.Assets.at("javascripts/lib/atjs/jquery.caret.min.js")"></script> |
123 | 123 |
<script type="text/javascript" src="@routes.Assets.at("javascripts/lib/atjs/jquery.atwho.js")"></script> |
124 |
+<script type="text/jAvascript" src="@routes.Assets.at("javascripts/common/yona.TitleHeadAutoCompletion.js")"></script> |
|
124 | 125 |
<script type="text/javascript"> |
125 | 126 |
$(function(){ |
126 | 127 |
$yobi.loadModule("board.Write", { |
... | ... | @@ -153,6 +154,11 @@ |
153 | 154 |
$(".project-menu-gruop > li").removeClass("active"); |
154 | 155 |
$(".code-menu").addClass("active"); |
155 | 156 |
} |
157 |
+ |
|
158 |
+ yonaTitleHeadModule({ |
|
159 |
+ "target": 'input[id=title]', |
|
160 |
+ "url" : "@api.routes.ProjectApi.titleHeads(project.owner, project.name)" |
|
161 |
+ }); |
|
156 | 162 |
}); |
157 | 163 |
</script> |
158 | 164 |
} |
--- app/views/issue/create.scala.html
+++ app/views/issue/create.scala.html
... | ... | @@ -146,6 +146,7 @@ |
146 | 146 |
<script type="text/javascript" src="@routes.Assets.at("javascripts/lib/atjs/jquery.atwho.js")"></script> |
147 | 147 |
<script type="text/javascript" src="@routes.Assets.at("javascripts/common/yona.Subtask.js")"></script> |
148 | 148 |
<script type="text/javascript" src="@routes.Assets.at("javascripts/service/yona.issue.Assginee.js")"></script> |
149 |
+<script type="text/jAvascript" src="@routes.Assets.at("javascripts/common/yona.TitleHeadAutoCompletion.js")"></script> |
|
149 | 150 |
<script type="text/javascript"> |
150 | 151 |
$(function(){ |
151 | 152 |
// yobi.issue.Write |
... | ... | @@ -173,6 +174,11 @@ |
173 | 174 |
"", |
174 | 175 |
"@Messages("issue.assignee")" |
175 | 176 |
); |
177 |
+ |
|
178 |
+ yonaTitleHeadModule({ |
|
179 |
+ "target": 'input[id=title]', |
|
180 |
+ "url" : "@api.routes.ProjectApi.titleHeads(project.owner, project.name)" |
|
181 |
+ }); |
|
176 | 182 |
}); |
177 | 183 |
</script> |
178 | 184 |
} |
--- app/views/issue/edit.scala.html
+++ app/views/issue/edit.scala.html
... | ... | @@ -185,6 +185,7 @@ |
185 | 185 |
<script type="text/javascript" src="@routes.Assets.at("javascripts/lib/atjs/jquery.atwho.js")"></script> |
186 | 186 |
<script type="text/javascript" src="@routes.Assets.at("javascripts/common/yona.Subtask.js")"></script> |
187 | 187 |
<script type="text/javascript" src="@routes.Assets.at("javascripts/service/yona.issue.Assginee.js")"></script> |
188 |
+<script type="text/jAvascript" src="@routes.Assets.at("javascripts/common/yona.TitleHeadAutoCompletion.js")"></script> |
|
188 | 189 |
<script type="text/javascript"> |
189 | 190 |
$(function(){ |
190 | 191 |
// yobi.issue.Write |
... | ... | @@ -209,6 +210,11 @@ |
209 | 210 |
"", |
210 | 211 |
"@Messages("issue.assignee")" |
211 | 212 |
); |
213 |
+ |
|
214 |
+ yonaTitleHeadModule({ |
|
215 |
+ "target": 'input[id=title]', |
|
216 |
+ "url" : "@api.routes.ProjectApi.titleHeads(project.owner, project.name)" |
|
217 |
+ }); |
|
212 | 218 |
}); |
213 | 219 |
</script> |
214 | 220 |
|
--- app/views/issue/partial_select_label.scala.html
+++ app/views/issue/partial_select_label.scala.html
... | ... | @@ -27,7 +27,7 @@ |
27 | 27 |
@Messages("label") @if(UserApp.currentUser().isManagerOf(project)){<a href="@routes.IssueLabelApp.labelsForm(project.owner, project.name)" target="_blank" class="label-edit">[@Messages("button.edit")]</a>} |
28 | 28 |
</dt> |
29 | 29 |
<dd> |
30 |
- <select name="labelIds" multiple="multiple" data-search="labelIds" |
|
30 |
+ <select id="labelIds" 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 | 33 |
data-placeholder="@Messages("label.select")" @additionalAttr class="hide"> |
+++ conf/evolutions/default/21.sql
... | ... | @@ -0,0 +1,16 @@ |
1 | +# --- !Ups | |
2 | +CREATE TABLE title_head ( | |
3 | + id BIGINT AUTO_INCREMENT NOT NULL, | |
4 | + project_id BIGINT, | |
5 | + head_keyword VARCHAR(255), | |
6 | + frequency INTEGER, | |
7 | + CONSTRAINT pk_title_head PRIMARY KEY (id), | |
8 | + CONSTRAINT fk_title_head_project FOREIGN KEY (project_id) REFERENCES project (id) on DELETE CASCADE | |
9 | +) | |
10 | +row_format=compressed, key_block_size=8; | |
11 | + | |
12 | +CREATE index ix_title_head_project_id ON title_head (project_id); | |
13 | +CREATE index ix_title_head_head_keyword ON title_head (head_keyword); | |
14 | + | |
15 | +# --- !Downs | |
16 | +DROP TABLE title_head; |
--- conf/routes
+++ conf/routes
... | ... | @@ -63,6 +63,7 @@ |
63 | 63 |
POST /-_-api/v1/users controllers.api.UserApi.newUser() |
64 | 64 |
POST /-_-api/v1/owners/:owner/projects controllers.api.ProjectApi.newProject(owner:String) |
65 | 65 |
POST /-_-api/v1/owners/:owner/projects/:projectName/labels controllers.api.ProjectApi.newLabel(owner:String, projectName:String) |
66 |
+GET /-_-api/v1/owners/:owner/projects/:projectName/titleHeads controllers.api.ProjectApi.titleHeads(owner:String, projectName:String, query: String ?= "") |
|
66 | 67 |
|
67 | 68 |
POST /-_-api/v1/user/defultLoginPage controllers.UserApp.setDefaultLoginPage() |
68 | 69 |
POST /-_-api/v1/translation controllers.api.IssueApi.translate() |
+++ public/javascripts/common/yona.TitleHeadAutoCompletion.js
... | ... | @@ -0,0 +1,202 @@ |
1 | +/** | |
2 | + * Yona, 21st Century Project Hosting SW | |
3 | + * <p> | |
4 | + * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. | |
5 | + * https://yona.io | |
6 | + **/ | |
7 | +function yonaTitleHeadModule(htOptions){ | |
8 | + var htVar = {}; | |
9 | + var htElement = {}; | |
10 | + var issueLabels = []; | |
11 | + var projectLabels = getProjectLabels(); | |
12 | + | |
13 | + /** | |
14 | + * Initialize | |
15 | + * | |
16 | + * @param {Hash Table} htOptions | |
17 | + */ | |
18 | + function _init(htOptions){ | |
19 | + _initVar(htOptions); | |
20 | + _initElement(); | |
21 | + _attachEvent(); | |
22 | + | |
23 | + if($("#labelIds").length > 0 ) { | |
24 | + issueLabels = $("#labelIds").select2("val"); | |
25 | + } | |
26 | + } | |
27 | + | |
28 | + /** | |
29 | + * Initialize Variables | |
30 | + * | |
31 | + * @param {Hash Table} htOptions | |
32 | + */ | |
33 | + function _initVar(htOptions) { | |
34 | + htVar = htOptions || {}; // set htVar as htOptions | |
35 | + htVar.doesNotDataLoaded = true; | |
36 | + htVar.nKeyupEventGenerator = null; | |
37 | + htVar.sMentionText = null; | |
38 | + } | |
39 | + | |
40 | + /** | |
41 | + * Initialize Element variables | |
42 | + */ | |
43 | + function _initElement() { | |
44 | + if (!htVar.target) { | |
45 | + if (window.console) { | |
46 | + console.error("mention form element targeting doesn't exist!") | |
47 | + } | |
48 | + return; | |
49 | + } | |
50 | + htElement.welTarget = $(htVar.target); | |
51 | + } | |
52 | + /** | |
53 | + * attachEvent | |
54 | + */ | |
55 | + function _attachEvent() { | |
56 | + htElement.welTarget.on("keypress", _onKeyInput); | |
57 | + if (jQuery.browser.mozilla){ | |
58 | + htElement.welTarget.on("focus", _startKeyupEventGenerator); | |
59 | + htElement.welTarget.on("blur", _stopKeyupEventGenerator); | |
60 | + } | |
61 | + } | |
62 | + | |
63 | + /** | |
64 | + * Event handler on KeyInput event | |
65 | + * | |
66 | + * @param {Event} eEvt | |
67 | + */ | |
68 | + function _onKeyInput(eEvt){ | |
69 | + eEvt = eEvt || window.event; | |
70 | + | |
71 | + var charCode = eEvt.which || eEvt.keyCode; | |
72 | + if(charCode === 91) { // 91 = [ | |
73 | + if(htVar.doesNotDataLoaded) { | |
74 | + _findTitleHead(eEvt); | |
75 | + } | |
76 | + } | |
77 | + } | |
78 | + | |
79 | + function _startKeyupEventGenerator(){ | |
80 | + if (htVar.nKeyupEventGenerator){ | |
81 | + clearInterval(htVar.nKeyupEventGenerator); | |
82 | + } | |
83 | + | |
84 | + htVar.nKeyupEventGenerator = setInterval( | |
85 | + function(){ | |
86 | + if (htVar.sMentionText != htElement.welTarget.val()){ | |
87 | + htElement.welTarget.trigger("keyup"); | |
88 | + htVar.sMentionText = htElement.welTarget.val(); | |
89 | + } | |
90 | + } | |
91 | + ,100); | |
92 | + } | |
93 | + | |
94 | + function _stopKeyupEventGenerator(){ | |
95 | + if (htVar.nKeyupEventGenerator){ | |
96 | + clearInterval(htVar.nKeyupEventGenerator); | |
97 | + htVar.nKeyupEventGenerator = null; | |
98 | + } | |
99 | + } | |
100 | + | |
101 | + function _findTitleHead(){ | |
102 | + htVar.doesNotDataLoaded = false; | |
103 | + | |
104 | + var searchPending; | |
105 | + | |
106 | + htElement.welTarget | |
107 | + .atwho({ | |
108 | + at: "[", | |
109 | + limit: 10, | |
110 | + displayTpl: "<li data-value='@${name}' data-category-id='${categoryId}' data-is-exclusive='${isExclusive}' data-id='${id}'><small style='color: #${labelColor}'>${category}</small> ${name}</li>", | |
111 | + suspendOnComposing: false, | |
112 | + searchKey: "searchText", | |
113 | + suffix: "", | |
114 | + insertTpl: "[${name}]", | |
115 | + callbacks: { | |
116 | + remoteFilter: function(query, callback) { | |
117 | + NProgress.start(); | |
118 | + issueLabels = $("#labelIds").select2("val"); | |
119 | + clearTimeout(searchPending); | |
120 | + | |
121 | + searchPending = setTimeout(function () { | |
122 | + $.getJSON(htVar.url, { query: query }, function (data) { | |
123 | + NProgress.done(); | |
124 | + callback(data.result); | |
125 | + }); | |
126 | + }, 300); | |
127 | + }, | |
128 | + sorter: function(query, items, searchKey) { | |
129 | + var results = []; | |
130 | + | |
131 | + items.forEach(function (item) { | |
132 | + if (item[searchKey].toLowerCase().indexOf(query.toLowerCase()) !== -1) { | |
133 | + results.push(item); | |
134 | + } | |
135 | + }); | |
136 | + | |
137 | + return results.sort(function(a, b) { | |
138 | + if(b.frequency === a.frequency) { | |
139 | + if(b.category.toLowerCase() === a.category.toLowerCase()) { | |
140 | + return a.name.toLowerCase() >= b.name.toLowerCase() ? 1 : -1; | |
141 | + } else { | |
142 | + return a.category.toLowerCase() > b.category.toLowerCase() ? 1 : -1; | |
143 | + } | |
144 | + } | |
145 | + return b.frequency - a.frequency; | |
146 | + }); | |
147 | + }, | |
148 | + beforeInsert: function(value, $li, e) { | |
149 | + var category = $li.find("small").text(); | |
150 | + var selectedValueOnly = value.substring(1, value.length - 1); | |
151 | + var $labelField = $("#labelIds"); | |
152 | + | |
153 | + if (category && $labelField.length > 0) { | |
154 | + var $selectedLabel = $labelField.find("option[value=" + $li.data("id") + "]"); | |
155 | + $selectedLabel.prop('selected', true); | |
156 | + | |
157 | + if($li.data("isExclusive")){ | |
158 | + issueLabels = issueLabels.filter(function(label){ | |
159 | + return projectLabels[$li.data("categoryId")].indexOf(label) === -1 | |
160 | + }); | |
161 | + } | |
162 | + | |
163 | + issueLabels.push($selectedLabel.val()); | |
164 | + $("#labelIds").select2("val", issueLabels); | |
165 | + | |
166 | + $yobi.notify('Label: ' + selectedValueOnly, 3000); | |
167 | + return ""; | |
168 | + } else { | |
169 | + return value + " "; // intended white space | |
170 | + } | |
171 | + } | |
172 | + } | |
173 | + }); | |
174 | + } | |
175 | + | |
176 | + /** | |
177 | + * It gather all project labels. | |
178 | + * Category id is used for the key. | |
179 | + * Label ids are used for the value. | |
180 | + * { | |
181 | + * 31: [130, 120, ...], | |
182 | + * 70: [200, 201, 320, ...] | |
183 | + * } | |
184 | + */ | |
185 | + function getProjectLabels(){ | |
186 | + var allLabels = {}; | |
187 | + $("#labelIds > optgroup").each(function(){ | |
188 | + var allLabelsOfTheCategory = []; | |
189 | + var categoryId; | |
190 | + $(this).children().each(function(){ | |
191 | + $this = $(this); | |
192 | + allLabelsOfTheCategory.push($this.val()); | |
193 | + categoryId = $this.data("categoryId") | |
194 | + }); | |
195 | + | |
196 | + allLabels[categoryId] = allLabelsOfTheCategory; | |
197 | + }); | |
198 | + return allLabels; | |
199 | + } | |
200 | + | |
201 | + _init(htOptions || {}); | |
202 | +} |
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?