Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable Uploading/Removing Attachments When Editing an Issue/Comment #8426

Merged
merged 19 commits into from
Oct 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions models/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,26 @@ func AddDeletePRBranchComment(doer *User, repo *Repository, issueID int64, branc
return sess.Commit()
}

// UpdateAttachments update attachments by UUIDs for the issue
func (issue *Issue) UpdateAttachments(uuids []string) (err error) {
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
attachments, err := getAttachmentsByUUIDs(sess, uuids)
if err != nil {
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", uuids, err)
}
for i := 0; i < len(attachments); i++ {
attachments[i].IssueID = issue.ID
if err := updateAttachment(sess, attachments[i]); err != nil {
return fmt.Errorf("update attachment [id: %d]: %v", attachments[i].ID, err)
}
}
return sess.Commit()
}

// ChangeContent changes issue content, as the given user.
func (issue *Issue) ChangeContent(doer *User, content string) (err error) {
oldContent := issue.Content
Expand Down
21 changes: 21 additions & 0 deletions models/issue_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,27 @@ func (c *Comment) LoadAttachments() error {
return nil
}

// UpdateAttachments update attachments by UUIDs for the comment
func (c *Comment) UpdateAttachments(uuids []string) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps it's worth having a common function and use it from Comment and Issue to avoid duplicating code? This function is not too short but also not too long, so it sits in-between.

sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
attachments, err := getAttachmentsByUUIDs(sess, uuids)
if err != nil {
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", uuids, err)
}
for i := 0; i < len(attachments); i++ {
attachments[i].IssueID = c.IssueID
attachments[i].CommentID = c.ID
if err := updateAttachment(sess, attachments[i]); err != nil {
return fmt.Errorf("update attachment [id: %d]: %v", attachments[i].ID, err)
}
}
return sess.Commit()
}

// LoadAssigneeUser if comment.Type is CommentTypeAssignees, then load assignees
func (c *Comment) LoadAssigneeUser() error {
var err error
Expand Down
10 changes: 10 additions & 0 deletions modules/util/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ func ExistsInSlice(target string, slice []string) bool {
return i < len(slice)
}

// IsStringInSlice sequential searches if string exists in slice.
func IsStringInSlice(target string, slice []string) bool {
for i := 0; i < len(slice); i++ {
if slice[i] == target {
return true
}
}
return false
}

// IsEqualSlice returns true if slices are equal.
func IsEqualSlice(target []string, source []string) bool {
if len(target) != len(source) {
Expand Down
119 changes: 104 additions & 15 deletions public/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,73 @@ function initRepository() {
issuesTribute.attach($textarea.get());
emojiTribute.attach($textarea.get());

const $dropzone = $editContentZone.find('.dropzone');
$dropzone.data("saved", false);
const $files = $editContentZone.find('.comment-files');
if ($dropzone.length > 0) {
const filenameDict = {};
$dropzone.dropzone({
url: $dropzone.data('upload-url'),
headers: {"X-Csrf-Token": csrf},
maxFiles: $dropzone.data('max-file'),
maxFilesize: $dropzone.data('max-size'),
acceptedFiles: ($dropzone.data('accepts') === '*/*') ? null : $dropzone.data('accepts'),
addRemoveLinks: true,
dictDefaultMessage: $dropzone.data('default-message'),
dictInvalidFileType: $dropzone.data('invalid-input-type'),
dictFileTooBig: $dropzone.data('file-too-big'),
dictRemoveFile: $dropzone.data('remove-file'),
init: function () {
this.on("success", function (file, data) {
filenameDict[file.name] = {
"uuid": data.uuid,
"submitted": false
}
const input = $('<input id="' + data.uuid + '" name="files" type="hidden">').val(data.uuid);
$files.append(input);
});
this.on("removedfile", function (file) {
if (!(file.name in filenameDict)) {
return;
}
$('#' + filenameDict[file.name].uuid).remove();
if ($dropzone.data('remove-url') && $dropzone.data('csrf') && !filenameDict[file.name].submitted) {
$.post($dropzone.data('remove-url'), {
file: filenameDict[file.name].uuid,
_csrf: $dropzone.data('csrf')
});
}
});
this.on("submit", function () {
$.each(filenameDict, function(name){
filenameDict[name].submitted = true;
});
});
this.on("reload", function (){
$.getJSON($editContentZone.data('attachment-url'), function(data){
const drop = $dropzone.get(0).dropzone;
drop.removeAllFiles(true);
$files.empty();
$.each(data, function(){
const imgSrc = $dropzone.data('upload-url') + "/" + this.uuid;
drop.emit("addedfile", this);
drop.emit("thumbnail", this, imgSrc);
drop.emit("complete", this);
drop.files.push(this);
filenameDict[this.name] = {
"submitted": true,
"uuid": this.uuid
}
$dropzone.find("img[src='" + imgSrc + "']").css("max-width", "100%");
const input = $('<input id="' + this.uuid + '" name="files" type="hidden">').val(this.uuid);
$files.append(input);
});
});
});
}
});
$dropzone.get(0).dropzone.emit("reload");
}
// Give new write/preview data-tab name to distinguish from others
const $editContentForm = $editContentZone.find('.ui.comment.form');
const $tabMenu = $editContentForm.find('.tabular.menu');
Expand All @@ -880,27 +947,49 @@ function initRepository() {
$editContentZone.find('.cancel.button').click(function () {
$renderContent.show();
$editContentZone.hide();
$dropzone.get(0).dropzone.emit("reload");
});
$editContentZone.find('.save.button').click(function () {
$renderContent.show();
$editContentZone.hide();

const $attachments = $files.find("[name=files]").map(function(){
return $(this).val();
}).get();
$.post($editContentZone.data('update-url'), {
"_csrf": csrf,
"content": $textarea.val(),
"context": $editContentZone.data('context')
},
function (data) {
if (data.length == 0) {
$renderContent.html($('#no-content').html());
} else {
$renderContent.html(data.content);
emojify.run($renderContent[0]);
$('pre code', $renderContent[0]).each(function () {
hljs.highlightBlock(this);
});
"_csrf": csrf,
"content": $textarea.val(),
"context": $editContentZone.data('context'),
"files": $attachments
},
function (data) {
if (data.length == 0) {
$renderContent.html($('#no-content').html());
} else {
$renderContent.html(data.content);
emojify.run($renderContent[0]);
$('pre code', $renderContent[0]).each(function () {
hljs.highlightBlock(this);
});
}
const $content = $segment.parent();
if(!$content.find(".ui.small.images").length){
if(data.attachments != ""){
$content.append(
'<div class="ui bottom attached segment">' +
' <div class="ui small images">' +
' </div>' +
'</div>'
);
$content.find(".ui.small.images").html(data.attachments);
}
});
} else if (data.attachments == "") {
$content.find(".ui.small.images").parent().remove();
} else {
$content.find(".ui.small.images").html(data.attachments);
}
$dropzone.get(0).dropzone.emit("submit");
$dropzone.get(0).dropzone.emit("reload");
});
});
} else {
$textarea = $segment.find('textarea');
Expand Down
22 changes: 22 additions & 0 deletions routers/repo/attachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,25 @@ func UploadAttachment(ctx *context.Context) {
"uuid": attach.UUID,
})
}

// DeleteAttachment response for deleting issue's attachment
func DeleteAttachment(ctx *context.Context) {
file := ctx.Query("file")
attach, err := models.GetAttachmentByUUID(file)
if !ctx.IsSigned || (ctx.User.ID != attach.UploaderID) {
ctx.Error(403)
return
}
if err != nil {
ctx.Error(400, err.Error())
return
}
err = models.DeleteAttachment(attach, true)
if err != nil {
ctx.Error(500, fmt.Sprintf("DeleteAttachment: %v", err))
return
}
ctx.JSON(200, map[string]string{
"uuid": attach.UUID,
})
}
110 changes: 108 additions & 2 deletions routers/repo/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import (
)

const (
tplAttachment base.TplName = "repo/issue/view_content/attachments"

tplIssues base.TplName = "repo/issue/list"
tplIssueNew base.TplName = "repo/issue/new"
tplIssueView base.TplName = "repo/issue/view"
Expand Down Expand Up @@ -1074,8 +1076,14 @@ func UpdateIssueContent(ctx *context.Context) {
return
}

files := ctx.QueryStrings("files[]")
if err := updateAttachments(issue, files); err != nil {
ctx.ServerError("UpdateAttachments", err)
}

ctx.JSON(200, map[string]interface{}{
"content": string(markdown.Render([]byte(issue.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())),
"content": string(markdown.Render([]byte(issue.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())),
"attachments": attachmentsHTML(ctx, issue.Attachments),
})
}

Expand Down Expand Up @@ -1325,6 +1333,13 @@ func UpdateCommentContent(ctx *context.Context) {
return
}

if comment.Type == models.CommentTypeComment {
if err := comment.LoadAttachments(); err != nil {
ctx.ServerError("LoadAttachments", err)
return
}
}

if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.Error(403)
return
Expand All @@ -1346,10 +1361,16 @@ func UpdateCommentContent(ctx *context.Context) {
return
}

files := ctx.QueryStrings("files[]")
if err := updateAttachments(comment, files); err != nil {
ctx.ServerError("UpdateAttachments", err)
}

notification.NotifyUpdateComment(ctx.User, comment, oldContent)

ctx.JSON(200, map[string]interface{}{
"content": string(markdown.Render([]byte(comment.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())),
"content": string(markdown.Render([]byte(comment.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())),
"attachments": attachmentsHTML(ctx, comment.Attachments),
})
}

Expand Down Expand Up @@ -1603,3 +1624,88 @@ func filterXRefComments(ctx *context.Context, issue *models.Issue) error {
}
return nil
}

// GetIssueAttachments returns attachments for the issue
func GetIssueAttachments(ctx *context.Context) {
issue := GetActionIssue(ctx)
var attachments = make([]*api.Attachment, len(issue.Attachments))
for i := 0; i < len(issue.Attachments); i++ {
attachments[i] = issue.Attachments[i].APIFormat()
}
ctx.JSON(200, attachments)
}

// GetCommentAttachments returns attachments for the comment
func GetCommentAttachments(ctx *context.Context) {
comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
if err != nil {
ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
return
}
var attachments = make([]*api.Attachment, 0)
if comment.Type == models.CommentTypeComment {
if err := comment.LoadAttachments(); err != nil {
ctx.ServerError("LoadAttachments", err)
return
}
for i := 0; i < len(comment.Attachments); i++ {
attachments = append(attachments, comment.Attachments[i].APIFormat())
}
}
ctx.JSON(200, attachments)
}

func updateAttachments(item interface{}, files []string) error {
var attachments []*models.Attachment
switch content := item.(type) {
case *models.Issue:
attachments = content.Attachments
blueworrybear marked this conversation as resolved.
Show resolved Hide resolved
case *models.Comment:
attachments = content.Attachments
default:
return fmt.Errorf("Unknow Type")
}
for i := 0; i < len(attachments); i++ {
if util.IsStringInSlice(attachments[i].UUID, files) {
continue
}
if err := models.DeleteAttachment(attachments[i], true); err != nil {
return err
}
}
var err error
if len(files) > 0 {
switch content := item.(type) {
case *models.Issue:
err = content.UpdateAttachments(files)
case *models.Comment:
err = content.UpdateAttachments(files)
default:
return fmt.Errorf("Unknow Type")
}
if err != nil {
return err
}
}
switch content := item.(type) {
case *models.Issue:
content.Attachments, err = models.GetAttachmentsByIssueID(content.ID)
case *models.Comment:
content.Attachments, err = models.GetAttachmentsByCommentID(content.ID)
default:
return fmt.Errorf("Unknow Type")
}
return err
}

func attachmentsHTML(ctx *context.Context, attachments []*models.Attachment) string {
attachHTML, err := ctx.HTMLString(string(tplAttachment), map[string]interface{}{
"ctx": ctx.Data,
"Attachments": attachments,
})
if err != nil {
ctx.ServerError("attachmentsHTML.HTMLString", err)
return ""
}
return attachHTML
}
Loading