Skip to content

Commit c05b676

Browse files
KN4CK3Rtechknowlogick
authored andcommitted
Add attachments for PR reviews (go-gitea#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 <techknowlogick@gitea.io>
1 parent 76611ef commit c05b676

File tree

15 files changed

+87
-47
lines changed

15 files changed

+87
-47
lines changed

models/issue_comment.go

+2
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,8 @@ func updateCommentInfos(e *xorm.Session, opts *CreateCommentOptions, comment *Co
762762
}
763763
}
764764
fallthrough
765+
case CommentTypeReview:
766+
fallthrough
765767
case CommentTypeComment:
766768
if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil {
767769
return err

models/review.go

+8-7
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ func IsContentEmptyErr(err error) bool {
347347
}
348348

349349
// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
350-
func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content, commitID string, stale bool) (*Review, *Comment, error) {
350+
func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content, commitID string, stale bool, attachmentUUIDs []string) (*Review, *Comment, error) {
351351
sess := x.NewSession()
352352
defer sess.Close()
353353
if err := sess.Begin(); err != nil {
@@ -419,12 +419,13 @@ func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content, comm
419419
}
420420

421421
comm, err := createComment(sess, &CreateCommentOptions{
422-
Type: CommentTypeReview,
423-
Doer: doer,
424-
Content: review.Content,
425-
Issue: issue,
426-
Repo: issue.Repo,
427-
ReviewID: review.ID,
422+
Type: CommentTypeReview,
423+
Doer: doer,
424+
Content: review.Content,
425+
Issue: issue,
426+
Repo: issue.Repo,
427+
ReviewID: review.ID,
428+
Attachments: attachmentUUIDs,
428429
})
429430
if err != nil || comm == nil {
430431
return nil, nil, err

routers/api/v1/repo/pull_review.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ func CreatePullReview(ctx *context.APIContext) {
359359
}
360360

361361
// create review and associate all pending review comments
362-
review, _, err := pull_service.SubmitReview(ctx.User, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, opts.CommitID)
362+
review, _, err := pull_service.SubmitReview(ctx.User, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, opts.CommitID, nil)
363363
if err != nil {
364364
ctx.Error(http.StatusInternalServerError, "SubmitReview", err)
365365
return
@@ -447,7 +447,7 @@ func SubmitPullReview(ctx *context.APIContext) {
447447
}
448448

449449
// create review and associate all pending review comments
450-
review, _, err = pull_service.SubmitReview(ctx.User, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID)
450+
review, _, err = pull_service.SubmitReview(ctx.User, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID, nil)
451451
if err != nil {
452452
ctx.Error(http.StatusInternalServerError, "SubmitReview", err)
453453
return

routers/web/repo/pull.go

+4
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,10 @@ func ViewPullFiles(ctx *context.Context) {
694694
getBranchData(ctx, issue)
695695
ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID)
696696
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
697+
698+
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
699+
upload.AddUploadContext(ctx, "comment")
700+
697701
ctx.HTML(http.StatusOK, tplPullFiles)
698702
}
699703

routers/web/repo/pull_review.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"code.gitea.io/gitea/modules/base"
1313
"code.gitea.io/gitea/modules/context"
1414
"code.gitea.io/gitea/modules/log"
15+
"code.gitea.io/gitea/modules/setting"
1516
"code.gitea.io/gitea/modules/web"
1617
"code.gitea.io/gitea/services/forms"
1718
pull_service "code.gitea.io/gitea/services/pull"
@@ -211,7 +212,12 @@ func SubmitReview(ctx *context.Context) {
211212
}
212213
}
213214

214-
_, comm, err := pull_service.SubmitReview(ctx.User, ctx.Repo.GitRepo, issue, reviewType, form.Content, form.CommitID)
215+
var attachments []string
216+
if setting.Attachment.Enabled {
217+
attachments = form.Files
218+
}
219+
220+
_, comm, err := pull_service.SubmitReview(ctx.User, ctx.Repo.GitRepo, issue, reviewType, form.Content, form.CommitID, attachments)
215221
if err != nil {
216222
if models.IsContentEmptyErr(err) {
217223
ctx.Flash.Error(ctx.Tr("repo.issues.review.content.empty"))

services/forms/repo_form.go

+1
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ type SubmitReviewForm struct {
587587
Content string
588588
Type string `binding:"Required;In(approve,comment,reject)"`
589589
CommitID string
590+
Files []string
590591
}
591592

592593
// Validate validates the fields

services/pull/review.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ func CreateCodeComment(doer *models.User, gitRepo *git.Repository, issue *models
100100

101101
if !isReview && !existsReview {
102102
// Submit the review we've just created so the comment shows up in the issue view
103-
if _, _, err = SubmitReview(doer, gitRepo, issue, models.ReviewTypeComment, "", latestCommitID); err != nil {
103+
if _, _, err = SubmitReview(doer, gitRepo, issue, models.ReviewTypeComment, "", latestCommitID, nil); err != nil {
104104
return nil, err
105105
}
106106
}
@@ -215,7 +215,7 @@ func createCodeComment(doer *models.User, repo *models.Repository, issue *models
215215
}
216216

217217
// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
218-
func SubmitReview(doer *models.User, gitRepo *git.Repository, issue *models.Issue, reviewType models.ReviewType, content, commitID string) (*models.Review, *models.Comment, error) {
218+
func SubmitReview(doer *models.User, gitRepo *git.Repository, issue *models.Issue, reviewType models.ReviewType, content, commitID string, attachmentUUIDs []string) (*models.Review, *models.Comment, error) {
219219
pr, err := issue.GetPullRequest()
220220
if err != nil {
221221
return nil, nil, err
@@ -240,7 +240,7 @@ func SubmitReview(doer *models.User, gitRepo *git.Repository, issue *models.Issu
240240
}
241241
}
242242

243-
review, comm, err := models.SubmitReview(doer, issue, reviewType, content, commitID, stale)
243+
review, comm, err := models.SubmitReview(doer, issue, reviewType, content, commitID, stale, attachmentUUIDs)
244244
if err != nil {
245245
return nil, nil, err
246246
}

templates/repo/diff/new_review.tmpl

+5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
<div class="ui field">
1616
<textarea name="content" tabindex="0" rows="2" placeholder="{{$.i18n.Tr "repo.diff.review.placeholder"}}"></textarea>
1717
</div>
18+
{{if .IsAttachmentEnabled}}
19+
<div class="field">
20+
{{template "repo/upload" .}}
21+
</div>
22+
{{end}}
1823
<div class="ui divider"></div>
1924
<button type="submit" name="type" value="approve" {{ if and $.IsSigned ($.Issue.IsPoster $.SignedUser.ID) }} disabled {{ end }} class="ui submit green tiny button btn-submit">{{$.i18n.Tr "repo.diff.review.approve"}}</button>
2025
<button type="submit" name="type" value="comment" class="ui submit tiny basic button btn-submit">{{$.i18n.Tr "repo.diff.review.comment"}}</button>

templates/repo/editor/upload.tmpl

-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
</div>
2727
</div>
2828
<div class="field">
29-
<div class="files"></div>
3029
{{template "repo/upload" .}}
3130
</div>
3231
{{template "repo/editor/commit_form" .}}

templates/repo/issue/comment_tab.tmpl

-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
</div>
1515
{{if .IsAttachmentEnabled}}
1616
<div class="field">
17-
<div class="files"></div>
1817
{{template "repo/upload" .}}
1918
</div>
2019
{{end}}

templates/repo/issue/view_content.tmpl

-1
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,6 @@
197197
</div>
198198
{{if .IsAttachmentEnabled}}
199199
<div class="field">
200-
<div class="comment-files"></div>
201200
{{template "repo/upload" .}}
202201
</div>
203202
{{end}}

templates/repo/issue/view_content/comments.tmpl

+3
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,9 @@
449449
<span class="no-content">{{$.i18n.Tr "repo.issues.no_content"}}</span>
450450
{{end}}
451451
</div>
452+
{{if .Attachments}}
453+
{{template "repo/issue/view_content/attachments" Dict "ctx" $ "Attachments" .Attachments "Content" .RenderedContent}}
454+
{{end}}
452455
</div>
453456
</div>
454457
</div>

templates/repo/release/new.tmpl

-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@
7676
{{end}}
7777
{{if .IsAttachmentEnabled}}
7878
<div class="field">
79-
<div class="files"></div>
8079
{{template "repo/upload" .}}
8180
</div>
8281
{{end}}

templates/repo/upload.tmpl

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<div
22
class="ui dropzone"
3-
id="dropzone"
43
data-link-url="{{.UploadLinkUrl}}"
54
data-upload-url="{{.UploadUrl}}"
65
data-remove-url="{{.UploadRemoveUrl}}"
@@ -11,4 +10,6 @@
1110
data-invalid-input-type="{{.i18n.Tr "dropzone.invalid_input_type"}}"
1211
data-file-too-big="{{.i18n.Tr "dropzone.file_too_big"}}"
1312
data-remove-file="{{.i18n.Tr "dropzone.remove_file"}}"
14-
></div>
13+
>
14+
<div class="files"></div>
15+
</div>

web_src/js/index.js

+49-28
Original file line numberDiff line numberDiff line change
@@ -327,11 +327,11 @@ function getPastedImages(e) {
327327
return files;
328328
}
329329

330-
async function uploadFile(file) {
330+
async function uploadFile(file, uploadUrl) {
331331
const formData = new FormData();
332332
formData.append('file', file, file.name);
333333

334-
const res = await fetch($('#dropzone').data('upload-url'), {
334+
const res = await fetch(uploadUrl, {
335335
method: 'POST',
336336
headers: {'X-Csrf-Token': csrf},
337337
body: formData,
@@ -345,24 +345,33 @@ function reload() {
345345

346346
function initImagePaste(target) {
347347
target.each(function () {
348-
this.addEventListener('paste', async (e) => {
349-
for (const img of getPastedImages(e)) {
350-
const name = img.name.substr(0, img.name.lastIndexOf('.'));
351-
insertAtCursor(this, `![${name}]()`);
352-
const data = await uploadFile(img);
353-
replaceAndKeepCursor(this, `![${name}]()`, `![${name}](${AppSubUrl}/attachments/${data.uuid})`);
354-
const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
355-
$('.files').append(input);
356-
}
357-
}, false);
348+
const dropzone = this.querySelector('.dropzone');
349+
if (!dropzone) {
350+
return;
351+
}
352+
const uploadUrl = dropzone.dataset.uploadUrl;
353+
const dropzoneFiles = dropzone.querySelector('.files');
354+
for (const textarea of this.querySelectorAll('textarea')) {
355+
textarea.addEventListener('paste', async (e) => {
356+
for (const img of getPastedImages(e)) {
357+
const name = img.name.substr(0, img.name.lastIndexOf('.'));
358+
insertAtCursor(textarea, `![${name}]()`);
359+
const data = await uploadFile(img, uploadUrl);
360+
replaceAndKeepCursor(textarea, `![${name}]()`, `![${name}](${AppSubUrl}/attachments/${data.uuid})`);
361+
const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
362+
dropzoneFiles.appendChild(input[0]);
363+
}
364+
}, false);
365+
}
358366
});
359367
}
360368

361-
function initSimpleMDEImagePaste(simplemde, files) {
369+
function initSimpleMDEImagePaste(simplemde, dropzone, files) {
370+
const uploadUrl = dropzone.dataset.uploadUrl;
362371
simplemde.codemirror.on('paste', async (_, e) => {
363372
for (const img of getPastedImages(e)) {
364373
const name = img.name.substr(0, img.name.lastIndexOf('.'));
365-
const data = await uploadFile(img);
374+
const data = await uploadFile(img, uploadUrl);
366375
const pos = simplemde.codemirror.getCursor();
367376
simplemde.codemirror.replaceRange(`![${name}](${AppSubUrl}/attachments/${data.uuid})`, pos);
368377
const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
@@ -381,7 +390,7 @@ function initCommentForm() {
381390
autoSimpleMDE = setCommentSimpleMDE($('.comment.form textarea:not(.review-textarea)'));
382391
initBranchSelector();
383392
initCommentPreviewTab($('.comment.form'));
384-
initImagePaste($('.comment.form textarea'));
393+
initImagePaste($('.comment.form'));
385394

386395
// Listsubmit
387396
function initListSubmits(selector, outerSelector) {
@@ -993,8 +1002,7 @@ async function initRepository() {
9931002

9941003
let dz;
9951004
const $dropzone = $editContentZone.find('.dropzone');
996-
const $files = $editContentZone.find('.comment-files');
997-
if ($dropzone.length > 0) {
1005+
if ($dropzone.length === 1) {
9981006
$dropzone.data('saved', false);
9991007

10001008
const filenameDict = {};
@@ -1020,7 +1028,7 @@ async function initRepository() {
10201028
submitted: false
10211029
};
10221030
const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
1023-
$files.append(input);
1031+
$dropzone.find('.files').append(input);
10241032
});
10251033
this.on('removedfile', (file) => {
10261034
if (!(file.name in filenameDict)) {
@@ -1042,7 +1050,7 @@ async function initRepository() {
10421050
this.on('reload', () => {
10431051
$.getJSON($editContentZone.data('attachment-url'), (data) => {
10441052
dz.removeAllFiles(true);
1045-
$files.empty();
1053+
$dropzone.find('.files').empty();
10461054
$.each(data, function () {
10471055
const imgSrc = `${$dropzone.data('link-url')}/${this.uuid}`;
10481056
dz.emit('addedfile', this);
@@ -1055,7 +1063,7 @@ async function initRepository() {
10551063
};
10561064
$dropzone.find(`img[src='${imgSrc}']`).css('max-width', '100%');
10571065
const input = $(`<input id="${this.uuid}" name="files" type="hidden">`).val(this.uuid);
1058-
$files.append(input);
1066+
$dropzone.find('.files').append(input);
10591067
});
10601068
});
10611069
});
@@ -1075,7 +1083,9 @@ async function initRepository() {
10751083
$simplemde = setCommentSimpleMDE($textarea);
10761084
commentMDEditors[$editContentZone.data('write')] = $simplemde;
10771085
initCommentPreviewTab($editContentForm);
1078-
initSimpleMDEImagePaste($simplemde, $files);
1086+
if ($dropzone.length === 1) {
1087+
initSimpleMDEImagePaste($simplemde, $dropzone[0], $dropzone.find('.files'));
1088+
}
10791089

10801090
$editContentZone.find('.cancel.button').on('click', () => {
10811091
$renderContent.show();
@@ -1087,7 +1097,7 @@ async function initRepository() {
10871097
$editContentZone.find('.save.button').on('click', () => {
10881098
$renderContent.show();
10891099
$editContentZone.hide();
1090-
const $attachments = $files.find('[name=files]').map(function () {
1100+
const $attachments = $dropzone.find('.files').find('[name=files]').map(function () {
10911101
return $(this).val();
10921102
}).get();
10931103
$.post($editContentZone.data('update-url'), {
@@ -1369,6 +1379,13 @@ function initPullRequestReview() {
13691379
$simplemde.codemirror.focus();
13701380
assingMenuAttributes(form.find('.menu'));
13711381
});
1382+
1383+
const $reviewBox = $('.review-box');
1384+
if ($reviewBox.length === 1) {
1385+
setCommentSimpleMDE($reviewBox.find('textarea'));
1386+
initImagePaste($reviewBox);
1387+
}
1388+
13721389
// The following part is only for diff views
13731390
if ($('.repository.pull.diff').length === 0) {
13741391
return;
@@ -1656,6 +1673,10 @@ $.fn.getCursorPosition = function () {
16561673
};
16571674

16581675
function setCommentSimpleMDE($editArea) {
1676+
if ($editArea.length === 0) {
1677+
return null;
1678+
}
1679+
16591680
const simplemde = new SimpleMDE({
16601681
autoDownloadFontAwesome: false,
16611682
element: $editArea[0],
@@ -1827,7 +1848,8 @@ function initReleaseEditor() {
18271848
const $files = $editor.parent().find('.files');
18281849
const $simplemde = setCommentSimpleMDE($textarea);
18291850
initCommentPreviewTab($editor);
1830-
initSimpleMDEImagePaste($simplemde, $files);
1851+
const dropzone = $editor.parent().find('.dropzone')[0];
1852+
initSimpleMDEImagePaste($simplemde, dropzone, $files);
18311853
}
18321854

18331855
function initOrganization() {
@@ -2610,11 +2632,10 @@ $(document).ready(async () => {
26102632
initLinkAccountView();
26112633

26122634
// Dropzone
2613-
const $dropzone = $('#dropzone');
2614-
if ($dropzone.length > 0) {
2635+
for (const el of document.querySelectorAll('.dropzone')) {
26152636
const filenameDict = {};
2616-
2617-
await createDropzone('#dropzone', {
2637+
const $dropzone = $(el);
2638+
await createDropzone(el, {
26182639
url: $dropzone.data('upload-url'),
26192640
headers: {'X-Csrf-Token': csrf},
26202641
maxFiles: $dropzone.data('max-file'),
@@ -2633,7 +2654,7 @@ $(document).ready(async () => {
26332654
this.on('success', (file, data) => {
26342655
filenameDict[file.name] = data.uuid;
26352656
const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
2636-
$('.files').append(input);
2657+
$dropzone.find('.files').append(input);
26372658
});
26382659
this.on('removedfile', (file) => {
26392660
if (file.name in filenameDict) {

0 commit comments

Comments
 (0)