[Notice] Announcing the End of Demo Server [Read me]
JiHan Kim 2014-03-27
Issue: edit label on issueView
@a3d11d5cb14c45bde9d3adf5182f4b5c79375b21
app/assets/stylesheets/less/_override.less
--- app/assets/stylesheets/less/_override.less
+++ app/assets/stylesheets/less/_override.less
@@ -194,21 +194,43 @@
 }
 
 .select2-container-multi {
-
     .select2-choices {
-        border: 1px solid rgba(0,0,0,0.15);
+        background: none;
+        filter: none;
+        border: none;
 
         .select2-search-choice {
-            border: 1px solid rgba(0,0,0,0.15);
+            border: none;
             background-image:none;
             filter:none;
+        }
+    }
+
+    &.issue-labels {
+        width:auto !important;
+        min-width:220px;
+
+        .select2-choices {
+            .select2-search-choice {
+                background:transparent;
+                filter:none;
+                border:none;
+                .box-shadow(none);
+            }
+
+            .select2-search-choice-close {
+                top:6px;
+            }
         }
     }
 
     &.select2-container-active {
         .select2-choices {
             border: 1px solid rgba(0,0,0,0.15);
+            border-bottom:0;
             background-image:none;
+            outline:none;
+            .box-shadow(none);
         }
     }
 }
app/assets/stylesheets/less/_yobiUI.less
--- app/assets/stylesheets/less/_yobiUI.less
+++ app/assets/stylesheets/less/_yobiUI.less
@@ -325,6 +325,8 @@
     &:hover {
         .opacity(70);
     }
+    &.static.active { border-left:initial !important; }
+    &.static:hover  { .opacity(100); }
 }
 span.issue-label {
     display: inline-block;
app/controllers/IssueApp.java
--- app/controllers/IssueApp.java
+++ app/controllers/IssueApp.java
@@ -446,11 +446,15 @@
             }
 
             if (issueMassUpdate.attachingLabel != null) {
-                issue.labels.add(issueMassUpdate.attachingLabel);
+                for (IssueLabel label : issueMassUpdate.attachingLabel) {
+                    issue.labels.add(label);
+                }
             }
 
             if (issueMassUpdate.detachingLabel != null) {
-                issue.labels.remove(issueMassUpdate.detachingLabel);
+                for (IssueLabel label : issueMassUpdate.detachingLabel) {
+                    issue.labels.remove(label);
+                }
             }
 
             issue.updatedDate = JodaDateUtil.now();
app/models/IssueMassUpdate.java
--- app/models/IssueMassUpdate.java
+++ app/models/IssueMassUpdate.java
@@ -13,6 +13,6 @@
 
     @Constraints.Required
     public List<Issue> issues;
-    public IssueLabel attachingLabel;
-    public IssueLabel detachingLabel;
+    public List<IssueLabel> attachingLabel;
+    public List<IssueLabel> detachingLabel;
 }
app/views/issue/partial_massupdate.scala.html
--- app/views/issue/partial_massupdate.scala.html
+++ app/views/issue/partial_massupdate.scala.html
@@ -79,7 +79,7 @@
         </ul>
     </div>
 
-    <div id="attaching-label" class="btn-group" data-name="attachingLabel.id">
+    <div id="attaching-label" class="btn-group" data-name="attachingLabel[0].id">
         <button class="btn dropdown-toggle medium" data-toggle="dropdown" disabled="disabled">
             <span class="d-label">@Messages("issue.update.attachLabel")</span>
             <span class="d-caret"><span class="caret"></span></span>
@@ -89,7 +89,7 @@
         </ul>
     </div>
 
-    <div id="detaching-label" class="btn-group" data-name="detachingLabel.id">
+    <div id="detaching-label" class="btn-group" data-name="detachingLabel[0].id">
         <button class="btn dropdown-toggle medium" data-toggle="dropdown" disabled="disabled">
             <span class="d-label">@Messages("issue.update.detachLabel")</span>
             <span class="d-caret"><span class="caret"></span></span>
app/views/issue/view.scala.html
--- app/views/issue/view.scala.html
+++ app/views/issue/view.scala.html
@@ -40,14 +40,35 @@
         <!--board-body-->
         <div class="board-body row-fluid">
             <div class="span9">
+                @if(IssueLabel.findByProject(project).size > 0){
                 <div class="pull-right" style="padding:20px;">
                     @**<!-- labels -->**@
                     <p class="labels-wrap">
-                    @for(label <- issue.labels.toList.sortBy(r => (r.category, r.name))) {
-                        <a href='@routes.IssueApp.issues(project.owner, project.name, issue.state.state(), "html", 1)&labelIds=@label.id' class="label issue-label" data-labelId="@label.id" data-color="@label.color" style="background:@label.color">@label.name</a>
+                    @if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE)) {
+                      <select id="issueLabels" multiple="multiple" data-placeholder="@Messages("label.select")"
+                              data-toggle="select2" data-format="issuelabel" data-containerCSSClass="issue-labels">
+                      <option></option>
+                      @IssueLabel.findByProject(project).groupBy(_.category).map {
+                        case (category, labels) => {
+                          <optgroup label="@category">
+                          @labels.map { label =>
+                            <option value="@label.id" data-category="@category" @if(issue.labels.contains(label)){ selected }>
+                              @label.name
+                            </option>
+                          }
+                          </optgroup>
+                        }
+                      }
+                      </select>
+                    } else {
+                      @for(label <- issue.labels.toList.sortBy(r => (r.category, r.name))) {
+                        <a href='@routes.IssueApp.issues(project.owner, project.name, issue.state.state(), "html", 1)&labelIds=@label.id' class="label issue-label active static" data-labelId="@label.id" data-color="@label.color" style="background:@label.color">@label.name</a>
+                      }
                     }
                     </p>
                 </div>
+                }
+
                 @if(StringUtils.isEmpty(issue.body)){
                 <div class="content empty-content"></div>
                 } else {
@@ -63,7 +84,7 @@
 
                         @**<!-- author  -->**@
                         <dl class="author">
-                            <dt>@Messages("igitssue.author")</dt>
+                            <dt>@Messages("issue.author")</dt>
                             <dd style="padding:5px 10px;">
                                 <a href="@routes.UserApp.userInfo(issue.authorLoginId)" class="usf-group">
                                     <span class="avatar-wrap smaller">
conf/messages
--- conf/messages
+++ conf/messages
@@ -237,6 +237,7 @@
 issue.update.assignee = Update assignee
 issue.update.attachLabel = Attach label
 issue.update.detachLabel = Detach label
+issue.update.label = Update label
 issue.update.milestone = Update milestone
 issue.update.state = Update state
 issue.vote = Sympathy
conf/messages.ko
--- conf/messages.ko
+++ conf/messages.ko
@@ -237,6 +237,7 @@
 issue.update.assignee = 담당자 변경
 issue.update.attachLabel = 라벨 추가
 issue.update.detachLabel = 라벨 제거
+issue.update.label = 라벨 변경
 issue.update.milestone = 마일스톤 변경
 issue.update.state = 상태 변경
 issue.vote = 공감
public/javascripts/common/yobi.ui.Select2.js
--- public/javascripts/common/yobi.ui.Select2.js
+++ public/javascripts/common/yobi.ui.Select2.js
@@ -33,7 +33,10 @@
         var welSelect = $(elSelect);
 
         // Select2.js default options
-        var htOpt = $.extend({"width": "resolve"}, htOptions);
+        var htOpt = $.extend({
+            "width": "resolve",
+            "containerCssClass": welSelect.data("containercssclass")
+        }, htOptions);
 
         // Customized formats
         var htFormat = {
@@ -79,6 +82,16 @@
                 });
 
                 return sText;
+            },
+            "issuelabel": function(oItem){
+                var welItem = $(oItem.element);
+                var sLabelId = welItem.val();
+
+                if(!sLabelId){
+                    return '<span>' + oItem.text.trim() + '</span>';
+                }
+
+                return '<a class="label issue-label active static" data-labelid="' + sLabelId + '">' + oItem.text.trim() + '</a>';
             }
         };
 
public/javascripts/service/yobi.issue.View.js
--- public/javascripts/service/yobi.issue.View.js
+++ public/javascripts/service/yobi.issue.View.js
@@ -41,11 +41,11 @@
 
             htElement.welBtnWatch = $('#watch-button');
 
+            htElement.welIssueLabels = $("#issueLabels");
             htElement.welAssignee = htOptions.welAssignee || $("#assignee");
             htElement.welMilestone = htOptions.welMilestone || $("#milestone");
             htElement.welIssueUpdateForm = htOptions.welIssueUpdateForm;
 
-            htElement.welChkIssueOpen = $("#issueOpen");
             htElement.welTimelineWrap = $("#timeline");
             htElement.welTimelineList = htElement.welTimelineWrap.find(".timeline-list");
         }
@@ -75,6 +75,9 @@
             htVar.sNextState = htOptions.sNextState;
             htVar.sNextStateUrl = htOptions.sNextStateUrl;
             htVar.sCommentWithStateUrl = htOptions.sCommentWithStateUrl;
+
+            // for label update
+            htVar.aLatestLabelIds = htElement.welIssueLabels.val();
         }
 
         /**
@@ -85,9 +88,9 @@
             htElement.welBtnWatch.click(_onClickBtnWatch);
 
             // 이슈 정보 업데이트
-            htElement.welChkIssueOpen.change(_onChangeIssueOpen);
             htElement.welAssignee.on("change", _onChangeAssignee);
             htElement.welMilestone.on("change", _onChangeMilestone);
+            htElement.welIssueLabels.on("change", _onChangeIssueLabels);
 
             // 타임라인 자동업데이트를 위한 정보
             if(htElement.welTextarea.length > 0){
@@ -134,27 +137,113 @@
         }
 
         /**
-         * 이슈 해결/미해결 스위치 변경시
+         * 이슈 라벨 변경시
+         * change 이벤트 핸들러
+         *
+         * @param weEvt
+         * @private
          */
-        function _onChangeIssueOpen(){
-            var welTarget  = $(this);
-            var bChecked   = welTarget.prop("checked");
-            var sNextState = bChecked ? "OPEN" : "CLOSED";
+        function _onChangeIssueLabels(weEvt){
+            var htReqData = _getRequestDataForUpdateIssueLabel(weEvt);
 
+            // 업데이트 요청 전송
             _requestUpdateIssue({
-               "htData" : {"state": sNextState},
-               "fOnLoad": function(){
-                    welTarget.prop("checked", bChecked);
-                    _updateTimeline();
-                },
+               "htData"  : htReqData,
+               "fOnLoad" : function(){
+                   $yobi.notify(Messages("issue.update.label"), 3000);
+               },
                "fOnError": function(oRes){
-                    welTarget.prop("checked", !bChecked);
-                    _onErrorRequest(Messages("issue.update.state"), oRes);
+                   _onErrorRequest(Messages("issue.update.label"), oRes);
                }
             });
         }
 
         /**
+         * 이슈 라벨 변경 요청 데이터를 반환
+         *
+         * @param weEvt
+         * @returns {Hash Table}
+         * @private
+         */
+        function _getRequestDataForUpdateIssueLabel(weEvt){
+            var htReqData = {};
+
+            // 삭제할 라벨
+            htReqData["detachingLabel[0].id"] = _getIdPropFromObject(weEvt.removed);
+
+            // 추가할 라벨
+            htReqData["attachingLabel[0].id"] = _getIdPropFromObject(weEvt.added);
+
+            // 추가하는 라벨이 있는 경우
+            // 해당 라벨을 추가함으로 인해 삭제해야 하는 라벨을 찾아 htReqData 에 넣는다
+            if(htReqData["attachingLabel[0].id"]){
+                var htRemove = _getLabelsToRemovedByAdding(weEvt.added);
+                htReqData = $.extend(htReqData, htRemove);
+            }
+
+            return htReqData;
+        }
+
+        /**
+         * {@code htItem}의 id 속성을 반환한다
+         *
+         * @param htItem
+         * @returns {*}
+         * @private
+         */
+        function _getIdPropFromObject(htItem){
+            return (htItem && htItem.id) ? htItem.id : undefined;
+        }
+
+        /**
+         * {@code htLabel} 을 추가함으로 인해 삭제해야 하는 라벨을 찾아 그 정보를 반환한다
+         *
+         * @param htLabel
+         * @private
+         * @return {Hash Table}
+         */
+        function _getLabelsToRemovedByAdding(htLabel){
+            var htRemove = {};
+            var oIssueLabels = htElement.welIssueLabels.data("select2");
+            var aIssueLabelValues = oIssueLabels.val();
+            var aRemoveLabelIds = _getLabelInSameCategoryWith(oIssueLabels.data(), htLabel);
+
+            // 삭제할 항목으로 추가하고
+            aRemoveLabelIds.forEach(function(nValue, nIndex){
+                htRemove["detachingLabel[" + (nIndex + 1) + "].id"] = nValue;
+                aIssueLabelValues.splice(aIssueLabelValues.indexOf(nValue), 1);
+            });
+
+            // 해당 항목이 제거된 상태로 Select2 값 설정
+            oIssueLabels.val(aIssueLabelValues);
+
+            return htRemove;
+        }
+
+        /**
+         * {@code aData} 를 기준으로 {@code htAddedLabel}과 같은 카테고리의 항목을 반환한다
+         *
+         * @param aData
+         * @param htAddedLabel
+         * @private
+         * @returns {Array}
+         */
+        function _getLabelInSameCategoryWith(aData, htAddedLabel){
+            var aLabelIds = [];
+            var sAddedCategory = $(htAddedLabel.element).data("category");
+
+            aData.forEach(function(htData){
+                var sCategory = $(htData.element).data("category");
+
+                if(htData.id !== htAddedLabel.id && sCategory === sAddedCategory){
+                    aLabelIds.push(htData.id);
+                }
+            });
+
+            return aLabelIds;
+        }
+
+        /**
          * 담당자 변경시
          *
          * @param {Wrapped Event} weEvt "change" 이벤트
Add a comment
List