doortts doortts 2018-01-29
issue: Issue sharing feature - view and message
@49812f7abaee35b151f57e8affd3ea5bf11aeb6c
app/assets/stylesheets/less/_common.less
--- app/assets/stylesheets/less/_common.less
+++ app/assets/stylesheets/less/_common.less
@@ -219,6 +219,7 @@
 .mt4 { margin-top:4px; }
 .mr3 { margin-right:3px; }
 .pb4 { padding-bottom: 4px}
+.pl0 { padding-left: 0}
 
 .margin-top-20   { margin-top:20px;   }
 .margin-left-20  { margin-left: 20px; }
@@ -281,3 +282,21 @@
 .va-text-top {
     vertical-align: text-top !important;
 }
+
+.width100p {
+    width: 100%
+}
+
+.text-ellipsis {
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+.z-index-1 {
+    z-index: 1 !important;
+}
+
+.hideFromDisplayOnly {
+    display: none;
+}
app/assets/stylesheets/less/_override.less
--- app/assets/stylesheets/less/_override.less
+++ app/assets/stylesheets/less/_override.less
@@ -182,6 +182,23 @@
     .box-shadow(none);
 }
 
+.sharer-list {
+    .select2-container{
+        border: none;
+        box-shadow: none;
+        border-radius: 0 !important;
+        border-bottom: 1px solid rgba(0, 0, 0, 0.15);
+    }
+    .select2-container-multi {
+        .select2-choices {
+            .select2-search-choice {
+                background-color: #ececec;
+                border: 1px solid #dfdfdf;
+            }
+        }
+    }
+}
+
 .select2-dropdown-open {
     .select2-choice {
         border-bottom-color: transparent;
app/assets/stylesheets/less/_page.less
--- app/assets/stylesheets/less/_page.less
+++ app/assets/stylesheets/less/_page.less
@@ -3745,8 +3745,15 @@
     .comments-count-color {
         color: @darkmagenta;
     }
+    .sharer-color {
+        color: @green;
+    }
 
     a:nth-child(2) {
+        margin-left: -5px;
+    }
+
+    a:nth-child(3) {
         margin-left: -5px;
     }
 
@@ -7070,3 +7077,19 @@
         border-radius: 4px;
     }
 }
+
+.sharer-list {
+    margin-top: 40px;
+    padding: 10px;
+
+    .issue-share-title {
+        font-size: 16px;
+    }
+    .sharer-item{
+        display: inline-block;
+        background-color: #ececec;
+        border: 1px solid #dfdfdf;
+        border-radius: 3px;
+        padding: 1px 8px;
+    }
+}
 
app/views/common/sharerCount.scala.html (added)
+++ app/views/common/sharerCount.scala.html
@@ -0,0 +1,11 @@
+@**
+* Yona, 21st Century Project Hosting SW
+*
+* Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp.
+* https://yona.io
+**@
+@(countNumber:Int, showColorAlways:Boolean = false)
+<a class="@if(showColorAlways){sharer-color}" data-toggle="tooltip" data-placement="bottom" title="@Messages("issue.sharer")">
+    <span class="count-groups item-icon">
+        <i class="yobicon-friends"></i>
+    </span><span class="count-groups item-count strong">@countNumber</span></a>
app/views/issue/partial_list.scala.html
--- app/views/issue/partial_list.scala.html
+++ app/views/issue/partial_list.scala.html
@@ -62,7 +62,7 @@
                     </span>
                     }
 
-                    @if(issue.comments.size>0 || issue.voters.size>0) {
+                    @if(issue.comments.size > 0 || issue.voters.size > 0 || issue.sharers.size > 0) {
                     <span class="infos-item item-count-groups">
                     @if(issue.comments.size>0){
                         @views.html.common.commentCount(routes.IssueApp.issue(project.owner, project.name, issue.getNumber).toString + "#comments", issue.comments.size, true)
@@ -70,6 +70,9 @@
                     @if(issue.voters.size>0){
                         @views.html.common.voteCount(routes.IssueApp.issue(project.owner, project.name, issue.getNumber).toString + "#vote", issue.voters.size, true)
                     }
+                    @if(issue.sharers.size > 0){
+                        @views.html.common.sharerCount(issue.sharers.size, true)
+                    }
                     </span>
                     }
 
app/views/issue/view.scala.html
--- app/views/issue/view.scala.html
+++ app/views/issue/view.scala.html
@@ -5,6 +5,7 @@
 * https://yona.io
 **@
 @(title:String, issue:Issue, issueForm: play.data.Form[Issue], commentForm: play.data.Form[Comment],project:Project)
+@import scala.collection.mutable.ArrayBuffer
 @import org.apache.commons.lang.StringUtils
 @import models.enumeration.ResourceType
 @import models.enumeration.Operation
@@ -56,6 +57,22 @@
     } else {
         issue.id
     }
+}
+
+@hasAssignee = @{
+    issue.assigneeName != null
+}
+
+@hasSharer = @{
+    issue.sharers.size > 0
+}
+
+@sharers = @{
+    var sharerIds = ArrayBuffer[String]()
+    for( sharedUser <- issue.sharers ) {
+        sharerIds += sharedUser.loginId
+    }
+    sharerIds.mkString(",")
 }
 
 @VOTER_AVATAR_SHOW_LIMIT = @{ 5 }
@@ -175,6 +192,9 @@
                                 }
                                 </button>
                             }
+                            @if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE) && !hasSharer) {
+                                <button id="issue-share-button" type="button" class="ybtn">@Messages("button.share.issue")</button>
+                            }
 
                         </div>
                     </div>
@@ -219,6 +239,24 @@
                     }
                     </span>
                 </div>
+                <dl class="sharer-list @if(!hasSharer){hideFromDisplayOnly}">
+                    <dt class="issue-share-title mb10">
+                        @Messages("issue.sharer") <span class="num issue-sharer-count">@if(issue.sharers.size > 0) { @issue.sharers.size }</span>
+                    </dt>
+                    <dd id="sharer-list" class="@if(!hasSharer){hideFromDisplayOnly}">
+                    @if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE)) {
+                        <input type="hidden" class="bigdrop width100p" id="issueSharer" name="issueSharer" placeholder="@Messages("issue.sharer.select")" value="@sharers" title="">
+                    } else {
+                        @for(sharer <- issue.getSortedSharer){
+                            <div class="text-ellipsis sharer-item">
+                                <a href="@userInfo(sharer.loginId)" class="usf-group">
+                                    <strong class="name">@sharer.user.getDisplayName</strong>
+                                </a>
+                            </div>
+                        }
+                    }
+                    </dd>
+                </dl>
                 <div class="watcher-list"></div>
                 <div class="subtasks">
                 @if(issue.parent == null) {
@@ -254,23 +292,21 @@
                             <dt>@Messages("issue.assignee")</dt>
 
                             <dd>
-                            @defining(issue.assigneeName != null) { isAssigned =>
-                                @if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE)) {
-                                    @partial_assignee(project, issue)
+                            @if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE)) {
+                                @partial_assignee(project, issue)
+                            } else {
+                                @if(hasAssignee){
+                                <a href="@userInfo(issue.assignee.user.loginId)" class="usf-group">
+                                    <span class="avatar-wrap smaller">
+                                        <img src="@User.findByLoginId(issue.assignee.user.loginId).avatarUrl" width="20" height="20">
+                                    </span>
+                                    <strong class="name">@issue.assignee.user.getDisplayName</strong>
+                                    <span class="loginid"> <strong>@{"@"}</strong>@issue.assignee.user.loginId</span>
+                                </a>
                                 } else {
-                                    @if(isAssigned){
-                                    <a href="@userInfo(issue.assignee.user.loginId)" class="usf-group">
-                                        <span class="avatar-wrap smaller">
-                                            <img src="@User.findByLoginId(issue.assignee.user.loginId).avatarUrl" width="20" height="20">
-                                        </span>
-                                        <strong class="name">@issue.assignee.user.getDisplayName</strong>
-                                        <span class="loginid"> <strong>@{"@"}</strong>@issue.assignee.user.loginId</span>
-                                    </a>
-                                    } else {
-                                    <div>
-                                       @Messages("issue.noAssignee")
-                                    </div>
-                                    }
+                                <div>
+                                   @Messages("issue.noAssignee")
+                                </div>
                                 }
                             }
                             </dd>
@@ -427,6 +463,7 @@
 <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>
 <script type="text/javascript" src="@routes.Assets.at("javascripts/service/yona.issue.Assginee.js")"></script>
+<script type="text/javascript" src="@routes.Assets.at("javascripts/service/yona.issue.Sharer.js")"></script>
 <script type="text/javascript">
     $(function(){
         // yobi.issue.View
@@ -471,6 +508,18 @@
                     "@Messages("issue.assignee")"
             );
 
+            yonaIssueSharerModule(
+                    "@api.routes.IssueApi.findSharerByloginIds(project.owner, project.name, issue.getNumber)",
+                    "@api.routes.IssueApi.findSharableUsers(project.owner, project.name, issue.getNumber)",
+                    "@api.routes.IssueApi.updateSharer(project.owner, project.name, issue.getNumber)",
+                    "@Messages("issue.sharer")"
+            );
+
+            $('#issue-share-button').on('click', function () {
+                $('#sharer-list').show();
+                $('.sharer-list').show();
+            });
+
             $('#translate').one('click', function (e) {
                 var data = {
                     owner: "@project.owner",
conf/messages
--- conf/messages
+++ conf/messages
@@ -75,6 +75,8 @@
 button.selectFile = Select file
 button.setDefaultLoginPage = Set to default page
 button.setDefaultLoginPage.desc = Make current page to index page when logged in
+button.share.issue = Issue Sharing
+button.share.issue.desc = Allow others can see this issue and receive notifications
 button.show.original = See text
 button.signup = Sign up for {0}
 button.submitForm = Submit form
@@ -302,6 +304,8 @@
 issue.noMilestone = No milestone
 issue.option = Option
 issue.search = Search Issues
+issue.sharer = Issue Sharer
+issue.sharer.select = Select Issue Sharer
 issue.state = Status
 issue.state.all = All
 issue.state.assigned = Assigned
conf/messages.ko-KR
--- conf/messages.ko-KR
+++ conf/messages.ko-KR
@@ -75,6 +75,8 @@
 button.selectFile = 파일 선택
 button.setDefaultLoginPage = 기본 페이지로 지정
 button.setDefaultLoginPage.desc = 현재 페이지를 로그인 후 표시되는 기본 인덱스 페이지로 지정합니다
+button.share.issue = 이슈 공유
+button.share.issue.desc = 이 이슈에 대한 접근과 알림을 허용할 사용자를 지정합니다
 button.show.original = 원문 보기
 button.signup = {0} 시작 하기
 button.submitForm = 폼 전송
@@ -302,6 +304,8 @@
 issue.noMilestone = 마일스톤 없음
 issue.option = 이슈 옵션
 issue.search = 이슈 검색
+issue.sharer = 이슈 공유
+issue.sharer.select = 이슈 공유 대상 선택
 issue.state = 상태
 issue.state.all = 전체
 issue.state.assigned = 할당됨
 
public/javascripts/service/yona.issue.Sharer.js (added)
+++ public/javascripts/service/yona.issue.Sharer.js
@@ -0,0 +1,113 @@
+/**
+ * Yona, 21st Century Project Hosting SW
+ * <p>
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp.
+ * https://yona.io
+ **/
+
+function yonaIssueSharerModule(findUsersByloginIdsApiUrl, findSharableUsersApiUrl, updateSharingApiUrl, message){
+  function formatter(result){
+    if(!result.avatarUrl){
+      return "<div>" + result.name + "</div>";
+    }
+
+    // Template text. Also you can use predefined template: $("#tplSelect2FormatUser").text()
+    var tplUserItem = "<div class='usf-group' title='${name} ${loginId}'>" +
+        "<strong class='name'>${name}</strong>";
+
+    var formattedResult = $yobi.tmpl(tplUserItem, {
+      "avatarURL": result.avatarUrl,
+      "name"     : result.name,
+      "loginId"  : result.loginId
+    });
+
+    return formattedResult;
+  }
+
+  function matcher(term, formattedResult, result){
+    term = term.toLowerCase();
+    formattedResult = formattedResult.toLowerCase();
+
+    var loginId = (typeof result.loginId !== "undefined") ? result.loginId.toLowerCase() : "";
+
+    return (loginId.indexOf(term) > -1) || (formattedResult.indexOf(term) > -1);
+  }
+
+  var $issueSharer = $("#issueSharer");
+  $issueSharer.select2({
+    minimumInputLength: 1,
+    multiple: true,
+    id: function(obj) {
+      return obj.loginId; // use slug field for id
+    },
+    ajax: { // instead of writing the function to execute the request we use Select2's convenient helper
+      url: findSharableUsersApiUrl,
+      dataType: "json",
+      quietMillis: 300,
+      data: function (term, page) {
+        return {
+          query: term, // search term
+        };
+      },
+      results: function (data, page) { // parse the results into the format expected by Select2.
+        // since we are using custom formatting functions we do not need to alter the remote JSON data
+        return { results: data };
+      },
+      cache: true
+    },
+    initSelection: function(element, callback) {
+      // the input tag has a value attribute preloaded that points to a preselected repository's id
+      // this function resolves that id attribute to an object that select2 can render
+      // using its formatResult renderer - that way the repository name is shown preselected
+
+      var ids = $(element).val();
+      if (ids !== "") {
+        $.ajax(findUsersByloginIdsApiUrl+ "?query=" + ids, {
+          dataType: "json"
+        }).done(function(data) {
+          if(data && data.length > 0) {
+            callback(data);
+          }
+        });
+      }
+    },
+    formatResult: formatter, // omitted for brevity, see the source of this page
+    formatSelection: formatter,  // omitted for brevity, see the source of this page
+    matcher: matcher,
+    escapeMarkup: function (m) { return m; } // we do not want to escape markup since we are displaying html in results
+  });
+
+  $issueSharer.on("select2-selecting", function(selected) {
+    var data = { sharer: [selected.val], action: 'add' };
+
+    if(updateSharingApiUrl){
+        $.ajax(updateSharingApiUrl, {
+            method: "POST",
+            dataType: "json",
+            contentType: "application/json",
+            data: JSON.stringify(data)
+        }).done(function(response){
+            $yobi.notify(response.action + ": " + response.sharer, 3000);
+        });
+    }
+  });
+
+  $issueSharer.on("select2-removing", function(selected) {
+    var data = { sharer: [selected.val], action: 'delete' };
+
+    if(updateSharingApiUrl){
+      $.ajax(updateSharingApiUrl, {
+        method: "POST",
+        dataType: "json",
+        contentType: "application/json",
+        data: JSON.stringify(data)
+      }).done(function(response){
+        $yobi.notify(response.action + ": " + response.sharer, 3000);
+      });
+    }
+  });
+
+  $issueSharer.on('change', function (e) {
+    $(".issue-sharer-count").text(e.val.length);
+  });
+}
Add a comment
List