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.review.reviewers"}}
+ {{svg "octicon-gear" 16 "gt-ml-2"}}
+
+
+
+
+
+ {{.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](${$(this).data('avatar')})
`;
+ } else if (input_id === '#reviewer_id') {
+ icon = `
![avatar](${$(this).data('avatar')})
`;
+ }
+
+ $list.find('.selected').html(`
+
+ `);
+
+ $(`.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();
+ }
+ });
+}