Skip to content

Commit 8c90982

Browse files
blueworrybearzeripath
authored andcommitted
Enable Uploading/Removing Attachments When Editing an Issue/Comment (#8426)
1 parent d7d348e commit 8c90982

File tree

10 files changed

+316
-39
lines changed

10 files changed

+316
-39
lines changed

models/issue.go

+20
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,26 @@ func AddDeletePRBranchComment(doer *User, repo *Repository, issueID int64, branc
855855
return sess.Commit()
856856
}
857857

858+
// UpdateAttachments update attachments by UUIDs for the issue
859+
func (issue *Issue) UpdateAttachments(uuids []string) (err error) {
860+
sess := x.NewSession()
861+
defer sess.Close()
862+
if err = sess.Begin(); err != nil {
863+
return err
864+
}
865+
attachments, err := getAttachmentsByUUIDs(sess, uuids)
866+
if err != nil {
867+
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", uuids, err)
868+
}
869+
for i := 0; i < len(attachments); i++ {
870+
attachments[i].IssueID = issue.ID
871+
if err := updateAttachment(sess, attachments[i]); err != nil {
872+
return fmt.Errorf("update attachment [id: %d]: %v", attachments[i].ID, err)
873+
}
874+
}
875+
return sess.Commit()
876+
}
877+
858878
// ChangeContent changes issue content, as the given user.
859879
func (issue *Issue) ChangeContent(doer *User, content string) (err error) {
860880
oldContent := issue.Content

models/issue_comment.go

+21
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,27 @@ func (c *Comment) LoadAttachments() error {
357357
return nil
358358
}
359359

360+
// UpdateAttachments update attachments by UUIDs for the comment
361+
func (c *Comment) UpdateAttachments(uuids []string) error {
362+
sess := x.NewSession()
363+
defer sess.Close()
364+
if err := sess.Begin(); err != nil {
365+
return err
366+
}
367+
attachments, err := getAttachmentsByUUIDs(sess, uuids)
368+
if err != nil {
369+
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", uuids, err)
370+
}
371+
for i := 0; i < len(attachments); i++ {
372+
attachments[i].IssueID = c.IssueID
373+
attachments[i].CommentID = c.ID
374+
if err := updateAttachment(sess, attachments[i]); err != nil {
375+
return fmt.Errorf("update attachment [id: %d]: %v", attachments[i].ID, err)
376+
}
377+
}
378+
return sess.Commit()
379+
}
380+
360381
// LoadAssigneeUser if comment.Type is CommentTypeAssignees, then load assignees
361382
func (c *Comment) LoadAssigneeUser() error {
362383
var err error

modules/util/compare.go

+10
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ func ExistsInSlice(target string, slice []string) bool {
3535
return i < len(slice)
3636
}
3737

38+
// IsStringInSlice sequential searches if string exists in slice.
39+
func IsStringInSlice(target string, slice []string) bool {
40+
for i := 0; i < len(slice); i++ {
41+
if slice[i] == target {
42+
return true
43+
}
44+
}
45+
return false
46+
}
47+
3848
// IsEqualSlice returns true if slices are equal.
3949
func IsEqualSlice(target []string, source []string) bool {
4050
if len(target) != len(source) {

public/js/index.js

+104-15
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,73 @@ function initRepository() {
865865
issuesTribute.attach($textarea.get());
866866
emojiTribute.attach($textarea.get());
867867

868+
const $dropzone = $editContentZone.find('.dropzone');
869+
$dropzone.data("saved", false);
870+
const $files = $editContentZone.find('.comment-files');
871+
if ($dropzone.length > 0) {
872+
const filenameDict = {};
873+
$dropzone.dropzone({
874+
url: $dropzone.data('upload-url'),
875+
headers: {"X-Csrf-Token": csrf},
876+
maxFiles: $dropzone.data('max-file'),
877+
maxFilesize: $dropzone.data('max-size'),
878+
acceptedFiles: ($dropzone.data('accepts') === '*/*') ? null : $dropzone.data('accepts'),
879+
addRemoveLinks: true,
880+
dictDefaultMessage: $dropzone.data('default-message'),
881+
dictInvalidFileType: $dropzone.data('invalid-input-type'),
882+
dictFileTooBig: $dropzone.data('file-too-big'),
883+
dictRemoveFile: $dropzone.data('remove-file'),
884+
init: function () {
885+
this.on("success", function (file, data) {
886+
filenameDict[file.name] = {
887+
"uuid": data.uuid,
888+
"submitted": false
889+
}
890+
const input = $('<input id="' + data.uuid + '" name="files" type="hidden">').val(data.uuid);
891+
$files.append(input);
892+
});
893+
this.on("removedfile", function (file) {
894+
if (!(file.name in filenameDict)) {
895+
return;
896+
}
897+
$('#' + filenameDict[file.name].uuid).remove();
898+
if ($dropzone.data('remove-url') && $dropzone.data('csrf') && !filenameDict[file.name].submitted) {
899+
$.post($dropzone.data('remove-url'), {
900+
file: filenameDict[file.name].uuid,
901+
_csrf: $dropzone.data('csrf')
902+
});
903+
}
904+
});
905+
this.on("submit", function () {
906+
$.each(filenameDict, function(name){
907+
filenameDict[name].submitted = true;
908+
});
909+
});
910+
this.on("reload", function (){
911+
$.getJSON($editContentZone.data('attachment-url'), function(data){
912+
const drop = $dropzone.get(0).dropzone;
913+
drop.removeAllFiles(true);
914+
$files.empty();
915+
$.each(data, function(){
916+
const imgSrc = $dropzone.data('upload-url') + "/" + this.uuid;
917+
drop.emit("addedfile", this);
918+
drop.emit("thumbnail", this, imgSrc);
919+
drop.emit("complete", this);
920+
drop.files.push(this);
921+
filenameDict[this.name] = {
922+
"submitted": true,
923+
"uuid": this.uuid
924+
}
925+
$dropzone.find("img[src='" + imgSrc + "']").css("max-width", "100%");
926+
const input = $('<input id="' + this.uuid + '" name="files" type="hidden">').val(this.uuid);
927+
$files.append(input);
928+
});
929+
});
930+
});
931+
}
932+
});
933+
$dropzone.get(0).dropzone.emit("reload");
934+
}
868935
// Give new write/preview data-tab name to distinguish from others
869936
const $editContentForm = $editContentZone.find('.ui.comment.form');
870937
const $tabMenu = $editContentForm.find('.tabular.menu');
@@ -880,27 +947,49 @@ function initRepository() {
880947
$editContentZone.find('.cancel.button').click(function () {
881948
$renderContent.show();
882949
$editContentZone.hide();
950+
$dropzone.get(0).dropzone.emit("reload");
883951
});
884952
$editContentZone.find('.save.button').click(function () {
885953
$renderContent.show();
886954
$editContentZone.hide();
887-
955+
const $attachments = $files.find("[name=files]").map(function(){
956+
return $(this).val();
957+
}).get();
888958
$.post($editContentZone.data('update-url'), {
889-
"_csrf": csrf,
890-
"content": $textarea.val(),
891-
"context": $editContentZone.data('context')
892-
},
893-
function (data) {
894-
if (data.length == 0) {
895-
$renderContent.html($('#no-content').html());
896-
} else {
897-
$renderContent.html(data.content);
898-
emojify.run($renderContent[0]);
899-
$('pre code', $renderContent[0]).each(function () {
900-
hljs.highlightBlock(this);
901-
});
959+
"_csrf": csrf,
960+
"content": $textarea.val(),
961+
"context": $editContentZone.data('context'),
962+
"files": $attachments
963+
},
964+
function (data) {
965+
if (data.length == 0) {
966+
$renderContent.html($('#no-content').html());
967+
} else {
968+
$renderContent.html(data.content);
969+
emojify.run($renderContent[0]);
970+
$('pre code', $renderContent[0]).each(function () {
971+
hljs.highlightBlock(this);
972+
});
973+
}
974+
const $content = $segment.parent();
975+
if(!$content.find(".ui.small.images").length){
976+
if(data.attachments != ""){
977+
$content.append(
978+
'<div class="ui bottom attached segment">' +
979+
' <div class="ui small images">' +
980+
' </div>' +
981+
'</div>'
982+
);
983+
$content.find(".ui.small.images").html(data.attachments);
902984
}
903-
});
985+
} else if (data.attachments == "") {
986+
$content.find(".ui.small.images").parent().remove();
987+
} else {
988+
$content.find(".ui.small.images").html(data.attachments);
989+
}
990+
$dropzone.get(0).dropzone.emit("submit");
991+
$dropzone.get(0).dropzone.emit("reload");
992+
});
904993
});
905994
} else {
906995
$textarea = $segment.find('textarea');

routers/repo/attachment.go

+22
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,25 @@ func UploadAttachment(ctx *context.Context) {
6363
"uuid": attach.UUID,
6464
})
6565
}
66+
67+
// DeleteAttachment response for deleting issue's attachment
68+
func DeleteAttachment(ctx *context.Context) {
69+
file := ctx.Query("file")
70+
attach, err := models.GetAttachmentByUUID(file)
71+
if !ctx.IsSigned || (ctx.User.ID != attach.UploaderID) {
72+
ctx.Error(403)
73+
return
74+
}
75+
if err != nil {
76+
ctx.Error(400, err.Error())
77+
return
78+
}
79+
err = models.DeleteAttachment(attach, true)
80+
if err != nil {
81+
ctx.Error(500, fmt.Sprintf("DeleteAttachment: %v", err))
82+
return
83+
}
84+
ctx.JSON(200, map[string]string{
85+
"uuid": attach.UUID,
86+
})
87+
}

routers/repo/issue.go

+108-2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import (
3434
)
3535

3636
const (
37+
tplAttachment base.TplName = "repo/issue/view_content/attachments"
38+
3739
tplIssues base.TplName = "repo/issue/list"
3840
tplIssueNew base.TplName = "repo/issue/new"
3941
tplIssueView base.TplName = "repo/issue/view"
@@ -1074,8 +1076,14 @@ func UpdateIssueContent(ctx *context.Context) {
10741076
return
10751077
}
10761078

1079+
files := ctx.QueryStrings("files[]")
1080+
if err := updateAttachments(issue, files); err != nil {
1081+
ctx.ServerError("UpdateAttachments", err)
1082+
}
1083+
10771084
ctx.JSON(200, map[string]interface{}{
1078-
"content": string(markdown.Render([]byte(issue.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())),
1085+
"content": string(markdown.Render([]byte(issue.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())),
1086+
"attachments": attachmentsHTML(ctx, issue.Attachments),
10791087
})
10801088
}
10811089

@@ -1325,6 +1333,13 @@ func UpdateCommentContent(ctx *context.Context) {
13251333
return
13261334
}
13271335

1336+
if comment.Type == models.CommentTypeComment {
1337+
if err := comment.LoadAttachments(); err != nil {
1338+
ctx.ServerError("LoadAttachments", err)
1339+
return
1340+
}
1341+
}
1342+
13281343
if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
13291344
ctx.Error(403)
13301345
return
@@ -1346,10 +1361,16 @@ func UpdateCommentContent(ctx *context.Context) {
13461361
return
13471362
}
13481363

1364+
files := ctx.QueryStrings("files[]")
1365+
if err := updateAttachments(comment, files); err != nil {
1366+
ctx.ServerError("UpdateAttachments", err)
1367+
}
1368+
13491369
notification.NotifyUpdateComment(ctx.User, comment, oldContent)
13501370

13511371
ctx.JSON(200, map[string]interface{}{
1352-
"content": string(markdown.Render([]byte(comment.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())),
1372+
"content": string(markdown.Render([]byte(comment.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())),
1373+
"attachments": attachmentsHTML(ctx, comment.Attachments),
13531374
})
13541375
}
13551376

@@ -1603,3 +1624,88 @@ func filterXRefComments(ctx *context.Context, issue *models.Issue) error {
16031624
}
16041625
return nil
16051626
}
1627+
1628+
// GetIssueAttachments returns attachments for the issue
1629+
func GetIssueAttachments(ctx *context.Context) {
1630+
issue := GetActionIssue(ctx)
1631+
var attachments = make([]*api.Attachment, len(issue.Attachments))
1632+
for i := 0; i < len(issue.Attachments); i++ {
1633+
attachments[i] = issue.Attachments[i].APIFormat()
1634+
}
1635+
ctx.JSON(200, attachments)
1636+
}
1637+
1638+
// GetCommentAttachments returns attachments for the comment
1639+
func GetCommentAttachments(ctx *context.Context) {
1640+
comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
1641+
if err != nil {
1642+
ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
1643+
return
1644+
}
1645+
var attachments = make([]*api.Attachment, 0)
1646+
if comment.Type == models.CommentTypeComment {
1647+
if err := comment.LoadAttachments(); err != nil {
1648+
ctx.ServerError("LoadAttachments", err)
1649+
return
1650+
}
1651+
for i := 0; i < len(comment.Attachments); i++ {
1652+
attachments = append(attachments, comment.Attachments[i].APIFormat())
1653+
}
1654+
}
1655+
ctx.JSON(200, attachments)
1656+
}
1657+
1658+
func updateAttachments(item interface{}, files []string) error {
1659+
var attachments []*models.Attachment
1660+
switch content := item.(type) {
1661+
case *models.Issue:
1662+
attachments = content.Attachments
1663+
case *models.Comment:
1664+
attachments = content.Attachments
1665+
default:
1666+
return fmt.Errorf("Unknow Type")
1667+
}
1668+
for i := 0; i < len(attachments); i++ {
1669+
if util.IsStringInSlice(attachments[i].UUID, files) {
1670+
continue
1671+
}
1672+
if err := models.DeleteAttachment(attachments[i], true); err != nil {
1673+
return err
1674+
}
1675+
}
1676+
var err error
1677+
if len(files) > 0 {
1678+
switch content := item.(type) {
1679+
case *models.Issue:
1680+
err = content.UpdateAttachments(files)
1681+
case *models.Comment:
1682+
err = content.UpdateAttachments(files)
1683+
default:
1684+
return fmt.Errorf("Unknow Type")
1685+
}
1686+
if err != nil {
1687+
return err
1688+
}
1689+
}
1690+
switch content := item.(type) {
1691+
case *models.Issue:
1692+
content.Attachments, err = models.GetAttachmentsByIssueID(content.ID)
1693+
case *models.Comment:
1694+
content.Attachments, err = models.GetAttachmentsByCommentID(content.ID)
1695+
default:
1696+
return fmt.Errorf("Unknow Type")
1697+
}
1698+
return err
1699+
}
1700+
1701+
func attachmentsHTML(ctx *context.Context, attachments []*models.Attachment) string {
1702+
attachHTML, err := ctx.HTMLString(string(tplAttachment), map[string]interface{}{
1703+
"ctx": ctx.Data,
1704+
"Attachments": attachments,
1705+
})
1706+
if err != nil {
1707+
ctx.ServerError("attachmentsHTML.HTMLString", err)
1708+
return ""
1709+
}
1710+
return attachHTML
1711+
}

0 commit comments

Comments
 (0)