THIS IS A TEST INSTANCE ONLY! REPOSITORIES CAN BE DELETED AT ANY TIME!

Browse Source

comment: Support attachments modification

tags/v1.11.0
Suwon Chae 1 year ago
parent
commit
cca1b798fa
11 changed files with 317 additions and 102 deletions
  1. +88
    -65
      app/assets/stylesheets/less/_page.less
  2. +15
    -5
      app/controllers/AttachmentApp.java
  3. +1
    -1
      app/views/board/partial_comments.scala.html
  4. +1
    -0
      app/views/board/view.scala.html
  5. +18
    -0
      app/views/common/attachmentFile.scala.html
  6. +38
    -6
      app/views/common/commentUpdateForm.scala.html
  7. +19
    -19
      app/views/common/editor.scala.html
  8. +2
    -3
      app/views/issue/view.scala.html
  9. +2
    -1
      build.sbt
  10. +2
    -2
      conf/messages.ko-KR
  11. +131
    -0
      public/javascripts/common/yona.CommentAttachmentsUpdate.js

+ 88
- 65
app/assets/stylesheets/less/_page.less View File

@@ -3308,7 +3308,6 @@ div.markdown-preview {
font-family:@base-font-family;

.write-comment-wrap {
margin-bottom: 10px;
position: relative;
.nav {
margin-bottom: 0;
@@ -3461,77 +3460,76 @@ div.markdown-preview {
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; }
.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: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space:nowrap;
vertical-align:middle;
-webkit-transition-duration: 0.5s;
}
.size { font-size:11px; vertical-align:middle; }
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 {
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; }
}
/* 업로드 하는 도중에는 진행 상태 표시 */
.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; }
}
/* 업로드 완료 후에는 삭제 버튼 표시 */
&.complete {
.progress { display:none; }
.btn-delete { display:inline-block; }
.btn-insert { display:block; }
}

/* 임시 파일 표시 */
&.temporary {
i { display:inline-block; }
}
}
/* 임시 파일 표시 */
&.temporary {
i { display:inline-block; }
}
}

.upload-drop-here {
position: absolute;
top: 2px;
@@ -7428,3 +7426,28 @@ div.diff-body[data-outdated="true"] tr:hover .icon-comment {
}
}
}

.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;
}

}

+ 15
- 5
app/controllers/AttachmentApp.java View File

@@ -16,6 +16,7 @@ import org.apache.commons.lang3.StringUtils;
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 class AttachmentApp extends Controller {

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 @@ public class AttachmentApp extends Controller {
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 =

+ 1
- 1
app/views/board/partial_comments.scala.html View File

@@ -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)

+ 1
- 0
app/views/board/view.scala.html View File

@@ -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", {

+ 18
- 0
app/views/common/attachmentFile.scala.html View File

@@ -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>

+ 38
- 6
app/views/common/commentUpdateForm.scala.html View File

@@ -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._

@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">
<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>

+ 19
- 19
app/views/common/editor.scala.html View File

@@ -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>

+ 2
- 3
app/views/issue/view.scala.html View File

@@ -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>
}

+ 2
- 1
build.sbt View File

@@ -58,7 +58,8 @@ libraryDependencies ++= Seq(
"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"

+ 2
- 2
conf/messages.ko-KR View File

@@ -158,7 +158,7 @@ common.attach.dropFilesHere = 여기에 파일을 끌어다 놓으면 업로드
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.closed = 리뷰 스레드 닫힘
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 = 이슈 담당자 변경

+ 131
- 0
public/javascripts/common/yona.CommentAttachmentsUpdate.js View File

@@ -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);
});
});

Loading…
Cancel
Save