Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add attachments for PR reviews #16075

Merged
merged 7 commits into from
Jun 15, 2021
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions models/issue_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 8 additions & 7 deletions models/review.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions routers/api/v1/repo/pull_review.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions routers/web/repo/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
8 changes: 7 additions & 1 deletion routers/web/repo/pull_review.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"))
Expand Down
1 change: 1 addition & 0 deletions services/forms/repo_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,7 @@ type SubmitReviewForm struct {
Content string
Type string `binding:"Required;In(approve,comment,reject)"`
CommitID string
Files []string
}

// Validate validates the fields
Expand Down
6 changes: 3 additions & 3 deletions services/pull/review.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand Down
5 changes: 5 additions & 0 deletions templates/repo/diff/new_review.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
<div class="ui field">
<textarea name="content" tabindex="0" rows="2" placeholder="{{$.i18n.Tr "repo.diff.review.placeholder"}}"></textarea>
</div>
{{if .IsAttachmentEnabled}}
<div class="field">
{{template "repo/upload" .}}
</div>
{{end}}
<div class="ui divider"></div>
<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>
<button type="submit" name="type" value="comment" class="ui submit tiny basic button btn-submit">{{$.i18n.Tr "repo.diff.review.comment"}}</button>
Expand Down
1 change: 0 additions & 1 deletion templates/repo/editor/upload.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
</div>
</div>
<div class="field">
<div class="files"></div>
{{template "repo/upload" .}}
</div>
{{template "repo/editor/commit_form" .}}
Expand Down
1 change: 0 additions & 1 deletion templates/repo/issue/comment_tab.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
</div>
{{if .IsAttachmentEnabled}}
<div class="field">
<div class="files"></div>
{{template "repo/upload" .}}
</div>
{{end}}
1 change: 0 additions & 1 deletion templates/repo/issue/view_content.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,6 @@
</div>
{{if .IsAttachmentEnabled}}
<div class="field">
<div class="comment-files"></div>
{{template "repo/upload" .}}
</div>
{{end}}
Expand Down
3 changes: 3 additions & 0 deletions templates/repo/issue/view_content/comments.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,9 @@
<span class="no-content">{{$.i18n.Tr "repo.issues.no_content"}}</span>
{{end}}
</div>
{{if .Attachments}}
{{template "repo/issue/view_content/attachments" Dict "ctx" $ "Attachments" .Attachments "Content" .RenderedContent}}
{{end}}
</div>
</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion templates/repo/release/new.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@
{{end}}
{{if .IsAttachmentEnabled}}
<div class="field">
<div class="files"></div>
{{template "repo/upload" .}}
</div>
{{end}}
Expand Down
5 changes: 3 additions & 2 deletions templates/repo/upload.tmpl
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<div
class="ui dropzone"
id="dropzone"
data-link-url="{{.UploadLinkUrl}}"
data-upload-url="{{.UploadUrl}}"
data-remove-url="{{.UploadRemoveUrl}}"
Expand All @@ -11,4 +10,6 @@
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>
>
<div class="files"></div>
</div>
77 changes: 49 additions & 28 deletions web_src/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 = $(`<input id="${data.uuid}" name="files" type="hidden">`).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 = $(`<input id="${data.uuid}" name="files" type="hidden">`).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 = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
Expand All @@ -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) {
Expand Down Expand Up @@ -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 = {};
Expand All @@ -1020,7 +1028,7 @@ async function initRepository() {
submitted: false
};
const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
$files.append(input);
$dropzone.find('.files').append(input);
});
this.on('removedfile', (file) => {
if (!(file.name in filenameDict)) {
Expand All @@ -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);
Expand All @@ -1055,7 +1063,7 @@ async function initRepository() {
};
$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.find('.files').append(input);
});
});
});
Expand All @@ -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();
Expand All @@ -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'), {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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'),
Expand All @@ -2633,7 +2654,7 @@ $(document).ready(async () => {
this.on('success', (file, data) => {
filenameDict[file.name] = data.uuid;
const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
$('.files').append(input);
$dropzone.find('.files').append(input);
});
this.on('removedfile', (file) => {
if (file.name in filenameDict) {
Expand Down