* 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>tags/v1.15.0-rc1
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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) | |||
} | |||
@@ -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")) |
@@ -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 |
@@ -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 | |||
} |
@@ -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> |
@@ -26,7 +26,6 @@ | |||
</div> | |||
</div> | |||
<div class="field"> | |||
<div class="files"></div> | |||
{{template "repo/upload" .}} | |||
</div> | |||
{{template "repo/editor/commit_form" .}} |
@@ -14,7 +14,6 @@ | |||
</div> | |||
{{if .IsAttachmentEnabled}} | |||
<div class="field"> | |||
<div class="files"></div> | |||
{{template "repo/upload" .}} | |||
</div> | |||
{{end}} |
@@ -197,7 +197,6 @@ | |||
</div> | |||
{{if .IsAttachmentEnabled}} | |||
<div class="field"> | |||
<div class="comment-files"></div> | |||
{{template "repo/upload" .}} | |||
</div> | |||
{{end}} |
@@ -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> |
@@ -76,7 +76,6 @@ | |||
{{end}} | |||
{{if .IsAttachmentEnabled}} | |||
<div class="field"> | |||
<div class="files"></div> | |||
{{template "repo/upload" .}} | |||
</div> | |||
{{end}} |
@@ -1,6 +1,5 @@ | |||
<div | |||
class="ui dropzone" | |||
id="dropzone" | |||
data-link-url="{{.UploadLinkUrl}}" | |||
data-upload-url="{{.UploadUrl}}" | |||
data-remove-url="{{.UploadRemoveUrl}}" | |||
@@ -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> |
@@ -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 = $(`<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); | |||
@@ -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 = $(`<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)) { | |||
@@ -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 = $(`<input id="${this.uuid}" name="files" type="hidden">`).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 = $(`<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) { |