| @@ -855,6 +855,26 @@ func AddDeletePRBranchComment(doer *User, repo *Repository, issueID int64, branc | |||||
| return sess.Commit() | 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. | // ChangeContent changes issue content, as the given user. | ||||
| func (issue *Issue) ChangeContent(doer *User, content string) (err error) { | func (issue *Issue) ChangeContent(doer *User, content string) (err error) { | ||||
| oldContent := issue.Content | oldContent := issue.Content | ||||
| @@ -357,6 +357,27 @@ func (c *Comment) LoadAttachments() error { | |||||
| return nil | 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 | // LoadAssigneeUser if comment.Type is CommentTypeAssignees, then load assignees | ||||
| func (c *Comment) LoadAssigneeUser() error { | func (c *Comment) LoadAssigneeUser() error { | ||||
| var err error | var err error | ||||
| @@ -35,6 +35,16 @@ func ExistsInSlice(target string, slice []string) bool { | |||||
| return i < len(slice) | 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. | // IsEqualSlice returns true if slices are equal. | ||||
| func IsEqualSlice(target []string, source []string) bool { | func IsEqualSlice(target []string, source []string) bool { | ||||
| if len(target) != len(source) { | if len(target) != len(source) { | ||||
| @@ -865,6 +865,73 @@ function initRepository() { | |||||
| issuesTribute.attach($textarea.get()); | issuesTribute.attach($textarea.get()); | ||||
| emojiTribute.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 | // Give new write/preview data-tab name to distinguish from others | ||||
| const $editContentForm = $editContentZone.find('.ui.comment.form'); | const $editContentForm = $editContentZone.find('.ui.comment.form'); | ||||
| const $tabMenu = $editContentForm.find('.tabular.menu'); | const $tabMenu = $editContentForm.find('.tabular.menu'); | ||||
| @@ -880,27 +947,49 @@ function initRepository() { | |||||
| $editContentZone.find('.cancel.button').click(function () { | $editContentZone.find('.cancel.button').click(function () { | ||||
| $renderContent.show(); | $renderContent.show(); | ||||
| $editContentZone.hide(); | $editContentZone.hide(); | ||||
| $dropzone.get(0).dropzone.emit("reload"); | |||||
| }); | }); | ||||
| $editContentZone.find('.save.button').click(function () { | $editContentZone.find('.save.button').click(function () { | ||||
| $renderContent.show(); | $renderContent.show(); | ||||
| $editContentZone.hide(); | $editContentZone.hide(); | ||||
| const $attachments = $files.find("[name=files]").map(function(){ | |||||
| return $(this).val(); | |||||
| }).get(); | |||||
| $.post($editContentZone.data('update-url'), { | $.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 { | } else { | ||||
| $textarea = $segment.find('textarea'); | $textarea = $segment.find('textarea'); | ||||
| @@ -63,3 +63,25 @@ func UploadAttachment(ctx *context.Context) { | |||||
| "uuid": attach.UUID, | "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, | |||||
| }) | |||||
| } | |||||
| @@ -34,6 +34,8 @@ import ( | |||||
| ) | ) | ||||
| const ( | const ( | ||||
| tplAttachment base.TplName = "repo/issue/view_content/attachments" | |||||
| tplIssues base.TplName = "repo/issue/list" | tplIssues base.TplName = "repo/issue/list" | ||||
| tplIssueNew base.TplName = "repo/issue/new" | tplIssueNew base.TplName = "repo/issue/new" | ||||
| tplIssueView base.TplName = "repo/issue/view" | tplIssueView base.TplName = "repo/issue/view" | ||||
| @@ -1074,8 +1076,14 @@ func UpdateIssueContent(ctx *context.Context) { | |||||
| return | return | ||||
| } | } | ||||
| files := ctx.QueryStrings("files[]") | |||||
| if err := updateAttachments(issue, files); err != nil { | |||||
| ctx.ServerError("UpdateAttachments", err) | |||||
| } | |||||
| ctx.JSON(200, map[string]interface{}{ | 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 | 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)) { | if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { | ||||
| ctx.Error(403) | ctx.Error(403) | ||||
| return | return | ||||
| @@ -1346,10 +1361,16 @@ func UpdateCommentContent(ctx *context.Context) { | |||||
| return | return | ||||
| } | } | ||||
| files := ctx.QueryStrings("files[]") | |||||
| if err := updateAttachments(comment, files); err != nil { | |||||
| ctx.ServerError("UpdateAttachments", err) | |||||
| } | |||||
| notification.NotifyUpdateComment(ctx.User, comment, oldContent) | notification.NotifyUpdateComment(ctx.User, comment, oldContent) | ||||
| ctx.JSON(200, map[string]interface{}{ | 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 | 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 | |||||
| } | |||||
| @@ -513,8 +513,9 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| }) | }) | ||||
| }, ignSignIn) | }, ignSignIn) | ||||
| m.Group("", func() { | |||||
| m.Post("/attachments", repo.UploadAttachment) | |||||
| m.Group("/attachments", func() { | |||||
| m.Post("", repo.UploadAttachment) | |||||
| m.Post("/delete", repo.DeleteAttachment) | |||||
| }, reqSignIn) | }, reqSignIn) | ||||
| m.Group("/:username", func() { | m.Group("/:username", func() { | ||||
| @@ -710,6 +711,7 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| m.Post("/reactions/:action", bindIgnErr(auth.ReactionForm{}), repo.ChangeIssueReaction) | m.Post("/reactions/:action", bindIgnErr(auth.ReactionForm{}), repo.ChangeIssueReaction) | ||||
| m.Post("/lock", reqRepoIssueWriter, bindIgnErr(auth.IssueLockForm{}), repo.LockIssue) | m.Post("/lock", reqRepoIssueWriter, bindIgnErr(auth.IssueLockForm{}), repo.LockIssue) | ||||
| m.Post("/unlock", reqRepoIssueWriter, repo.UnlockIssue) | m.Post("/unlock", reqRepoIssueWriter, repo.UnlockIssue) | ||||
| m.Get("/attachments", repo.GetIssueAttachments) | |||||
| }, context.RepoMustNotBeArchived()) | }, context.RepoMustNotBeArchived()) | ||||
| m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel) | m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel) | ||||
| @@ -721,6 +723,7 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| m.Post("", repo.UpdateCommentContent) | m.Post("", repo.UpdateCommentContent) | ||||
| m.Post("/delete", repo.DeleteComment) | m.Post("/delete", repo.DeleteComment) | ||||
| m.Post("/reactions/:action", bindIgnErr(auth.ReactionForm{}), repo.ChangeCommentReaction) | m.Post("/reactions/:action", bindIgnErr(auth.ReactionForm{}), repo.ChangeCommentReaction) | ||||
| m.Get("/attachments", repo.GetCommentAttachments) | |||||
| }, context.RepoMustNotBeArchived()) | }, context.RepoMustNotBeArchived()) | ||||
| m.Group("/labels", func() { | m.Group("/labels", func() { | ||||
| m.Post("/new", bindIgnErr(auth.CreateLabelForm{}), repo.NewLabel) | m.Post("/new", bindIgnErr(auth.CreateLabelForm{}), repo.NewLabel) | ||||
| @@ -46,7 +46,7 @@ | |||||
| {{end}} | {{end}} | ||||
| </div> | </div> | ||||
| <div class="raw-content hide">{{.Issue.Content}}</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> | </div> | ||||
| {{$reactions := .Issue.Reactions.GroupByType}} | {{$reactions := .Issue.Reactions.GroupByType}} | ||||
| {{if $reactions}} | {{if $reactions}} | ||||
| @@ -57,15 +57,7 @@ | |||||
| {{if .Issue.Attachments}} | {{if .Issue.Attachments}} | ||||
| <div class="ui bottom attached segment"> | <div class="ui bottom attached segment"> | ||||
| <div class="ui small images"> | <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> | ||||
| </div> | </div> | ||||
| {{end}} | {{end}} | ||||
| @@ -182,6 +174,19 @@ | |||||
| <div class="ui bottom attached tab preview segment markdown"> | <div class="ui bottom attached tab preview segment markdown"> | ||||
| {{$.i18n.Tr "loading"}} | {{$.i18n.Tr "loading"}} | ||||
| </div> | </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="text right edit buttons"> | ||||
| <div class="ui basic blue cancel button" tabindex="3">{{.i18n.Tr "repo.issues.cancel"}}</div> | <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> | <div class="ui green save button" tabindex="2">{{.i18n.Tr "repo.issues.save"}}</div> | ||||
| @@ -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}} | |||||
| @@ -55,7 +55,7 @@ | |||||
| {{end}} | {{end}} | ||||
| </div> | </div> | ||||
| <div class="raw-content hide">{{.Content}}</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> | </div> | ||||
| {{$reactions := .Reactions.GroupByType}} | {{$reactions := .Reactions.GroupByType}} | ||||
| {{if $reactions}} | {{if $reactions}} | ||||
| @@ -66,15 +66,7 @@ | |||||
| {{if .Attachments}} | {{if .Attachments}} | ||||
| <div class="ui bottom attached segment"> | <div class="ui bottom attached segment"> | ||||
| <div class="ui small images"> | <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> | ||||
| </div> | </div> | ||||
| {{end}} | {{end}} | ||||