mirror of
https://github.com/go-gitea/gitea.git
synced 2025-02-02 15:09:33 -05:00
Add reviewers selection to new pull request form
This commit is contained in:
parent
9914c9ab08
commit
d37f246d09
@ -554,7 +554,7 @@ func CreatePullRequest(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil {
|
||||
if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs, []int64{}); err != nil {
|
||||
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
|
||||
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
@ -39,6 +40,7 @@ import (
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/context/upload"
|
||||
"code.gitea.io/gitea/services/gitdiff"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -789,6 +791,47 @@ func CompareDiff(ctx *context.Context) {
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
// Get reviewer info for pr
|
||||
var (
|
||||
reviewers []*user_model.User
|
||||
teamReviewers []*organization.Team
|
||||
reviewersResult []*repoReviewerSelection
|
||||
)
|
||||
reviewers, err = repo_model.GetReviewers(ctx, ctx.Repo.Repository, ctx.Doer.ID, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetReviewers", err)
|
||||
return
|
||||
}
|
||||
|
||||
teamReviewers, err = repo_service.GetReviewerTeams(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetReviewerTeams", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, user := range reviewers {
|
||||
reviewersResult = append(reviewersResult, &repoReviewerSelection{
|
||||
IsTeam: false,
|
||||
CanChange: true,
|
||||
User: user,
|
||||
ItemID: user.ID,
|
||||
})
|
||||
}
|
||||
|
||||
// negative reviewIDs represent team requests
|
||||
for _, team := range teamReviewers {
|
||||
reviewersResult = append(reviewersResult, &repoReviewerSelection{
|
||||
IsTeam: true,
|
||||
CanChange: true,
|
||||
Team: team,
|
||||
ItemID: -team.ID,
|
||||
})
|
||||
}
|
||||
ctx.Data["Reviewers"] = reviewersResult
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
beforeCommitID := ctx.Data["BeforeCommitID"].(string)
|
||||
|
@ -1118,7 +1118,7 @@ func DeleteIssue(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// ValidateRepoMetas check and returns repository's meta information
|
||||
func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) ([]int64, []int64, int64, int64) {
|
||||
func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) ([]int64, []int64, []int64, int64, int64) {
|
||||
var (
|
||||
repo = ctx.Repo.Repository
|
||||
err error
|
||||
@ -1126,7 +1126,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
|
||||
|
||||
labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository, isPull)
|
||||
if ctx.Written() {
|
||||
return nil, nil, 0, 0
|
||||
return nil, nil, nil, 0, 0
|
||||
}
|
||||
|
||||
var labelIDs []int64
|
||||
@ -1135,7 +1135,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
|
||||
if len(form.LabelIDs) > 0 {
|
||||
labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0
|
||||
return nil, nil, nil, 0, 0
|
||||
}
|
||||
labelIDMark := make(container.Set[int64])
|
||||
labelIDMark.AddMultiple(labelIDs...)
|
||||
@ -1158,11 +1158,11 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
|
||||
milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetMilestoneByID", err)
|
||||
return nil, nil, 0, 0
|
||||
return nil, nil, nil, 0, 0
|
||||
}
|
||||
if milestone.RepoID != repo.ID {
|
||||
ctx.ServerError("GetMilestoneByID", err)
|
||||
return nil, nil, 0, 0
|
||||
return nil, nil, nil, 0, 0
|
||||
}
|
||||
ctx.Data["Milestone"] = milestone
|
||||
ctx.Data["milestone_id"] = milestoneID
|
||||
@ -1172,11 +1172,11 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
|
||||
p, err := project_model.GetProjectByID(ctx, form.ProjectID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjectByID", err)
|
||||
return nil, nil, 0, 0
|
||||
return nil, nil, nil, 0, 0
|
||||
}
|
||||
if p.RepoID != ctx.Repo.Repository.ID && p.OwnerID != ctx.Repo.Repository.OwnerID {
|
||||
ctx.NotFound("", nil)
|
||||
return nil, nil, 0, 0
|
||||
return nil, nil, nil, 0, 0
|
||||
}
|
||||
|
||||
ctx.Data["Project"] = p
|
||||
@ -1188,7 +1188,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
|
||||
if len(form.AssigneeIDs) > 0 {
|
||||
assigneeIDs, err = base.StringsToInt64s(strings.Split(form.AssigneeIDs, ","))
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0
|
||||
return nil, nil, nil, 0, 0
|
||||
}
|
||||
|
||||
// Check if the passed assignees actually exists and is assignable
|
||||
@ -1196,18 +1196,18 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
|
||||
assignee, err := user_model.GetUserByID(ctx, aID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserByID", err)
|
||||
return nil, nil, 0, 0
|
||||
return nil, nil, nil, 0, 0
|
||||
}
|
||||
|
||||
valid, err := access_model.CanBeAssigned(ctx, assignee, repo, isPull)
|
||||
if err != nil {
|
||||
ctx.ServerError("CanBeAssigned", err)
|
||||
return nil, nil, 0, 0
|
||||
return nil, nil, nil, 0, 0
|
||||
}
|
||||
|
||||
if !valid {
|
||||
ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
|
||||
return nil, nil, 0, 0
|
||||
return nil, nil, nil, 0, 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1217,7 +1217,34 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
|
||||
assigneeIDs = append(assigneeIDs, form.AssigneeID)
|
||||
}
|
||||
|
||||
return labelIDs, assigneeIDs, milestoneID, form.ProjectID
|
||||
// Check reviewers
|
||||
var reviewerIDs []int64
|
||||
if len(form.ReviewerIDs) > 0 {
|
||||
reviewerIDs, err = base.StringsToInt64s(strings.Split(form.ReviewerIDs, ","))
|
||||
if err != nil {
|
||||
return nil, nil, nil, 0, 0
|
||||
}
|
||||
|
||||
// Check if the passed reviewers actually exist
|
||||
for _, rID := range reviewerIDs {
|
||||
if rID < 0 {
|
||||
_, err := organization.GetTeamByID(ctx, -rID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetTeamByID", err)
|
||||
return nil, nil, nil, 0, 0
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := user_model.GetUserByID(ctx, rID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserByID", err)
|
||||
return nil, nil, nil, 0, 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return labelIDs, assigneeIDs, reviewerIDs, milestoneID, form.ProjectID
|
||||
}
|
||||
|
||||
// NewIssuePost response for creating new issue
|
||||
@ -1235,7 +1262,7 @@ func NewIssuePost(ctx *context.Context) {
|
||||
attachments []string
|
||||
)
|
||||
|
||||
labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, false)
|
||||
labelIDs, assigneeIDs, _, milestoneID, projectID := ValidateRepoMetas(ctx, *form, false)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
@ -1268,7 +1268,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, true)
|
||||
labelIDs, assigneeIDs, reviewerIDs, milestoneID, _ := ValidateRepoMetas(ctx, *form, true)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@ -1318,7 +1318,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
||||
// FIXME: check error in the case two people send pull request at almost same time, give nice error prompt
|
||||
// instead of 500.
|
||||
|
||||
if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
|
||||
if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs, reviewerIDs); err != nil {
|
||||
switch {
|
||||
case repo_model.IsErrUserDoesNotHaveAccessToRepo(err):
|
||||
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
|
||||
|
@ -138,7 +138,7 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
|
||||
Flow: issues_model.PullRequestFlowAGit,
|
||||
}
|
||||
|
||||
if err := pull_service.NewPullRequest(ctx, repo, prIssue, []int64{}, []string{}, pr, []int64{}); err != nil {
|
||||
if err := pull_service.NewPullRequest(ctx, repo, prIssue, []int64{}, []string{}, pr, []int64{}, []int64{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -447,6 +447,7 @@ type CreateIssueForm struct {
|
||||
Title string `binding:"Required;MaxSize(255)"`
|
||||
LabelIDs string `form:"label_ids"`
|
||||
AssigneeIDs string `form:"assignee_ids"`
|
||||
ReviewerIDs string `form:"reviewer_ids"`
|
||||
Ref string `form:"ref"`
|
||||
MilestoneID int64
|
||||
ProjectID int64
|
||||
|
@ -17,6 +17,7 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
@ -42,7 +43,7 @@ func getPullWorkingLockKey(prID int64) string {
|
||||
}
|
||||
|
||||
// NewPullRequest creates new pull request with labels for repository.
|
||||
func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64) error {
|
||||
func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64, reviewerIDs []int64) error {
|
||||
if err := issue.LoadPoster(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -116,6 +117,28 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *iss
|
||||
assigneeCommentMap[assigneeID] = comment
|
||||
}
|
||||
|
||||
for _, reviewerID := range reviewerIDs {
|
||||
if reviewerID < 0 {
|
||||
team, err := organization.GetTeamByID(ctx, -reviewerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = issue_service.TeamReviewRequest(ctx, issue, issue.Poster, team, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
reviewer, err := user_model.GetUserByID(ctx, reviewerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = issue_service.ReviewRequest(ctx, issue, issue.Poster, reviewer, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
pr.Issue = issue
|
||||
issue.PullRequest = pr
|
||||
|
||||
|
@ -48,6 +48,58 @@
|
||||
|
||||
<div class="issue-content-right ui segment">
|
||||
{{template "repo/issue/branch_selector_field" .}}
|
||||
{{if .PageIsComparePull}}
|
||||
<input id="reviewer_ids" name="reviewer_ids" type="hidden" value="{{.reviewer_ids}}">
|
||||
<div class="ui {{if not .Reviewers}}disabled{{end}} floating jump select-reviewers dropdown">
|
||||
<span class="text flex-text-block">
|
||||
<strong>{{.locale.Tr "repo.issues.review.reviewers"}}</strong>
|
||||
{{svg "octicon-gear" 16 "gt-ml-2"}}
|
||||
</span>
|
||||
<div class="filter menu" data-id="#reviewer_ids">
|
||||
<div class="ui icon search input">
|
||||
<i class="icon gt-df gt-ac gt-jc">{{svg "octicon-search" 16}}</i>
|
||||
<input type="text" placeholder="{{.locale.Tr "repo.issues.filter_reviewers"}}">
|
||||
</div>
|
||||
<div class="no-select item">{{.locale.Tr "repo.issues.new.clear_reviewers"}}</div>
|
||||
{{range .Reviewers}}
|
||||
{{if .User}}
|
||||
<a class="item muted" data-id="{{.ItemID}}" data-id-selector="#reviewer_{{.ItemID}}">
|
||||
<span class="octicon-check invisible">{{svg "octicon-check"}}</span>
|
||||
<span class="text">
|
||||
{{ctx.AvatarUtils.Avatar .User 28 "gt-mr-3"}}{{template "repo/search_name" .User}}
|
||||
</span>
|
||||
</a>
|
||||
{{else if .Team}}
|
||||
<a class="item muted" data-id="{{.ItemID}}" data-id-selector="#reviewer_{{.ItemID}}">
|
||||
<span class="octicon-check invisible">{{svg "octicon-check" 16}}</span>
|
||||
<span class="text">
|
||||
{{svg "octicon-people" 16 "gt-ml-4 gt-mr-2"}}{{$.Repository.OwnerName}}/{{.Team.Name}}
|
||||
</span>
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui reviewers list">
|
||||
<span class="no-select item">
|
||||
{{.locale.Tr "repo.issues.new.no_reviewers"}}
|
||||
</span>
|
||||
<div class="selected">
|
||||
{{range .Reviewers}}
|
||||
{{if .User}}
|
||||
<a class="item gt-p-2 muted gt-hidden" id="reviewer_{{.ItemID}}" href="#">
|
||||
{{ctx.AvatarUtils.Avatar .User 28 "gt-mr-3 gt-vm"}}{{.User.GetDisplayName}}
|
||||
</a>
|
||||
{{else if .Team}}
|
||||
<a class="item gt-p-2 muted gt-hidden" id="reviewer_{{.ItemID}}" href="#">
|
||||
{{svg "octicon-people" 20 "gt-mr-3"}}{{$.Repository.OwnerName}}/{{.Team.Name}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
{{end}}
|
||||
|
||||
<input id="label_ids" name="label_ids" type="hidden" value="{{.label_ids}}">
|
||||
{{template "repo/issue/labels/labels_selector_field" .}}
|
||||
|
@ -145,7 +145,7 @@ func TestPullRequestTargetEvent(t *testing.T) {
|
||||
BaseRepo: baseRepo,
|
||||
Type: issues_model.PullRequestGitea,
|
||||
}
|
||||
err = pull_service.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil)
|
||||
err = pull_service.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// load and compare ActionRun
|
||||
@ -199,7 +199,7 @@ func TestPullRequestTargetEvent(t *testing.T) {
|
||||
BaseRepo: baseRepo,
|
||||
Type: issues_model.PullRequestGitea,
|
||||
}
|
||||
err = pull_service.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil)
|
||||
err = pull_service.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// the new pull request cannot trigger actions, so there is still only 1 record
|
||||
|
@ -520,7 +520,7 @@ func TestConflictChecking(t *testing.T) {
|
||||
BaseRepo: baseRepo,
|
||||
Type: issues_model.PullRequestGitea,
|
||||
}
|
||||
err = pull.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil)
|
||||
err = pull.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"})
|
||||
|
@ -173,7 +173,7 @@ func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_mod
|
||||
BaseRepo: baseRepo,
|
||||
Type: issues_model.PullRequestGitea,
|
||||
}
|
||||
err = pull_service.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil)
|
||||
err = pull_service.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "Test Pull -to-update-"})
|
||||
|
600
web_src/js/features/repo-legacy.js
Normal file
600
web_src/js/features/repo-legacy.js
Normal file
@ -0,0 +1,600 @@
|
||||
import $ from 'jquery';
|
||||
import {
|
||||
initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, initRepoIssueCommentDelete,
|
||||
initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueReferenceIssue,
|
||||
initRepoIssueTitleEdit, initRepoIssueWipToggle,
|
||||
initRepoPullRequestUpdate, updateIssuesMeta, handleReply, initIssueTemplateCommentEditors, initSingleCommentEditor,
|
||||
} from './repo-issue.js';
|
||||
import {initUnicodeEscapeButton} from './repo-unicode-escape.js';
|
||||
import {svg} from '../svg.js';
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
import {initRepoBranchTagSelector} from '../components/RepoBranchTagSelector.vue';
|
||||
import {
|
||||
initRepoCloneLink, initRepoCommonBranchOrTagDropdown, initRepoCommonFilterSearchDropdown,
|
||||
initRepoCommonLanguageStats,
|
||||
} from './repo-common.js';
|
||||
import {initCitationFileCopyContent} from './citation.js';
|
||||
import {initCompLabelEdit} from './comp/LabelEdit.js';
|
||||
import {initRepoDiffConversationNav} from './repo-diff.js';
|
||||
import {createDropzone} from './dropzone.js';
|
||||
import {initCommentContent, initMarkupContent} from '../markup/content.js';
|
||||
import {initCompReactionSelector} from './comp/ReactionSelector.js';
|
||||
import {initRepoSettingBranches} from './repo-settings.js';
|
||||
import {initRepoPullRequestMergeForm} from './repo-issue-pr-form.js';
|
||||
import {hideElem, showElem} from '../utils/dom.js';
|
||||
import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
|
||||
import {attachRefIssueContextPopup} from './contextpopup.js';
|
||||
|
||||
const {csrfToken} = window.config;
|
||||
|
||||
// if there are draft comments, confirm before reloading, to avoid losing comments
|
||||
function reloadConfirmDraftComment() {
|
||||
const commentTextareas = [
|
||||
document.querySelector('.edit-content-zone:not(.gt-hidden) textarea'),
|
||||
document.querySelector('#comment-form textarea'),
|
||||
];
|
||||
for (const textarea of commentTextareas) {
|
||||
// Most users won't feel too sad if they lose a comment with 10 chars, they can re-type these in seconds.
|
||||
// But if they have typed more (like 50) chars and the comment is lost, they will be very unhappy.
|
||||
if (textarea && textarea.value.trim().length > 10) {
|
||||
textarea.parentElement.scrollIntoView();
|
||||
if (!window.confirm('Page will be reloaded, but there are draft comments. Continuing to reload will discard the comments. Continue?')) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
export function initRepoCommentForm() {
|
||||
const $commentForm = $('.comment.form');
|
||||
if ($commentForm.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($commentForm.find('.field.combo-editor-dropzone').length) {
|
||||
// at the moment, if a form has multiple combo-markdown-editors, it must be a issue template form
|
||||
initIssueTemplateCommentEditors($commentForm);
|
||||
} else {
|
||||
initSingleCommentEditor($commentForm);
|
||||
}
|
||||
|
||||
function initBranchSelector() {
|
||||
const $selectBranch = $('.ui.select-branch');
|
||||
const $branchMenu = $selectBranch.find('.reference-list-menu');
|
||||
const $isNewIssue = $branchMenu.hasClass('new-issue');
|
||||
$branchMenu.find('.item:not(.no-select)').on('click', function () {
|
||||
const selectedValue = $(this).data('id');
|
||||
const editMode = $('#editing_mode').val();
|
||||
$($(this).data('id-selector')).val(selectedValue);
|
||||
if ($isNewIssue) {
|
||||
$selectBranch.find('.ui .branch-name').text($(this).data('name'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (editMode === 'true') {
|
||||
const form = $('#update_issueref_form');
|
||||
$.post(form.attr('action'), {_csrf: csrfToken, ref: selectedValue}, () => window.location.reload());
|
||||
} else if (editMode === '') {
|
||||
$selectBranch.find('.ui .branch-name').text(selectedValue);
|
||||
}
|
||||
});
|
||||
$selectBranch.find('.reference.column').on('click', function () {
|
||||
hideElem($selectBranch.find('.scrolling.reference-list-menu'));
|
||||
$selectBranch.find('.reference .text').removeClass('black');
|
||||
showElem($($(this).data('target')));
|
||||
$(this).find('.text').addClass('black');
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
initBranchSelector();
|
||||
|
||||
// List submits
|
||||
function initListSubmits(selector, outerSelector) {
|
||||
const $list = $(`.ui.${outerSelector}.list`);
|
||||
const $noSelect = $list.find('.no-select');
|
||||
const $listMenu = $(`.${selector} .menu`);
|
||||
let hasUpdateAction = $listMenu.data('action') === 'update';
|
||||
const items = {};
|
||||
|
||||
$(`.${selector}`).dropdown({
|
||||
'action': 'nothing', // do not hide the menu if user presses Enter
|
||||
fullTextSearch: 'exact',
|
||||
async onHide() {
|
||||
hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var
|
||||
if (hasUpdateAction) {
|
||||
// TODO: Add batch functionality and make this 1 network request.
|
||||
const itemEntries = Object.entries(items);
|
||||
for (const [elementId, item] of itemEntries) {
|
||||
await updateIssuesMeta(
|
||||
item['update-url'],
|
||||
item.action,
|
||||
item['issue-id'],
|
||||
elementId,
|
||||
);
|
||||
}
|
||||
if (itemEntries.length) {
|
||||
reloadConfirmDraftComment();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
$listMenu.find('.item:not(.no-select)').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
if ($(this).hasClass('ban-change')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var
|
||||
|
||||
const clickedItem = $(this);
|
||||
const scope = $(this).attr('data-scope');
|
||||
|
||||
$(this).parent().find('.item').each(function () {
|
||||
if (scope) {
|
||||
// Enable only clicked item for scoped labels
|
||||
if ($(this).attr('data-scope') !== scope) {
|
||||
return true;
|
||||
}
|
||||
if (!$(this).is(clickedItem) && !$(this).hasClass('checked')) {
|
||||
return true;
|
||||
}
|
||||
} else if (!$(this).is(clickedItem)) {
|
||||
// Toggle for other labels
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($(this).hasClass('checked')) {
|
||||
$(this).removeClass('checked');
|
||||
$(this).find('.octicon-check').addClass('invisible');
|
||||
if (hasUpdateAction) {
|
||||
if (!($(this).data('id') in items)) {
|
||||
items[$(this).data('id')] = {
|
||||
'update-url': $listMenu.data('update-url'),
|
||||
action: 'detach',
|
||||
'issue-id': $listMenu.data('issue-id'),
|
||||
};
|
||||
} else {
|
||||
delete items[$(this).data('id')];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$(this).addClass('checked');
|
||||
$(this).find('.octicon-check').removeClass('invisible');
|
||||
if (hasUpdateAction) {
|
||||
if (!($(this).data('id') in items)) {
|
||||
items[$(this).data('id')] = {
|
||||
'update-url': $listMenu.data('update-url'),
|
||||
action: 'attach',
|
||||
'issue-id': $listMenu.data('issue-id'),
|
||||
};
|
||||
} else {
|
||||
delete items[$(this).data('id')];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: Which thing should be done for choosing review requests
|
||||
// to make chosen items be shown on time here?
|
||||
if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const listIds = [];
|
||||
$(this).parent().find('.item').each(function () {
|
||||
if ($(this).hasClass('checked')) {
|
||||
listIds.push($(this).data('id'));
|
||||
$($(this).data('id-selector')).removeClass('gt-hidden');
|
||||
} else {
|
||||
$($(this).data('id-selector')).addClass('gt-hidden');
|
||||
}
|
||||
});
|
||||
if (listIds.length === 0) {
|
||||
$noSelect.removeClass('gt-hidden');
|
||||
} else {
|
||||
$noSelect.addClass('gt-hidden');
|
||||
}
|
||||
$($(this).parent().data('id')).val(listIds.join(','));
|
||||
return false;
|
||||
});
|
||||
$listMenu.find('.no-select.item').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
if (hasUpdateAction) {
|
||||
updateIssuesMeta(
|
||||
$listMenu.data('update-url'),
|
||||
'clear',
|
||||
$listMenu.data('issue-id'),
|
||||
'',
|
||||
).then(reloadConfirmDraftComment);
|
||||
}
|
||||
|
||||
$(this).parent().find('.item').each(function () {
|
||||
$(this).removeClass('checked');
|
||||
$(this).find('.octicon-check').addClass('invisible');
|
||||
});
|
||||
|
||||
if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$list.find('.item').each(function () {
|
||||
$(this).addClass('gt-hidden');
|
||||
});
|
||||
$noSelect.removeClass('gt-hidden');
|
||||
$($(this).parent().data('id')).val('');
|
||||
});
|
||||
}
|
||||
|
||||
// Init labels and assignees
|
||||
initListSubmits('select-label', 'labels');
|
||||
initListSubmits('select-assignees', 'assignees');
|
||||
initListSubmits('select-assignees-modify', 'assignees');
|
||||
initListSubmits('select-reviewers', 'reviewers');
|
||||
initListSubmits('select-reviewers-modify', 'reviewers');
|
||||
|
||||
function selectItem(select_id, input_id) {
|
||||
const $menu = $(`${select_id} .menu`);
|
||||
const $list = $(`.ui${select_id}.list`);
|
||||
const hasUpdateAction = $menu.data('action') === 'update';
|
||||
|
||||
$menu.find('.item:not(.no-select)').on('click', function () {
|
||||
$(this).parent().find('.item').each(function () {
|
||||
$(this).removeClass('selected active');
|
||||
});
|
||||
|
||||
$(this).addClass('selected active');
|
||||
if (hasUpdateAction) {
|
||||
updateIssuesMeta(
|
||||
$menu.data('update-url'),
|
||||
'',
|
||||
$menu.data('issue-id'),
|
||||
$(this).data('id'),
|
||||
).then(reloadConfirmDraftComment);
|
||||
}
|
||||
|
||||
let icon = '';
|
||||
if (input_id === '#milestone_id') {
|
||||
icon = svg('octicon-milestone', 18, 'gt-mr-3');
|
||||
} else if (input_id === '#project_id') {
|
||||
icon = svg('octicon-project', 18, 'gt-mr-3');
|
||||
} else if (input_id === '#assignee_id') {
|
||||
icon = `<img class="ui avatar image gt-mr-3" alt="avatar" src=${$(this).data('avatar')}>`;
|
||||
} else if (input_id === '#reviewer_id') {
|
||||
icon = `<img class="ui avatar image gt-mr-3" alt="avatar" src=${$(this).data('avatar')}>`;
|
||||
}
|
||||
|
||||
$list.find('.selected').html(`
|
||||
<a class="item muted sidebar-item-link" href=${$(this).data('href')}>
|
||||
${icon}
|
||||
${htmlEscape($(this).text())}
|
||||
</a>
|
||||
`);
|
||||
|
||||
$(`.ui${select_id}.list .no-select`).addClass('gt-hidden');
|
||||
$(input_id).val($(this).data('id'));
|
||||
});
|
||||
$menu.find('.no-select.item').on('click', function () {
|
||||
$(this).parent().find('.item:not(.no-select)').each(function () {
|
||||
$(this).removeClass('selected active');
|
||||
});
|
||||
|
||||
if (hasUpdateAction) {
|
||||
updateIssuesMeta(
|
||||
$menu.data('update-url'),
|
||||
'',
|
||||
$menu.data('issue-id'),
|
||||
$(this).data('id'),
|
||||
).then(reloadConfirmDraftComment);
|
||||
}
|
||||
|
||||
$list.find('.selected').html('');
|
||||
$list.find('.no-select').removeClass('gt-hidden');
|
||||
$(input_id).val('');
|
||||
});
|
||||
}
|
||||
|
||||
// Milestone, Assignee, Project
|
||||
selectItem('.select-project', '#project_id');
|
||||
selectItem('.select-milestone', '#milestone_id');
|
||||
selectItem('.select-assignee', '#assignee_id');
|
||||
selectItem('.select-reviewer', '#reviewer_id');
|
||||
}
|
||||
|
||||
|
||||
async function onEditContent(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const $segment = $(this).closest('.header').next();
|
||||
const $editContentZone = $segment.find('.edit-content-zone');
|
||||
const $renderContent = $segment.find('.render-content');
|
||||
const $rawContent = $segment.find('.raw-content');
|
||||
|
||||
let comboMarkdownEditor;
|
||||
|
||||
const setupDropzone = async ($dropzone) => {
|
||||
if ($dropzone.length === 0) return null;
|
||||
|
||||
let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event
|
||||
let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone
|
||||
const dz = await createDropzone($dropzone[0], {
|
||||
url: $dropzone.attr('data-upload-url'),
|
||||
headers: {'X-Csrf-Token': csrfToken},
|
||||
maxFiles: $dropzone.attr('data-max-file'),
|
||||
maxFilesize: $dropzone.attr('data-max-size'),
|
||||
acceptedFiles: (['*/*', ''].includes($dropzone.attr('data-accepts'))) ? null : $dropzone.attr('data-accepts'),
|
||||
addRemoveLinks: true,
|
||||
dictDefaultMessage: $dropzone.attr('data-default-message'),
|
||||
dictInvalidFileType: $dropzone.attr('data-invalid-input-type'),
|
||||
dictFileTooBig: $dropzone.attr('data-file-too-big'),
|
||||
dictRemoveFile: $dropzone.attr('data-remove-file'),
|
||||
timeout: 0,
|
||||
thumbnailMethod: 'contain',
|
||||
thumbnailWidth: 480,
|
||||
thumbnailHeight: 480,
|
||||
init() {
|
||||
this.on('success', (file, data) => {
|
||||
file.uuid = data.uuid;
|
||||
fileUuidDict[file.uuid] = {submitted: false};
|
||||
const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
|
||||
$dropzone.find('.files').append(input);
|
||||
});
|
||||
this.on('removedfile', (file) => {
|
||||
if (disableRemovedfileEvent) return;
|
||||
$(`#${file.uuid}`).remove();
|
||||
if ($dropzone.attr('data-remove-url') && !fileUuidDict[file.uuid].submitted) {
|
||||
$.post($dropzone.attr('data-remove-url'), {
|
||||
file: file.uuid,
|
||||
_csrf: csrfToken,
|
||||
});
|
||||
}
|
||||
});
|
||||
this.on('submit', () => {
|
||||
$.each(fileUuidDict, (fileUuid) => {
|
||||
fileUuidDict[fileUuid].submitted = true;
|
||||
});
|
||||
});
|
||||
this.on('reload', () => {
|
||||
$.getJSON($editContentZone.attr('data-attachment-url'), (data) => {
|
||||
// do not trigger the "removedfile" event, otherwise the attachments would be deleted from server
|
||||
disableRemovedfileEvent = true;
|
||||
dz.removeAllFiles(true);
|
||||
$dropzone.find('.files').empty();
|
||||
fileUuidDict = {};
|
||||
disableRemovedfileEvent = false;
|
||||
|
||||
for (const attachment of data) {
|
||||
const imgSrc = `${$dropzone.attr('data-link-url')}/${attachment.uuid}`;
|
||||
dz.emit('addedfile', attachment);
|
||||
dz.emit('thumbnail', attachment, imgSrc);
|
||||
dz.emit('complete', attachment);
|
||||
dz.files.push(attachment);
|
||||
fileUuidDict[attachment.uuid] = {submitted: true};
|
||||
$dropzone.find(`img[src='${imgSrc}']`).css('max-width', '100%');
|
||||
const input = $(`<input id="${attachment.uuid}" name="files" type="hidden">`).val(attachment.uuid);
|
||||
$dropzone.find('.files').append(input);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
dz.emit('reload');
|
||||
return dz;
|
||||
};
|
||||
|
||||
const cancelAndReset = (dz) => {
|
||||
showElem($renderContent);
|
||||
hideElem($editContentZone);
|
||||
if (dz) {
|
||||
dz.emit('reload');
|
||||
}
|
||||
};
|
||||
|
||||
const saveAndRefresh = (dz, $dropzone) => {
|
||||
showElem($renderContent);
|
||||
hideElem($editContentZone);
|
||||
const $attachments = $dropzone.find('.files').find('[name=files]').map(function () {
|
||||
return $(this).val();
|
||||
}).get();
|
||||
$.post($editContentZone.attr('data-update-url'), {
|
||||
_csrf: csrfToken,
|
||||
content: comboMarkdownEditor.value(),
|
||||
context: $editContentZone.attr('data-context'),
|
||||
files: $attachments,
|
||||
}, (data) => {
|
||||
if (!data.content) {
|
||||
$renderContent.html($('#no-content').html());
|
||||
$rawContent.text('');
|
||||
} else {
|
||||
$renderContent.html(data.content);
|
||||
$rawContent.text(comboMarkdownEditor.value());
|
||||
|
||||
const refIssues = $renderContent.find('p .ref-issue');
|
||||
attachRefIssueContextPopup(refIssues);
|
||||
}
|
||||
const $content = $segment;
|
||||
if (!$content.find('.dropzone-attachments').length) {
|
||||
if (data.attachments !== '') {
|
||||
$content.append(`<div class="dropzone-attachments"></div>`);
|
||||
$content.find('.dropzone-attachments').replaceWith(data.attachments);
|
||||
}
|
||||
} else if (data.attachments === '') {
|
||||
$content.find('.dropzone-attachments').remove();
|
||||
} else {
|
||||
$content.find('.dropzone-attachments').replaceWith(data.attachments);
|
||||
}
|
||||
if (dz) {
|
||||
dz.emit('submit');
|
||||
dz.emit('reload');
|
||||
}
|
||||
initMarkupContent();
|
||||
initCommentContent();
|
||||
});
|
||||
};
|
||||
|
||||
if (!$editContentZone.html()) {
|
||||
$editContentZone.html($('#issue-comment-editor-template').html());
|
||||
comboMarkdownEditor = await initComboMarkdownEditor($editContentZone.find('.combo-markdown-editor'));
|
||||
|
||||
const $dropzone = $editContentZone.find('.dropzone');
|
||||
const dz = await setupDropzone($dropzone);
|
||||
$editContentZone.find('.cancel.button').on('click', (e) => {
|
||||
e.preventDefault();
|
||||
cancelAndReset(dz);
|
||||
});
|
||||
$editContentZone.find('.save.button').on('click', (e) => {
|
||||
e.preventDefault();
|
||||
saveAndRefresh(dz, $dropzone);
|
||||
});
|
||||
} else {
|
||||
comboMarkdownEditor = getComboMarkdownEditor($editContentZone.find('.combo-markdown-editor'));
|
||||
}
|
||||
|
||||
// Show write/preview tab and copy raw content as needed
|
||||
showElem($editContentZone);
|
||||
hideElem($renderContent);
|
||||
if (!comboMarkdownEditor.value()) {
|
||||
comboMarkdownEditor.value($rawContent.text());
|
||||
}
|
||||
comboMarkdownEditor.focus();
|
||||
}
|
||||
|
||||
export function initRepository() {
|
||||
if ($('.page-content.repository').length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
initRepoBranchTagSelector('.js-branch-tag-selector');
|
||||
|
||||
// Options
|
||||
if ($('.repository.settings.options').length > 0) {
|
||||
// Enable or select internal/external wiki system and issue tracker.
|
||||
$('.enable-system').on('change', function () {
|
||||
if (this.checked) {
|
||||
$($(this).data('target')).removeClass('disabled');
|
||||
if (!$(this).data('context')) $($(this).data('context')).addClass('disabled');
|
||||
} else {
|
||||
$($(this).data('target')).addClass('disabled');
|
||||
if (!$(this).data('context')) $($(this).data('context')).removeClass('disabled');
|
||||
}
|
||||
});
|
||||
$('.enable-system-radio').on('change', function () {
|
||||
if (this.value === 'false') {
|
||||
$($(this).data('target')).addClass('disabled');
|
||||
if ($(this).data('context') !== undefined) $($(this).data('context')).removeClass('disabled');
|
||||
} else if (this.value === 'true') {
|
||||
$($(this).data('target')).removeClass('disabled');
|
||||
if ($(this).data('context') !== undefined) $($(this).data('context')).addClass('disabled');
|
||||
}
|
||||
});
|
||||
const $trackerIssueStyleRadios = $('.js-tracker-issue-style');
|
||||
$trackerIssueStyleRadios.on('change input', () => {
|
||||
const checkedVal = $trackerIssueStyleRadios.filter(':checked').val();
|
||||
$('#tracker-issue-style-regex-box').toggleClass('disabled', checkedVal !== 'regexp');
|
||||
});
|
||||
}
|
||||
|
||||
// Labels
|
||||
initCompLabelEdit('.repository.labels');
|
||||
|
||||
// Milestones
|
||||
if ($('.repository.new.milestone').length > 0) {
|
||||
$('#clear-date').on('click', () => {
|
||||
$('#deadline').val('');
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// Repo Creation
|
||||
if ($('.repository.new.repo').length > 0) {
|
||||
$('input[name="gitignores"], input[name="license"]').on('change', () => {
|
||||
const gitignores = $('input[name="gitignores"]').val();
|
||||
const license = $('input[name="license"]').val();
|
||||
if (gitignores || license) {
|
||||
$('input[name="auto_init"]').prop('checked', true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Compare or pull request
|
||||
const $repoDiff = $('.repository.diff');
|
||||
if ($repoDiff.length) {
|
||||
initRepoCommonBranchOrTagDropdown('.choose.branch .dropdown');
|
||||
initRepoCommonFilterSearchDropdown('.choose.branch .dropdown');
|
||||
}
|
||||
|
||||
initRepoCloneLink();
|
||||
initCitationFileCopyContent();
|
||||
initRepoCommonLanguageStats();
|
||||
initRepoSettingBranches();
|
||||
|
||||
// Issues
|
||||
if ($('.repository.view.issue').length > 0) {
|
||||
initRepoIssueCommentEdit();
|
||||
|
||||
initRepoIssueBranchSelect();
|
||||
initRepoIssueTitleEdit();
|
||||
initRepoIssueWipToggle();
|
||||
initRepoIssueComments();
|
||||
|
||||
initRepoDiffConversationNav();
|
||||
initRepoIssueReferenceIssue();
|
||||
|
||||
|
||||
initRepoIssueCommentDelete();
|
||||
initRepoIssueDependencyDelete();
|
||||
initRepoIssueCodeCommentCancel();
|
||||
initRepoPullRequestUpdate();
|
||||
initCompReactionSelector($(document));
|
||||
|
||||
initRepoPullRequestMergeForm();
|
||||
}
|
||||
|
||||
// Pull request
|
||||
const $repoComparePull = $('.repository.compare.pull');
|
||||
if ($repoComparePull.length > 0) {
|
||||
// show pull request form
|
||||
$repoComparePull.find('button.show-form').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
hideElem($(this).parent());
|
||||
|
||||
const $form = $repoComparePull.find('.pullrequest-form');
|
||||
showElem($form);
|
||||
});
|
||||
}
|
||||
|
||||
initUnicodeEscapeButton();
|
||||
}
|
||||
|
||||
function initRepoIssueCommentEdit() {
|
||||
// Edit issue or comment content
|
||||
$(document).on('click', '.edit-content', onEditContent);
|
||||
|
||||
// Quote reply
|
||||
$(document).on('click', '.quote-reply', async function (event) {
|
||||
event.preventDefault();
|
||||
const target = $(this).data('target');
|
||||
const quote = $(`#${target}`).text().replace(/\n/g, '\n> ');
|
||||
const content = `> ${quote}\n\n`;
|
||||
let editor;
|
||||
if ($(this).hasClass('quote-reply-diff')) {
|
||||
const $replyBtn = $(this).closest('.comment-code-cloud').find('button.comment-form-reply');
|
||||
editor = await handleReply($replyBtn);
|
||||
} else {
|
||||
// for normal issue/comment page
|
||||
editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor'));
|
||||
}
|
||||
if (editor) {
|
||||
if (editor.value()) {
|
||||
editor.value(`${editor.value()}\n\n${content}`);
|
||||
} else {
|
||||
editor.value(content);
|
||||
}
|
||||
editor.focus();
|
||||
editor.moveCursorToEnd();
|
||||
}
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user