[Notice] Announcing the End of Demo Server [Read me]
doortts doortts 2018-10-22
comment: Support attachments modification
@cca1b798fa5b993207d228bd48206e5b708bb553
app/assets/stylesheets/less/_page.less
--- app/assets/stylesheets/less/_page.less
+++ app/assets/stylesheets/less/_page.less
@@ -3308,7 +3308,6 @@
     font-family:@base-font-family;
 
     .write-comment-wrap {
-        margin-bottom: 10px;
         position: relative;
         .nav {
           margin-bottom: 0;
@@ -3461,77 +3460,76 @@
         margin-top: 15px;
         border-top: 1px solid #e0e0e0;
         padding: 15px 0px;
-
-        .attached-file {
-            display: inline-block;
-            width: 355px;
-            height: 30px;
-            line-height: 30px;
-            border: 1px solid #ccc;
-            background: #fafafa;
-            padding: 0 10px;
-            margin-bottom: 5px;
-            margin-right: 4px;
-            cursor:pointer;
-            overflow: hidden;
-            -webkit-transition-duration: 0.5s;
-            &:hover { border:1px solid @primary; }
-
-            i {
-                display:none;
-                margin-right:3px;
-                vertical-align: middle;
-                color:@yobi-blue;
-            }
-            .name {
-                display: inline-block;
-                max-width: 120px;
-                overflow: hidden;
-                text-overflow: ellipsis;
-                white-space:nowrap;
-                vertical-align:middle;
-                -webkit-transition-duration: 0.5s;
-            }
-            .size { font-size:11px; vertical-align:middle; }
-
-            /* 업로드 하는 도중에는 진행 상태 표시 */
-            .progress {
-                display: inline-block;
-                width: 100px;
-                height: 7px;
-                margin: 0;
-                overflow: hidden;
-            }
-            .btn-delete {
-                display:none;
-                width: 30px;
-                height: 30px;
-                font-size: 1.5em;
-                font-weight: bold;
-                &:hover { color:@primary; }
-            }
-            .btn-insert {
-                display:none;
-                line-height: 20px;
-                margin-top: 2px; margin-right: 10px;
-                .box-shadow(none);
-                &:hover { background:#fff; color:@primary; }
-            }
-
-            /* 업로드 완료 후에는 삭제 버튼 표시 */
-            &.complete {
-               .progress    { display:none; }
-               .btn-delete  { display:inline-block; }
-               .btn-insert  { display:block; }
-            }
-
-            /* 임시 파일 표시 */
-            &.temporary {
-               i { display:inline-block; }
-            }
-        }
     }
 }
+
+.attached-file {
+    display: inline-block;
+    max-width: 50%;
+    height: 30px;
+    line-height: 30px;
+    border: 1px solid #ccc;
+    background: #fafafa;
+    padding: 0 10px;
+    margin: 5px 4px;
+    cursor:pointer;
+    overflow: hidden;
+    -webkit-transition-duration: 0.5s;
+    &:hover { border:1px solid @primary; }
+
+    i {
+        display:none;
+        margin-right:3px;
+        vertical-align: middle;
+        color:@yobi-blue;
+    }
+    .name {
+        display: inline-block;
+        max-width: 250px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space:nowrap;
+        vertical-align:middle;
+        -webkit-transition-duration: 0.5s;
+    }
+    .size { font-size:11px; vertical-align:middle; }
+
+    /* 업로드 하는 도중에는 진행 상태 표시 */
+    .progress {
+        display: inline-block;
+        width: 100px;
+        height: 7px;
+        margin: 0;
+        overflow: hidden;
+    }
+    .btn-delete {
+        width: 30px;
+        height: 30px;
+        font-size: 1.5em;
+        font-weight: bold;
+        &:hover { color:@primary; }
+    }
+    .btn-insert {
+        display:none;
+        line-height: 20px;
+        margin-top: 2px; margin-right: 10px;
+        .box-shadow(none);
+        &:hover { background:#fff; color:@primary; }
+    }
+
+    /* 업로드 완료 후에는 삭제 버튼 표시 */
+    &.complete {
+        .progress    { display:none; }
+        .btn-delete  { display:inline-block; }
+        .btn-insert  { display:block; }
+    }
+
+    /* 임시 파일 표시 */
+    &.temporary {
+        i { display:inline-block; }
+    }
+}
+
 .upload-drop-here {
     position: absolute;
     top: 2px;
@@ -7428,3 +7426,28 @@
         }
     }
 }
+
+.upload-button-line {
+    .file-upload {
+        position: relative;
+        display: inline-block;
+    }
+
+    .file-upload__label {
+        display: block;
+        border-radius: 2px;
+        transition: background .3s;
+    }
+
+    .file-upload__input {
+        position: fixed;
+        left: 0;
+        top: 0;
+        right: 0;
+        height: 0;
+        bottom: 0;
+        width:0;
+        opacity: 0;
+    }
+
+}
app/controllers/AttachmentApp.java
--- app/controllers/AttachmentApp.java
+++ app/controllers/AttachmentApp.java
@@ -16,6 +16,7 @@
 import play.Configuration;
 import play.Logger;
 import play.mvc.Controller;
+import play.mvc.Http;
 import play.mvc.Http.MultipartFormData.FilePart;
 import play.mvc.Result;
 import utils.AccessControl;
@@ -168,11 +169,15 @@
 
     public static Result deleteFile(Long id) {
         // _method must be 'delete'
-        Map<String, String[]> data =
-                request().body().asMultipartFormData().asFormUrlEncoded();
-        if (!HttpUtil.getFirstValueFromQuery(data, "_method").toLowerCase()
-                .equals("delete")) {
-            return badRequest("_method must be 'delete'.");
+        Http.MultipartFormData formData = request().body().asMultipartFormData();
+
+        if( formData != null ) {
+            Map<String, String[]> data =
+                    formData.asFormUrlEncoded();
+            if (!HttpUtil.getFirstValueFromQuery(data, "_method").toLowerCase()
+                    .equals("delete")) {
+                return badRequest("_method must be 'delete'.");
+            }
         }
 
         // Remove the attachment.
@@ -222,6 +227,11 @@
         return metadata;
     }
 
+    public static Map<String, List<Map<String, String>>> getFileList(ResourceType resourceType, Long containerId)
+            throws PermissionDeniedException {
+        return getFileList(resourceType.toString(), String.valueOf(containerId));
+    }
+
     public static Map<String, List<Map<String, String>>> getFileList(String containerType, String
             containerId) throws PermissionDeniedException {
         Map<String, List<Map<String, String>>> files =
app/views/board/partial_comments.scala.html
--- app/views/board/partial_comments.scala.html
+++ app/views/board/partial_comments.scala.html
@@ -67,7 +67,7 @@
                     <div id="comment-body-@comment.id">
                         @common.tasklistBar()
                         <div class="comment-body markdown-wrap" data-via-email="@OriginalEmail.exists(comment.asResource)" data-allowed-update="@isAllowedUpdate" >@Html(Markdown.render(comment.contents, project))</div>
-                        <div class="attachments pull-right" data-attachments="@toJson(AttachmentApp.getFileList(ResourceType.NONISSUE_COMMENT.toString(), comment.id.toString()))"></div>
+                        <div class="attachments" data-attachments="@toJson(AttachmentApp.getFileList(ResourceType.NONISSUE_COMMENT.toString(), comment.id.toString()))"></div>
                     </div>
                 </div>
                 @common.childComments(post, comment, ResourceType.NONISSUE_COMMENT)
app/views/board/view.scala.html
--- app/views/board/view.scala.html
+++ app/views/board/view.scala.html
@@ -212,6 +212,7 @@
 <script type="text/javascript" src="@routes.Assets.at("javascripts/common/yona.Sha1.js")"></script>
 <script type="text/javascript" src="@routes.Assets.at("javascripts/common/yona.Tasklist.js")"></script>
 <script type="text/javascript" src="@routes.Assets.at("javascripts/common/yona.SubComment.js")"></script>
+<script type="text/javascript" src="@routes.Assets.at("javascripts/common/yona.CommentAttachmentsUpdate.js")"></script>
 <script type="text/javascript">
         $(document).ready(function(){
             $yobi.loadModule("board.View", {
 
app/views/common/attachmentFile.scala.html (added)
+++ app/views/common/attachmentFile.scala.html
@@ -0,0 +1,18 @@
+@**
+* Yona, 21st Century Project Hosting SW
+*
+* Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp.
+* https://yona.io
+**@
+@import models.enumeration.ResourceType
+@import utils.AccessControl._
+@import humanize.Humanize._;
+
+@(file:Map[String, String], parentType:ResourceType, parentId:Long)
+
+<div class="attached-file attached-file-marker" data-name="@file.get("name")" data-href="@routes.AttachmentApp.getFile(file.get("id").toLong)" data-mime="@file.get("mimeType")">
+    <i class="mimetype"></i>
+    <strong class="name">@file.get("name")</strong>
+    <span class="size">@binaryPrefix(file.get("size").toDouble)</span>
+    <button type="button" class="btn-transparent btn-delete" data-id="@file.get("id")">&times;</button>
+</div>
app/views/common/commentUpdateForm.scala.html
--- app/views/common/commentUpdateForm.scala.html
+++ app/views/common/commentUpdateForm.scala.html
@@ -1,21 +1,45 @@
 @**
 * Yona, 21st Century Project Hosting SW
 *
-* Copyright Yona & Yobi Authors & NAVER Corp.
+* Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp.
 * https://yona.io
 **@
+@import models.enumeration.ResourceType
 @(comment:Comment, action:String, contents:String, isAllowedUpdate:Boolean)
 @import utils.AccessControl._
 
-<div id="comment-editform-@comment.id" class="comment-update-form">
-    <form action="@action/@comment.id" method="post">
-        <input type="hidden" name="id" value="@comment.id">
+@files = @{
+    if(comment.isInstanceOf[IssueComment]) {
+        AttachmentApp.getFileList(ResourceType.ISSUE_COMMENT, comment.id)
+    } else {
+        AttachmentApp.getFileList(ResourceType.NONISSUE_COMMENT, comment.id)
+    }
+}
 
+@resourceType = @{
+    if(comment.isInstanceOf[IssueComment]) {
+        ResourceType.ISSUE_COMMENT
+    } else {
+        ResourceType.NONISSUE_COMMENT
+    }
+}
+
+<div id="comment-editform-@comment.id" class="comment-update-form">
+    <form action="@action/@comment.id" method="post" enctype="multipart/form-data">
+        <input type="hidden" name="id" value="@comment.id">
         <div class="write-comment-box">
             <div class="write-comment-wrap">
-                @common.editor("contents", contents,"", "update-comment-body")
-
-                <div class="right-txt comment-update-button">
+                @common.editor("contents-" + comment.id, contents,"", "update-comment-body")
+                <div class="upload-drop-here">
+                    <div class="msg-wrap">
+                        <div class="msg">@Messages("common.attach.dropFilesHere")</div>
+                    </div>
+                </div>
+                <div class="right-txt comment-update-button upload-button-line">
+                    <span class="file-upload">
+                        <label for="upload-@comment.id" class="file-upload__label ybtn">@Messages("button.upload")</label>
+                        <input id="upload-@comment.id" class="file-upload__input" type="file" name="filePath" multiple>
+                    </span>
                     @if(comment.isAuthoredBy(UserApp.currentUser())){
                     <span class="send-notification-check" data-toggle='popover' data-trigger="hover" data-placement="top" data-content="@Messages("notification.send.mail.warning")">
                         <label class="checkbox inline">
@@ -30,6 +54,14 @@
                     }
                 </div>
             </div>
+            <input type="hidden" name="temporaryUploadFiles" class="temporaryUploadFiles" value="">
+            <div class="preview-@comment.id"></div>
+            <div class="attachment-files">
+            @for(file <- files.get("attachments")) {
+                @attachmentFile(file, resourceType, comment.id)
+            }
+            </div>
+            <div id="upload-@comment.id" data-resourceType="@resourceType" data-resourceId="@comment.id"></div>
         </div>
     </form>
 </div>
app/views/common/editor.scala.html
--- app/views/common/editor.scala.html
+++ app/views/common/editor.scala.html
@@ -1,29 +1,29 @@
 @**
-* Yobi, Project Hosting SW
+* Yona, 21st Century Project Hosting SW
 *
-* Copyright 2014 NAVER Corp.
-* http://yobi.io
-*
-* @author JiHan Kim
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*   http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
+* Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp.
+* https://yona.io
 **@
 @(editorName:String, textValue:String="", additionalAttr:String="", editorMode:String="content-body", viaEmail:Boolean=false)
 
 @import org.apache.commons.lang3.RandomStringUtils._
 @import utils.TemplateHelper._
 
-@defining(randomAlphabetic(8)){ wrapId =>
+@wrapIdGen = @{
+    var split = editorName.split("-")
+    if (split.length > 1) {
+        split(1)
+    } else {
+        randomAlphabetic(8)
+    }
+}
+
+@textareaName = @{
+    var split = editorName.split("-")
+    split(0)
+}
+
+@defining(wrapIdGen){ wrapId =>
 <div data-toggle="markdown-editor" class="mt10">
     <ul class="nav nav-tabs nm small">
         <li class="active">
@@ -45,7 +45,7 @@
 
         <div id="edit-@wrapId" class="tab-pane active">
             <div class="textarea-box">
-                <textarea name="@editorName" class="content comment nm" data-editor-mode="@editorMode" markdown="true" id="editor-@editorName-@wrapId"
+                <textarea name="@textareaName" class="content comment nm" data-editor-mode="@editorMode" markdown="true" id="editor-@textareaName-@wrapId"
                     @additionalAttr>@textValue</textarea>
             </div>
         </div>
app/views/issue/view.scala.html
--- app/views/issue/view.scala.html
+++ app/views/issue/view.scala.html
@@ -465,6 +465,7 @@
 <script type="text/javascript" src="@routes.Assets.at("javascripts/common/yona.Sha1.js")"></script>
 <script type="text/javascript" src="@routes.Assets.at("javascripts/common/yona.Tasklist.js")"></script>
 <script type="text/javascript" src="@routes.Assets.at("javascripts/common/yona.SubComment.js")"></script>
+<script type="text/javascript" src="@routes.Assets.at("javascripts/common/yona.CommentAttachmentsUpdate.js")"></script>
 <script type="text/javascript">
     $(function(){
         // yobi.issue.View
@@ -500,8 +501,7 @@
         // detect comment which contains mention at me
         $(".comment-body:contains('@UserApp.currentUser().getPureNameOnly')").closest(".comment").addClass("mentioned");
     });
-</script>
-<script>
+
         $(function () {
             yonaAssgineeModule(
                     "@api.routes.IssueApi.findAssignableUsers(project.owner, project.name, issue.getNumber)",
@@ -580,7 +580,6 @@
                 shape: 'rounded',
                 tooltips: true
             });
-
         });
 </script>
 }
build.sbt
--- build.sbt
+++ build.sbt
@@ -58,7 +58,8 @@
   "com.google.guava" % "guava" % "19.0",
   "com.googlecode.htmlcompressor" % "htmlcompressor" % "1.4",
   "org.springframework" % "spring-jdbc" % "4.1.5.RELEASE",
-  "javax.xml.bind" % "jaxb-api" % "2.3.0"
+  "javax.xml.bind" % "jaxb-api" % "2.3.0",
+  "com.github.mfornos" % "humanize-slim" % "1.2.2"
 )
 
 libraryDependencies += "org.apache.subversion" % "svn-javahl-api" % "1.9.0"
conf/messages.ko-KR
--- conf/messages.ko-KR
+++ conf/messages.ko-KR
@@ -158,7 +158,7 @@
 common.attach.drophere = 첨부할 파일을 끌어다 놓거나
 common.attach.error.delete = 파일 삭제에 실패했습니다.<br>{1} ({0})
 common.attach.error.upload = 파일 첨부에 실패했습니다.<br>{1} ({0})
-common.attach.pastehere = . 클립보드 이미지를 붙여 넣을 수도 있습니다
+common.attach.pastehere = 클립보드 이미지를 붙여 넣을 수도 있습니다
 common.attachment = 첨부파일
 common.comment = 댓글
 common.comment.author = 댓글 작성자
@@ -461,7 +461,7 @@
 notification.reviewthread.inTheFile = {0} 에서:
 notification.reviewthread.reopened = 리뷰 스레드 다시 열림
 notification.resource.deleted = {0} 님에 의해 삭제되었습니다
-notification.send.mail = 수정에 대한 알림 메일 발송
+notification.send.mail = 수정 알림 메일 발송
 notification.send.mail.warning = 만약 해당글의 원 작성자가 아니라면 이 옵션은 무시되고 알림 메일이 발송됩니다.
 notification.type.comment.updated = 댓글 수정
 notification.type.issue.assignee.changed = 이슈 담당자 변경
 
public/javascripts/common/yona.CommentAttachmentsUpdate.js (added)
+++ public/javascripts/common/yona.CommentAttachmentsUpdate.js
@@ -0,0 +1,131 @@
+$(function(){
+    function deleteAttachment() {
+        var $this = $(this);
+        var $parent = $this.parent(".attached-file-marker");
+        var id = $this.data("id");
+        var filename = $parent.data("name");
+        var url = $parent.data("href");
+        var mimeType = $parent.data("mime");
+        var linkStr = "[" + filename + "](" + url + ")";
+
+        if (mimeType.startsWith("image")) {
+            linkStr = "!" + linkStr;
+        }
+
+        var $form = $this.parent().closest("form");
+        var $textarea = $form.find("textarea");
+        var $attachfiles = $form.find(".temporaryUploadFiles");
+
+        $attachfiles.val($attachfiles.val().split(",").filter(function(item){
+            return item != id;
+        }).join(","));
+        $textarea.val($textarea.val().split(linkStr).join(""));
+
+        $.post(url)
+            .done(function (data) {
+                $parent.remove();
+            })
+            .fail(function (data) {
+                console.log(data);
+            });
+    }
+
+    function insertLinkIntoTextarea($textarea, data, caretPos) {
+        var textAreaTxt = $textarea.val();
+        var txtToAdd = "[" + data.name + "](" + data.url + ")";
+        if (data.mimeType.startsWith("image")) {
+            txtToAdd = "!" + txtToAdd;
+        }
+
+        txtToAdd = " " + txtToAdd;
+
+        if (textAreaTxt.length > 0 && caretPos === 0) {
+            caretPos = textAreaTxt.length;
+        }
+
+        $textarea.val(textAreaTxt.substring(0, caretPos) + txtToAdd + textAreaTxt.substring(caretPos));
+
+        return caretPos + txtToAdd.length;
+    }
+
+    $(".attached-file-marker").on("click", ".btn-delete", deleteAttachment);
+    $(".file-upload__input").on("change", function (e) {
+        var $this = $(this);
+        var files = $this[0].files;
+
+        NProgress.start();
+        var doneCount = 0;
+
+        var caretPos = $this.parent().closest("form").find("textarea")[0].selectionStart;
+        for (var i = 0; i < files.length; i++) {
+            var file = files[i];
+            var formData = new FormData();
+
+            formData.append("filePath", file);
+
+            $.ajax({
+                url: '/files',
+                type: 'POST',
+                cache: false,
+                contentType: false,
+                processData: false,
+                data: formData
+            }).done(function (data) {
+                var $parentForm = $this.parent().closest("form");
+                var $attachfiles = $parentForm.find(".temporaryUploadFiles");
+                var $textarea = $parentForm.find("textarea");
+
+                if (doneCount === 0) {
+                    $attachfiles.val(data.id);
+                } else {
+                    $attachfiles.val($attachfiles.val() + "," + data.id);
+                }
+                var attachment = '<div class="attached-file attached-file-marker" data-mime="' +
+                    data.mimeType.trim() + '" data-name="' + data.name + '" data-href="' + data.url + '">\n' +
+                    '<strong class="name">' + data.name + '</strong>\n' +
+                    '<span class="size">' + humanize.filesize(data.size) + '</span>\n' +
+                    '<button type="button" class="btn-transparent btn-delete" data-id="' + data.id + '">&times;</button>\n' +
+                    '</div>';
+                $parentForm.find(".attachment-files").append(attachment);
+                $parentForm.find(".attachment-files").on("click", ".btn-delete", deleteAttachment);
+
+                caretPos = insertLinkIntoTextarea($textarea, data, caretPos);
+
+                doneCount++;
+                if (doneCount === files.length) {
+                    NProgress.done();
+                }
+            }).fail(function (data) {
+                $yobi.notify(data);
+            });
+        }
+    });
+
+    var rememberBorder = "";
+    $(".textarea-box")
+        .on("dragenter", "textarea", function(e){
+            e.stopPropagation();
+            e.preventDefault();
+            rememberBorder = $(this).css("border");
+            $(this).css("border", "1px dashed orange");
+        })
+        .on("dragover", "textarea", function(e){
+            e.stopPropagation();
+            e.preventDefault();
+        })
+        .on("drop", "textarea", function(e){
+            e.stopPropagation();
+            e.preventDefault();
+
+            var dt = e.originalEvent.dataTransfer;
+            var files = dt.files;
+
+            console.log(files);
+
+            $(this).css("border", rememberBorder);
+            $(this).parent().closest("form").find(".file-upload__input")[0].files = files;
+        })
+        .on("dragleave", "textarea", function(e){
+            $(this).css("border", rememberBorder);
+        });
+});
Add a comment
List