summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblueworrybear <blueworrybear@gmail.com>2019-10-15 20:19:32 +0800
committerzeripath <art27@cantab.net>2019-10-15 13:19:32 +0100
commit8c909820a9fd1697bb690ec0451c4ead97b51505 (patch)
treebda76eca8361237fbd5bc59bda58b278a516ffd6
parentd7d348ea86bd8066aeb079ad121120095d5cba4d (diff)
downloadgitea-8c909820a9fd1697bb690ec0451c4ead97b51505.tar.gz
gitea-8c909820a9fd1697bb690ec0451c4ead97b51505.zip
Enable Uploading/Removing Attachments When Editing an Issue/Comment (#8426)
-rw-r--r--models/issue.go20
-rw-r--r--models/issue_comment.go21
-rw-r--r--modules/util/compare.go10
-rw-r--r--public/js/index.js119
-rw-r--r--routers/repo/attachment.go22
-rw-r--r--routers/repo/issue.go110
-rw-r--r--routers/routes/routes.go7
-rw-r--r--templates/repo/issue/view_content.tmpl25
-rw-r--r--templates/repo/issue/view_content/attachments.tmpl9
-rw-r--r--templates/repo/issue/view_content/comments.tmpl12
10 files changed, 316 insertions, 39 deletions
diff --git a/models/issue.go b/models/issue.go
index 90925f92f5..6503a0618f 100644
--- a/models/issue.go
+++ b/models/issue.go
@@ -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
diff --git a/models/issue_comment.go b/models/issue_comment.go
index 3a090c3b19..ccf239d600 100644
--- a/models/issue_comment.go
+++ b/models/issue_comment.go
@@ -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 {
+ 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
diff --git a/modules/util/compare.go b/modules/util/compare.go
index c61e7965ae..f1d1e5718e 100644
--- a/modules/util/compare.go
+++ b/modules/util/compare.go
@@ -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) {
diff --git a/public/js/index.js b/public/js/index.js
index 3b15ad8f18..11b2e75f2d 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -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');
@@ -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');
diff --git a/routers/repo/attachment.go b/routers/repo/attachment.go
index a07a2a8ace..0d496230e1 100644
--- a/routers/repo/attachment.go
+++ b/routers/repo/attachment.go
@@ -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,
+ })
+}
diff --git a/routers/repo/issue.go b/routers/repo/issue.go
index 16a049c7aa..dee2c6e698 100644
--- a/routers/repo/issue.go
+++ b/routers/repo/issue.go
@@ -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"
@@ -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),
})
}
@@ -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
@@ -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),
})
}
@@ -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
+ 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
+}
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index 0db0af43f0..9572ea8039 100644
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -513,8 +513,9 @@ func RegisterRoutes(m *macaron.Macaron) {
})
}, ignSignIn)
- m.Group("", func() {
- m.Post("/attachments", repo.UploadAttachment)
+ m.Group("/attachments", func() {
+ m.Post("", repo.UploadAttachment)
+ m.Post("/delete", repo.DeleteAttachment)
}, reqSignIn)
m.Group("/:username", func() {
@@ -710,6 +711,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Post("/reactions/:action", bindIgnErr(auth.ReactionForm{}), repo.ChangeIssueReaction)
m.Post("/lock", reqRepoIssueWriter, bindIgnErr(auth.IssueLockForm{}), repo.LockIssue)
m.Post("/unlock", reqRepoIssueWriter, repo.UnlockIssue)
+ m.Get("/attachments", repo.GetIssueAttachments)
}, context.RepoMustNotBeArchived())
m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel)
@@ -721,6 +723,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Post("", repo.UpdateCommentContent)
m.Post("/delete", repo.DeleteComment)
m.Post("/reactions/:action", bindIgnErr(auth.ReactionForm{}), repo.ChangeCommentReaction)
+ m.Get("/attachments", repo.GetCommentAttachments)
}, context.RepoMustNotBeArchived())
m.Group("/labels", func() {
m.Post("/new", bindIgnErr(auth.CreateLabelForm{}), repo.NewLabel)
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index acabe34782..29d48d7089 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -46,7 +46,7 @@
{{end}}
</div>
<div class="raw-content hide">{{.Issue.Content}}</div>
- <div class="edit-content-zone hide" data-write="issue-{{.Issue.ID}}-write" data-preview="issue-{{.Issue.ID}}-preview" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/content" data-context="{{.RepoLink}}"></div>
+ <div class="edit-content-zone hide" data-write="issue-{{.Issue.ID}}-write" data-preview="issue-{{.Issue.ID}}-preview" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/content" data-context="{{.RepoLink}}" data-attachment-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/attachments" data-view-attachment-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/view-attachments"></div>
</div>
{{$reactions := .Issue.Reactions.GroupByType}}
{{if $reactions}}
@@ -57,15 +57,7 @@
{{if .Issue.Attachments}}
<div class="ui bottom attached segment">
<div class="ui small images">
- {{range .Issue.Attachments}}
- <a target="_blank" rel="noopener noreferrer" href="{{AppSubUrl}}/attachments/{{.UUID}}">
- {{if FilenameIsImage .Name}}
- <img class="ui image" src="{{AppSubUrl}}/attachments/{{.UUID}}" title='{{$.i18n.Tr "repo.issues.attachment.open_tab" .Name}}'>
- {{else}}
- <span class="ui image octicon octicon-desktop-download" title='{{$.i18n.Tr "repo.issues.attachment.download" .Name}}'></span>
- {{end}}
- </a>
- {{end}}
+ {{template "repo/issue/view_content/attachments" Dict "ctx" $ "Attachments" .Issue.Attachments}}
</div>
</div>
{{end}}
@@ -182,6 +174,19 @@
<div class="ui bottom attached tab preview segment markdown">
{{$.i18n.Tr "loading"}}
</div>
+ {{if .IsAttachmentEnabled}}
+ <div class="comment-files"></div>
+ <div class="ui basic button dropzone" id="comment-dropzone"
+ data-upload-url="{{AppSubUrl}}/attachments"
+ data-remove-url="{{AppSubUrl}}/attachments/delete"
+ data-csrf="{{.CsrfToken}}" data-accepts="{{.AttachmentAllowedTypes}}"
+ data-max-file="{{.AttachmentMaxFiles}}" data-max-size="{{.AttachmentMaxSize}}"
+ data-default-message="{{.i18n.Tr "dropzone.default_message"}}"
+ data-invalid-input-type="{{.i18n.Tr "dropzone.invalid_input_type"}}"
+ data-file-too-big="{{.i18n.Tr "dropzone.file_too_big"}}"
+ data-remove-file="{{.i18n.Tr "dropzone.remove_file"}}">
+ </div>
+ {{end}}
<div class="text right edit buttons">
<div class="ui basic blue cancel button" tabindex="3">{{.i18n.Tr "repo.issues.cancel"}}</div>
<div class="ui green save button" tabindex="2">{{.i18n.Tr "repo.issues.save"}}</div>
diff --git a/templates/repo/issue/view_content/attachments.tmpl b/templates/repo/issue/view_content/attachments.tmpl
new file mode 100644
index 0000000000..e2d7d1b9de
--- /dev/null
+++ b/templates/repo/issue/view_content/attachments.tmpl
@@ -0,0 +1,9 @@
+{{range .Attachments}}
+ <a target="_blank" rel="noopener noreferrer" href="{{AppSubUrl}}/attachments/{{.UUID}}">
+ {{if FilenameIsImage .Name}}
+ <img class="ui image" src="{{AppSubUrl}}/attachments/{{.UUID}}" title='{{$.ctx.i18n.Tr "repo.issues.attachment.open_tab" .Name}}'>
+ {{else}}
+ <span class="ui image octicon octicon-desktop-download" title='{{$.ctx.i18n.Tr "repo.issues.attachment.download" .Name}}'></span>
+ {{end}}
+ </a>
+{{end}} \ No newline at end of file
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index a5f25954c7..e3ea9ba822 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -55,7 +55,7 @@
{{end}}
</div>
<div class="raw-content hide">{{.Content}}</div>
- <div class="edit-content-zone hide" data-write="issuecomment-{{.ID}}-write" data-preview="issuecomment-{{.ID}}-preview" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}"></div>
+ <div class="edit-content-zone hide" data-write="issuecomment-{{.ID}}-write" data-preview="issuecomment-{{.ID}}-preview" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
</div>
{{$reactions := .Reactions.GroupByType}}
{{if $reactions}}
@@ -66,15 +66,7 @@
{{if .Attachments}}
<div class="ui bottom attached segment">
<div class="ui small images">
- {{range .Attachments}}
- <a target="_blank" rel="noopener noreferrer" href="{{AppSubUrl}}/attachments/{{.UUID}}">
- {{if FilenameIsImage .Name}}
- <img class="ui image" src="{{AppSubUrl}}/attachments/{{.UUID}}" title='{{$.i18n.Tr "repo.issues.attachment.open_tab" .Name}}'>
- {{else}}
- <span class="ui image octicon octicon-desktop-download" title='{{$.i18n.Tr "repo.issues.attachment.download" .Name}}'></span>
- {{end}}
- </a>
- {{end}}
+ {{template "repo/issue/view_content/attachments" Dict "ctx" $ "Attachments" .Attachments}}
</div>
</div>
{{end}}