From d37f246d09ca0ea94cceed25befd2c366b5eb2b5 Mon Sep 17 00:00:00 2001 From: splitt3r Date: Sat, 19 Aug 2023 10:25:06 +0000 Subject: [PATCH] Add reviewers selection to new pull request form --- routers/api/v1/repo/pull.go | 2 +- routers/web/repo/compare.go | 43 ++ routers/web/repo/issue.go | 53 +- routers/web/repo/pull.go | 4 +- services/agit/agit.go | 2 +- services/forms/repo_form.go | 1 + services/pull/pull.go | 25 +- templates/repo/issue/new_form.tmpl | 52 ++ tests/integration/actions_trigger_test.go | 4 +- tests/integration/pull_merge_test.go | 2 +- tests/integration/pull_update_test.go | 2 +- web_src/js/features/repo-legacy.js | 600 ++++++++++++++++++++++ 12 files changed, 768 insertions(+), 22 deletions(-) create mode 100644 web_src/js/features/repo-legacy.js diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 34ebcb42d5..0f98507a7e 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -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) { diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index e6a04782e5..70d7d37d65 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -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) diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 507b5af9d9..136dd66e5a 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -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 } diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 0efe1be76a..05b47faeff 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -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()) diff --git a/services/agit/agit.go b/services/agit/agit.go index 82aa2791aa..b4ebb2eb18 100644 --- a/services/agit/agit.go +++ b/services/agit/agit.go @@ -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 } diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index ddd07a64cb..83f2dd6caa 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -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 diff --git a/services/pull/pull.go b/services/pull/pull.go index bab4e49998..68e81814de 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -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 diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl index e56d1b9ecc..db7a44fec3 100644 --- a/templates/repo/issue/new_form.tmpl +++ b/templates/repo/issue/new_form.tmpl @@ -48,6 +48,58 @@
{{template "repo/issue/branch_selector_field" .}} + {{if .PageIsComparePull}} + + +
+ + {{.locale.Tr "repo.issues.new.no_reviewers"}} + + +
+
+ {{end}} {{template "repo/issue/labels/labels_selector_field" .}} diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index c254c90958..4a542884fc 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -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 diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index 43210e852e..729d22facf 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -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!"}) diff --git a/tests/integration/pull_update_test.go b/tests/integration/pull_update_test.go index 5ae241f3af..c283488563 100644 --- a/tests/integration/pull_update_test.go +++ b/tests/integration/pull_update_test.go @@ -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-"}) diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js new file mode 100644 index 0000000000..9b1307d7bc --- /dev/null +++ b/web_src/js/features/repo-legacy.js @@ -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 = `avatar`; + } else if (input_id === '#reviewer_id') { + icon = `avatar`; + } + + $list.find('.selected').html(` + + ${icon} + ${htmlEscape($(this).text())} + + `); + + $(`.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 = $(``).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 = $(``).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(`
`); + $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(); + } + }); +}