From ebf253b841d56c5cb1e57cb1e5e50c06d315bdee Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 15 Jun 2021 03:12:33 +0200 Subject: [PATCH] Add attachments for PR reviews (#16075) * First step for multiple dropzones per page. * Allow attachments on review comments. * Lint. * Fixed accidental initialize of the review textarea. * Initialize SimpleMDE textarea. Co-authored-by: techknowlogick --- models/issue_comment.go | 2 + models/review.go | 15 ++-- routers/api/v1/repo/pull_review.go | 4 +- routers/web/repo/pull.go | 4 + routers/web/repo/pull_review.go | 8 +- services/forms/repo_form.go | 1 + services/pull/review.go | 6 +- templates/repo/diff/new_review.tmpl | 5 ++ templates/repo/editor/upload.tmpl | 1 - templates/repo/issue/comment_tab.tmpl | 1 - templates/repo/issue/view_content.tmpl | 1 - .../repo/issue/view_content/comments.tmpl | 3 + templates/repo/release/new.tmpl | 1 - templates/repo/upload.tmpl | 5 +- web_src/js/index.js | 77 ++++++++++++------- 15 files changed, 87 insertions(+), 47 deletions(-) diff --git a/models/issue_comment.go b/models/issue_comment.go index 26bf122dc9..1b98b248b1 100644 --- a/models/issue_comment.go +++ b/models/issue_comment.go @@ -762,6 +762,8 @@ func updateCommentInfos(e *xorm.Session, opts *CreateCommentOptions, comment *Co } } fallthrough + case CommentTypeReview: + fallthrough case CommentTypeComment: if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil { return err diff --git a/models/review.go b/models/review.go index 343621c0fa..316cbe4da6 100644 --- a/models/review.go +++ b/models/review.go @@ -347,7 +347,7 @@ func IsContentEmptyErr(err error) bool { } // SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist -func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content, commitID string, stale bool) (*Review, *Comment, error) { +func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content, commitID string, stale bool, attachmentUUIDs []string) (*Review, *Comment, error) { sess := x.NewSession() defer sess.Close() if err := sess.Begin(); err != nil { @@ -419,12 +419,13 @@ func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content, comm } comm, err := createComment(sess, &CreateCommentOptions{ - Type: CommentTypeReview, - Doer: doer, - Content: review.Content, - Issue: issue, - Repo: issue.Repo, - ReviewID: review.ID, + Type: CommentTypeReview, + Doer: doer, + Content: review.Content, + Issue: issue, + Repo: issue.Repo, + ReviewID: review.ID, + Attachments: attachmentUUIDs, }) if err != nil || comm == nil { return nil, nil, err diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index 63179aa990..35414e0a80 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -359,7 +359,7 @@ func CreatePullReview(ctx *context.APIContext) { } // create review and associate all pending review comments - review, _, err := pull_service.SubmitReview(ctx.User, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, opts.CommitID) + review, _, err := pull_service.SubmitReview(ctx.User, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, opts.CommitID, nil) if err != nil { ctx.Error(http.StatusInternalServerError, "SubmitReview", err) return @@ -447,7 +447,7 @@ func SubmitPullReview(ctx *context.APIContext) { } // create review and associate all pending review comments - review, _, err = pull_service.SubmitReview(ctx.User, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID) + review, _, err = pull_service.SubmitReview(ctx.User, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID, nil) if err != nil { ctx.Error(http.StatusInternalServerError, "SubmitReview", err) return diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 28f94c8417..e5554e9664 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -694,6 +694,10 @@ func ViewPullFiles(ctx *context.Context) { getBranchData(ctx, issue) ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID) ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) + + ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled + upload.AddUploadContext(ctx, "comment") + ctx.HTML(http.StatusOK, tplPullFiles) } diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go index 9e505c3db3..36eee3f377 100644 --- a/routers/web/repo/pull_review.go +++ b/routers/web/repo/pull_review.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/forms" pull_service "code.gitea.io/gitea/services/pull" @@ -211,7 +212,12 @@ func SubmitReview(ctx *context.Context) { } } - _, comm, err := pull_service.SubmitReview(ctx.User, ctx.Repo.GitRepo, issue, reviewType, form.Content, form.CommitID) + var attachments []string + if setting.Attachment.Enabled { + attachments = form.Files + } + + _, comm, err := pull_service.SubmitReview(ctx.User, ctx.Repo.GitRepo, issue, reviewType, form.Content, form.CommitID, attachments) if err != nil { if models.IsContentEmptyErr(err) { ctx.Flash.Error(ctx.Tr("repo.issues.review.content.empty")) diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index a40b0be9a7..71a83a8be3 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -587,6 +587,7 @@ type SubmitReviewForm struct { Content string Type string `binding:"Required;In(approve,comment,reject)"` CommitID string + Files []string } // Validate validates the fields diff --git a/services/pull/review.go b/services/pull/review.go index 4b647722fc..b07e21fad9 100644 --- a/services/pull/review.go +++ b/services/pull/review.go @@ -100,7 +100,7 @@ func CreateCodeComment(doer *models.User, gitRepo *git.Repository, issue *models if !isReview && !existsReview { // Submit the review we've just created so the comment shows up in the issue view - if _, _, err = SubmitReview(doer, gitRepo, issue, models.ReviewTypeComment, "", latestCommitID); err != nil { + if _, _, err = SubmitReview(doer, gitRepo, issue, models.ReviewTypeComment, "", latestCommitID, nil); err != nil { return nil, err } } @@ -215,7 +215,7 @@ func createCodeComment(doer *models.User, repo *models.Repository, issue *models } // SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist -func SubmitReview(doer *models.User, gitRepo *git.Repository, issue *models.Issue, reviewType models.ReviewType, content, commitID string) (*models.Review, *models.Comment, error) { +func SubmitReview(doer *models.User, gitRepo *git.Repository, issue *models.Issue, reviewType models.ReviewType, content, commitID string, attachmentUUIDs []string) (*models.Review, *models.Comment, error) { pr, err := issue.GetPullRequest() if err != nil { return nil, nil, err @@ -240,7 +240,7 @@ func SubmitReview(doer *models.User, gitRepo *git.Repository, issue *models.Issu } } - review, comm, err := models.SubmitReview(doer, issue, reviewType, content, commitID, stale) + review, comm, err := models.SubmitReview(doer, issue, reviewType, content, commitID, stale, attachmentUUIDs) if err != nil { return nil, nil, err } diff --git a/templates/repo/diff/new_review.tmpl b/templates/repo/diff/new_review.tmpl index 9e65d6d420..cbaabe255e 100644 --- a/templates/repo/diff/new_review.tmpl +++ b/templates/repo/diff/new_review.tmpl @@ -15,6 +15,11 @@
+ {{if .IsAttachmentEnabled}} +
+ {{template "repo/upload" .}} +
+ {{end}}
diff --git a/templates/repo/editor/upload.tmpl b/templates/repo/editor/upload.tmpl index 488465120e..fb00615abd 100644 --- a/templates/repo/editor/upload.tmpl +++ b/templates/repo/editor/upload.tmpl @@ -26,7 +26,6 @@
-
{{template "repo/upload" .}}
{{template "repo/editor/commit_form" .}} diff --git a/templates/repo/issue/comment_tab.tmpl b/templates/repo/issue/comment_tab.tmpl index 77e82930dc..22e1d5af84 100644 --- a/templates/repo/issue/comment_tab.tmpl +++ b/templates/repo/issue/comment_tab.tmpl @@ -14,7 +14,6 @@ {{if .IsAttachmentEnabled}}
-
{{template "repo/upload" .}}
{{end}} diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl index 00ce61921d..d2928df342 100644 --- a/templates/repo/issue/view_content.tmpl +++ b/templates/repo/issue/view_content.tmpl @@ -197,7 +197,6 @@ {{if .IsAttachmentEnabled}}
-
{{template "repo/upload" .}}
{{end}} diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 53005cc820..de31430ce0 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -449,6 +449,9 @@ {{$.i18n.Tr "repo.issues.no_content"}} {{end}} + {{if .Attachments}} + {{template "repo/issue/view_content/attachments" Dict "ctx" $ "Attachments" .Attachments "Content" .RenderedContent}} + {{end}} diff --git a/templates/repo/release/new.tmpl b/templates/repo/release/new.tmpl index c4b36597c6..49759713aa 100644 --- a/templates/repo/release/new.tmpl +++ b/templates/repo/release/new.tmpl @@ -76,7 +76,6 @@ {{end}} {{if .IsAttachmentEnabled}}
-
{{template "repo/upload" .}}
{{end}} diff --git a/templates/repo/upload.tmpl b/templates/repo/upload.tmpl index 9215da2b19..3dd40d1b27 100644 --- a/templates/repo/upload.tmpl +++ b/templates/repo/upload.tmpl @@ -1,6 +1,5 @@
+> +
+ diff --git a/web_src/js/index.js b/web_src/js/index.js index 8818511e32..e42a664015 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -327,11 +327,11 @@ function getPastedImages(e) { return files; } -async function uploadFile(file) { +async function uploadFile(file, uploadUrl) { const formData = new FormData(); formData.append('file', file, file.name); - const res = await fetch($('#dropzone').data('upload-url'), { + const res = await fetch(uploadUrl, { method: 'POST', headers: {'X-Csrf-Token': csrf}, body: formData, @@ -345,24 +345,33 @@ function reload() { function initImagePaste(target) { target.each(function () { - this.addEventListener('paste', async (e) => { - for (const img of getPastedImages(e)) { - const name = img.name.substr(0, img.name.lastIndexOf('.')); - insertAtCursor(this, `![${name}]()`); - const data = await uploadFile(img); - replaceAndKeepCursor(this, `![${name}]()`, `![${name}](${AppSubUrl}/attachments/${data.uuid})`); - const input = $(``).val(data.uuid); - $('.files').append(input); - } - }, false); + const dropzone = this.querySelector('.dropzone'); + if (!dropzone) { + return; + } + const uploadUrl = dropzone.dataset.uploadUrl; + const dropzoneFiles = dropzone.querySelector('.files'); + for (const textarea of this.querySelectorAll('textarea')) { + textarea.addEventListener('paste', async (e) => { + for (const img of getPastedImages(e)) { + const name = img.name.substr(0, img.name.lastIndexOf('.')); + insertAtCursor(textarea, `![${name}]()`); + const data = await uploadFile(img, uploadUrl); + replaceAndKeepCursor(textarea, `![${name}]()`, `![${name}](${AppSubUrl}/attachments/${data.uuid})`); + const input = $(``).val(data.uuid); + dropzoneFiles.appendChild(input[0]); + } + }, false); + } }); } -function initSimpleMDEImagePaste(simplemde, files) { +function initSimpleMDEImagePaste(simplemde, dropzone, files) { + const uploadUrl = dropzone.dataset.uploadUrl; simplemde.codemirror.on('paste', async (_, e) => { for (const img of getPastedImages(e)) { const name = img.name.substr(0, img.name.lastIndexOf('.')); - const data = await uploadFile(img); + const data = await uploadFile(img, uploadUrl); const pos = simplemde.codemirror.getCursor(); simplemde.codemirror.replaceRange(`![${name}](${AppSubUrl}/attachments/${data.uuid})`, pos); const input = $(``).val(data.uuid); @@ -381,7 +390,7 @@ function initCommentForm() { autoSimpleMDE = setCommentSimpleMDE($('.comment.form textarea:not(.review-textarea)')); initBranchSelector(); initCommentPreviewTab($('.comment.form')); - initImagePaste($('.comment.form textarea')); + initImagePaste($('.comment.form')); // Listsubmit function initListSubmits(selector, outerSelector) { @@ -993,8 +1002,7 @@ async function initRepository() { let dz; const $dropzone = $editContentZone.find('.dropzone'); - const $files = $editContentZone.find('.comment-files'); - if ($dropzone.length > 0) { + if ($dropzone.length === 1) { $dropzone.data('saved', false); const filenameDict = {}; @@ -1020,7 +1028,7 @@ async function initRepository() { submitted: false }; const input = $(``).val(data.uuid); - $files.append(input); + $dropzone.find('.files').append(input); }); this.on('removedfile', (file) => { if (!(file.name in filenameDict)) { @@ -1042,7 +1050,7 @@ async function initRepository() { this.on('reload', () => { $.getJSON($editContentZone.data('attachment-url'), (data) => { dz.removeAllFiles(true); - $files.empty(); + $dropzone.find('.files').empty(); $.each(data, function () { const imgSrc = `${$dropzone.data('link-url')}/${this.uuid}`; dz.emit('addedfile', this); @@ -1055,7 +1063,7 @@ async function initRepository() { }; $dropzone.find(`img[src='${imgSrc}']`).css('max-width', '100%'); const input = $(``).val(this.uuid); - $files.append(input); + $dropzone.find('.files').append(input); }); }); }); @@ -1075,7 +1083,9 @@ async function initRepository() { $simplemde = setCommentSimpleMDE($textarea); commentMDEditors[$editContentZone.data('write')] = $simplemde; initCommentPreviewTab($editContentForm); - initSimpleMDEImagePaste($simplemde, $files); + if ($dropzone.length === 1) { + initSimpleMDEImagePaste($simplemde, $dropzone[0], $dropzone.find('.files')); + } $editContentZone.find('.cancel.button').on('click', () => { $renderContent.show(); @@ -1087,7 +1097,7 @@ async function initRepository() { $editContentZone.find('.save.button').on('click', () => { $renderContent.show(); $editContentZone.hide(); - const $attachments = $files.find('[name=files]').map(function () { + const $attachments = $dropzone.find('.files').find('[name=files]').map(function () { return $(this).val(); }).get(); $.post($editContentZone.data('update-url'), { @@ -1369,6 +1379,13 @@ function initPullRequestReview() { $simplemde.codemirror.focus(); assingMenuAttributes(form.find('.menu')); }); + + const $reviewBox = $('.review-box'); + if ($reviewBox.length === 1) { + setCommentSimpleMDE($reviewBox.find('textarea')); + initImagePaste($reviewBox); + } + // The following part is only for diff views if ($('.repository.pull.diff').length === 0) { return; @@ -1656,6 +1673,10 @@ $.fn.getCursorPosition = function () { }; function setCommentSimpleMDE($editArea) { + if ($editArea.length === 0) { + return null; + } + const simplemde = new SimpleMDE({ autoDownloadFontAwesome: false, element: $editArea[0], @@ -1827,7 +1848,8 @@ function initReleaseEditor() { const $files = $editor.parent().find('.files'); const $simplemde = setCommentSimpleMDE($textarea); initCommentPreviewTab($editor); - initSimpleMDEImagePaste($simplemde, $files); + const dropzone = $editor.parent().find('.dropzone')[0]; + initSimpleMDEImagePaste($simplemde, dropzone, $files); } function initOrganization() { @@ -2610,11 +2632,10 @@ $(document).ready(async () => { initLinkAccountView(); // Dropzone - const $dropzone = $('#dropzone'); - if ($dropzone.length > 0) { + for (const el of document.querySelectorAll('.dropzone')) { const filenameDict = {}; - - await createDropzone('#dropzone', { + const $dropzone = $(el); + await createDropzone(el, { url: $dropzone.data('upload-url'), headers: {'X-Csrf-Token': csrf}, maxFiles: $dropzone.data('max-file'), @@ -2633,7 +2654,7 @@ $(document).ready(async () => { this.on('success', (file, data) => { filenameDict[file.name] = data.uuid; const input = $(``).val(data.uuid); - $('.files').append(input); + $dropzone.find('.files').append(input); }); this.on('removedfile', (file) => { if (file.name in filenameDict) {