Allow multiple projects per issue and pull requests (#36784)
Add ability to add and remove multiple projects per issue and pull request. Resolve #12974 --------- Signed-off-by: Icy Avocado <avocado@ovacoda.com> Co-authored-by: Tyrone Yeh <siryeh@gmail.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: OpenCode (gpt-5.2-codex) <opencode@openai.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
This commit is contained in:
@@ -690,11 +690,11 @@ func CreateIssue(ctx *context.APIContext) {
|
||||
form.Labels = make([]int64, 0)
|
||||
}
|
||||
|
||||
if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, 0); err != nil {
|
||||
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, form.Projects); err != nil {
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.APIError(http.StatusForbidden, err)
|
||||
} else if errors.Is(err, util.ErrPermissionDenied) || errors.Is(err, util.ErrNotExist) {
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
@@ -913,6 +913,18 @@ func EditIssue(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
|
||||
// Update projects if provided
|
||||
if canWrite && form.Projects != nil {
|
||||
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, *form.Projects); err != nil {
|
||||
if errors.Is(err, util.ErrPermissionDenied) || errors.Is(err, util.ErrNotExist) {
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Refetch from database to assign some automatic values
|
||||
issue, err = issues_model.GetIssueByID(ctx, issue.ID)
|
||||
if err != nil {
|
||||
|
||||
@@ -38,6 +38,14 @@ func retrieveProjectsForIssueList(ctx *context.Context, repo *repo_model.Reposit
|
||||
ctx.Data["OpenProjects"], ctx.Data["ClosedProjects"] = retrieveProjectsInternal(ctx, repo)
|
||||
}
|
||||
|
||||
// parseProjectIDsFromQuery parses the comma-separated `project` (preferred) or `projects`
|
||||
// query parameter into a slice of int64 IDs.
|
||||
func parseProjectIDsFromQuery(ctx *context.Context) []int64 {
|
||||
// FIXME: ISSUE-MULTIPLE-PROJECTS-FILTER: no multiple project filter support yet
|
||||
// Although here parses the project parameter as a slice, the "search" logic is wrong
|
||||
return ctx.FormStringInt64s("project")
|
||||
}
|
||||
|
||||
// SearchIssues searches for issues across the repositories that the user has access to
|
||||
func SearchIssues(ctx *context.Context) {
|
||||
before, since, err := context.GetQueryBeforeSince(ctx.Base)
|
||||
@@ -156,10 +164,7 @@ func SearchIssues(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
projectID := optional.None[int64]()
|
||||
if v := ctx.FormInt64("project"); v > 0 {
|
||||
projectID = optional.Some(v)
|
||||
}
|
||||
includedProjectIDs := parseProjectIDsFromQuery(ctx)
|
||||
|
||||
// this api is also used in UI,
|
||||
// so the default limit is set to fit UI needs
|
||||
@@ -182,7 +187,7 @@ func SearchIssues(ctx *context.Context) {
|
||||
IsClosed: isClosed,
|
||||
IncludedAnyLabelIDs: includedAnyLabels,
|
||||
MilestoneIDs: includedMilestones,
|
||||
ProjectID: projectID,
|
||||
ProjectIDs: includedProjectIDs,
|
||||
SortBy: issue_indexer.SortByCreatedDesc,
|
||||
}
|
||||
|
||||
@@ -298,11 +303,6 @@ func SearchRepoIssuesJSON(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
projectID := optional.None[int64]()
|
||||
if v := ctx.FormInt64("project"); v > 0 {
|
||||
projectID = optional.Some(v)
|
||||
}
|
||||
|
||||
isPull := optional.None[bool]()
|
||||
switch ctx.FormString("type") {
|
||||
case "pulls":
|
||||
@@ -330,13 +330,20 @@ func SearchRepoIssuesJSON(ctx *context.Context) {
|
||||
Page: ctx.FormInt("page"),
|
||||
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
|
||||
},
|
||||
Keyword: keyword,
|
||||
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
||||
IsPull: isPull,
|
||||
IsClosed: isClosed,
|
||||
ProjectID: projectID,
|
||||
SortBy: issue_indexer.SortByCreatedDesc,
|
||||
Keyword: keyword,
|
||||
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
||||
IsPull: isPull,
|
||||
IsClosed: isClosed,
|
||||
SortBy: issue_indexer.SortByCreatedDesc,
|
||||
}
|
||||
|
||||
projectIDs := parseProjectIDsFromQuery(ctx)
|
||||
if len(projectIDs) == 1 && projectIDs[0] == -1 {
|
||||
searchOpt.NoProjectOnly = true
|
||||
} else if len(projectIDs) > 0 {
|
||||
searchOpt.ProjectIDs = projectIDs
|
||||
}
|
||||
|
||||
if since != 0 {
|
||||
searchOpt.UpdatedAfterUnix = optional.Some(since)
|
||||
}
|
||||
@@ -467,7 +474,7 @@ func renderMilestones(ctx *context.Context) {
|
||||
ctx.Data["ClosedMilestones"] = closedMilestones
|
||||
}
|
||||
|
||||
func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int64, isPullOption optional.Option[bool]) {
|
||||
func prepareIssueFilterAndList(ctx *context.Context, milestoneID int64, projectIDs []int64, isPullOption optional.Option[bool]) {
|
||||
var err error
|
||||
viewType := ctx.FormString("type")
|
||||
sortType := ctx.FormString("sort")
|
||||
@@ -520,7 +527,7 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6
|
||||
RepoIDs: []int64{repo.ID},
|
||||
LabelIDs: preparedLabelFilter.SelectedLabelIDs,
|
||||
MilestoneIDs: mileIDs,
|
||||
ProjectID: projectID,
|
||||
ProjectIDs: projectIDs,
|
||||
AssigneeID: assigneeID,
|
||||
MentionedID: mentionedID,
|
||||
PosterID: posterUserID,
|
||||
@@ -529,6 +536,7 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6
|
||||
IsPull: isPullOption,
|
||||
IssueIDs: nil,
|
||||
}
|
||||
|
||||
if keyword != "" {
|
||||
keywordMatchedIssueIDs, _, err = issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, statsOpts))
|
||||
if err != nil {
|
||||
@@ -600,7 +608,7 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6
|
||||
ReviewRequestedID: reviewRequestedID,
|
||||
ReviewedID: reviewedID,
|
||||
MilestoneIDs: mileIDs,
|
||||
ProjectID: projectID,
|
||||
ProjectIDs: projectIDs,
|
||||
IsClosed: isShowClosed,
|
||||
IsPull: isPullOption,
|
||||
LabelIDs: preparedLabelFilter.SelectedLabelIDs,
|
||||
@@ -708,7 +716,7 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6
|
||||
ctx.Data["ViewType"] = viewType
|
||||
ctx.Data["SortType"] = sortType
|
||||
ctx.Data["MilestoneID"] = milestoneID
|
||||
ctx.Data["ProjectID"] = projectID
|
||||
ctx.Data["ProjectIDs"] = projectIDs
|
||||
ctx.Data["AssigneeID"] = assigneeID
|
||||
ctx.Data["PosterUsername"] = posterUsername
|
||||
ctx.Data["Keyword"] = keyword
|
||||
@@ -749,7 +757,9 @@ func Issues(ctx *context.Context) {
|
||||
ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
|
||||
}
|
||||
|
||||
prepareIssueFilterAndList(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), optional.Some(isPullList))
|
||||
projectIDs := parseProjectIDsFromQuery(ctx)
|
||||
|
||||
prepareIssueFilterAndList(ctx, ctx.FormInt64("milestone"), projectIDs, optional.Some(isPullList))
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -121,7 +121,8 @@ func NewIssue(ctx *context.Context) {
|
||||
}
|
||||
|
||||
pageMetaData.MilestonesData.SelectedMilestoneID = ctx.FormInt64("milestone")
|
||||
pageMetaData.ProjectsData.SelectedProjectIDs, _ = base.StringsToInt64s(strings.Split(ctx.FormString("project"), ","))
|
||||
|
||||
pageMetaData.SetSelectedProjectIDs(parseProjectIDsFromQuery(ctx))
|
||||
if len(pageMetaData.ProjectsData.SelectedProjectIDs) == 1 {
|
||||
ctx.Data["redirect_after_creation"] = "project"
|
||||
}
|
||||
@@ -237,8 +238,9 @@ func toSet[ItemType any, KeyType comparable](slice []ItemType, keyFunc func(Item
|
||||
|
||||
// ValidateRepoMetasForNewIssue check and returns repository's meta information
|
||||
func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueForm, isPull bool) (ret struct {
|
||||
LabelIDs, AssigneeIDs []int64
|
||||
MilestoneID, ProjectID int64
|
||||
LabelIDs, AssigneeIDs []int64
|
||||
MilestoneID int64
|
||||
ProjectIDs []int64
|
||||
|
||||
Reviewers []*user_model.User
|
||||
TeamReviewers []*organization.Team
|
||||
@@ -249,7 +251,7 @@ func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueFo
|
||||
return ret
|
||||
}
|
||||
|
||||
inputLabelIDs, _ := base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
|
||||
inputLabelIDs := ctx.FormStringInt64s("label_ids")
|
||||
candidateLabels := toSet(pageMetaData.LabelsData.AllLabels, func(label *issues_model.Label) int64 { return label.ID })
|
||||
if len(inputLabelIDs) > 0 && !candidateLabels.Contains(inputLabelIDs...) {
|
||||
ctx.NotFound(nil)
|
||||
@@ -265,13 +267,8 @@ func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueFo
|
||||
}
|
||||
pageMetaData.MilestonesData.SelectedMilestoneID = form.MilestoneID
|
||||
|
||||
allProjects := append(slices.Clone(pageMetaData.ProjectsData.OpenProjects), pageMetaData.ProjectsData.ClosedProjects...)
|
||||
candidateProjects := toSet(allProjects, func(project *project_model.Project) int64 { return project.ID })
|
||||
if form.ProjectID > 0 && !candidateProjects.Contains(form.ProjectID) {
|
||||
ctx.NotFound(nil)
|
||||
return ret
|
||||
}
|
||||
pageMetaData.ProjectsData.SelectedProjectIDs = util.Iif(form.ProjectID > 0, []int64{form.ProjectID}, nil)
|
||||
inputProjectIDs := ctx.FormStringInt64s("project_ids")
|
||||
pageMetaData.SetSelectedProjectIDs(inputProjectIDs)
|
||||
|
||||
// prepare assignees
|
||||
candidateAssignees := toSet(pageMetaData.AssigneesData.CandidateAssignees, func(user *user_model.User) int64 { return user.ID })
|
||||
@@ -316,7 +313,8 @@ func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueFo
|
||||
}
|
||||
}
|
||||
|
||||
ret.LabelIDs, ret.AssigneeIDs, ret.MilestoneID, ret.ProjectID = inputLabelIDs, inputAssigneeIDs, form.MilestoneID, form.ProjectID
|
||||
// Return only the validated IDs.
|
||||
ret.LabelIDs, ret.AssigneeIDs, ret.MilestoneID, ret.ProjectIDs = inputLabelIDs, inputAssigneeIDs, form.MilestoneID, inputProjectIDs
|
||||
ret.Reviewers, ret.TeamReviewers = reviewers, teamReviewers
|
||||
return ret
|
||||
}
|
||||
@@ -324,26 +322,17 @@ func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueFo
|
||||
// NewIssuePost response for creating new issue
|
||||
func NewIssuePost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.CreateIssueForm)
|
||||
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
|
||||
ctx.Data["PageIsIssueList"] = true
|
||||
ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
|
||||
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
|
||||
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
||||
upload.AddUploadContext(ctx, "comment")
|
||||
|
||||
var (
|
||||
repo = ctx.Repo.Repository
|
||||
attachments []string
|
||||
)
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
validateRet := ValidateRepoMetasForNewIssue(ctx, *form, false)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
labelIDs, assigneeIDs, milestoneID, projectID := validateRet.LabelIDs, validateRet.AssigneeIDs, validateRet.MilestoneID, validateRet.ProjectID
|
||||
labelIDs, assigneeIDs, milestoneID, projectIDs := validateRet.LabelIDs, validateRet.AssigneeIDs, validateRet.MilestoneID, validateRet.ProjectIDs
|
||||
|
||||
if projectID > 0 {
|
||||
if len(projectIDs) > 0 {
|
||||
if !ctx.Repo.Permission.CanRead(unit.TypeProjects) {
|
||||
// User must also be able to see the project.
|
||||
ctx.HTTPError(http.StatusBadRequest, "user hasn't permissions to read projects")
|
||||
@@ -351,6 +340,7 @@ func NewIssuePost(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
var attachments []string
|
||||
if setting.Attachment.Enabled {
|
||||
attachments = form.Files
|
||||
}
|
||||
@@ -383,7 +373,7 @@ func NewIssuePost(ctx *context.Context) {
|
||||
Ref: form.Ref,
|
||||
}
|
||||
|
||||
if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs, projectID); err != nil {
|
||||
if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs, projectIDs); err != nil {
|
||||
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||
ctx.HTTPError(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
|
||||
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
@@ -395,8 +385,9 @@ func NewIssuePost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
|
||||
if ctx.FormString("redirect_after_creation") == "project" && projectID > 0 {
|
||||
project, err := project_model.GetProjectByID(ctx, projectID)
|
||||
if ctx.FormString("redirect_after_creation") == "project" && len(projectIDs) > 0 {
|
||||
// When issue is in multiple projects, redirect to first project from form order.
|
||||
project, err := project_model.GetProjectByID(ctx, projectIDs[0])
|
||||
if err == nil {
|
||||
if project.Type == project_model.TypeOrganization {
|
||||
ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.Repo.Owner, project.ID))
|
||||
|
||||
@@ -40,7 +40,7 @@ type issueSidebarProjectCardData struct {
|
||||
}
|
||||
|
||||
type issueSidebarProjectsData struct {
|
||||
SelectedProjectIDs []int64 // TODO: support multiple projects in the future
|
||||
SelectedProjectIDs []int64
|
||||
ProjectCards []*issueSidebarProjectCardData
|
||||
|
||||
OpenProjects []*project_model.Project
|
||||
@@ -171,33 +171,49 @@ func (d *IssuePageMetaData) retrieveAssigneesData(ctx *context.Context) {
|
||||
ctx.Data["Assignees"] = d.AssigneesData.CandidateAssignees
|
||||
}
|
||||
|
||||
func (d *IssuePageMetaData) retrieveProjectData(ctx *context.Context) {
|
||||
if d.Issue == nil || d.Issue.Project == nil {
|
||||
func (d *IssuePageMetaData) retrieveProjectCardsForExistingIssue(ctx *context.Context) {
|
||||
if err := d.Issue.LoadProjects(ctx); err != nil {
|
||||
ctx.ServerError("LoadProjects", err)
|
||||
return
|
||||
}
|
||||
columns, err := d.Issue.Project.GetColumns(ctx)
|
||||
|
||||
// Load column mappings for all projects
|
||||
projectColumnMap, err := d.Issue.ProjectColumnMap(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjectColumns", err)
|
||||
ctx.ServerError("ProjectColumnMap", err)
|
||||
return
|
||||
}
|
||||
columnID, err := d.Issue.ProjectColumnID(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("ProjectColumnID", err)
|
||||
return
|
||||
}
|
||||
var selectedColumn *project_model.Column
|
||||
for _, col := range columns {
|
||||
if col.ID == columnID {
|
||||
selectedColumn = col
|
||||
break
|
||||
|
||||
// Build project cards for each project
|
||||
d.ProjectsData.ProjectCards = make([]*issueSidebarProjectCardData, 0, len(d.Issue.Projects))
|
||||
for _, project := range d.Issue.Projects {
|
||||
columns, err := project.GetColumns(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjectColumns", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
d.ProjectsData.ProjectCards = []*issueSidebarProjectCardData{
|
||||
{
|
||||
Project: d.Issue.Project,
|
||||
|
||||
var selectedColumn *project_model.Column
|
||||
columnID := projectColumnMap[project.ID]
|
||||
for _, col := range columns {
|
||||
if col.ID == columnID {
|
||||
selectedColumn = col
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if selectedColumn == nil {
|
||||
selectedColumn, err = project.MustDefaultColumn(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("MustDefaultColumn", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
d.ProjectsData.ProjectCards = append(d.ProjectsData.ProjectCards, &issueSidebarProjectCardData{
|
||||
Project: project,
|
||||
Columns: columns,
|
||||
SelectedColumn: selectedColumn,
|
||||
},
|
||||
})
|
||||
}
|
||||
d.ProjectsData.SelectedProjectIDs = make([]int64, 0, len(d.ProjectsData.ProjectCards))
|
||||
for _, card := range d.ProjectsData.ProjectCards {
|
||||
@@ -205,6 +221,29 @@ func (d *IssuePageMetaData) retrieveProjectData(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (d *IssuePageMetaData) retrieveProjectData(ctx *context.Context) {
|
||||
if d.Issue == nil {
|
||||
return
|
||||
}
|
||||
d.retrieveProjectCardsForExistingIssue(ctx)
|
||||
}
|
||||
|
||||
func (d *IssuePageMetaData) SetSelectedProjectIDs(ids []int64) {
|
||||
allProjects := map[int64]*project_model.Project{}
|
||||
for _, p := range d.ProjectsData.OpenProjects {
|
||||
allProjects[p.ID] = p
|
||||
}
|
||||
for _, p := range d.ProjectsData.ClosedProjects {
|
||||
allProjects[p.ID] = p
|
||||
}
|
||||
for _, id := range ids {
|
||||
if project, ok := allProjects[id]; ok {
|
||||
d.ProjectsData.ProjectCards = append(d.ProjectsData.ProjectCards, &issueSidebarProjectCardData{Project: project})
|
||||
}
|
||||
}
|
||||
d.ProjectsData.SelectedProjectIDs = ids
|
||||
}
|
||||
|
||||
func (d *IssuePageMetaData) retrieveProjectsDataForIssueWriter(ctx *context.Context) {
|
||||
d.ProjectsData.OpenProjects, d.ProjectsData.ClosedProjects = retrieveProjectsInternal(ctx, ctx.Repo.Repository)
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ func DeleteMilestone(ctx *context.Context) {
|
||||
// MilestoneIssuesAndPulls lists all the issues and pull requests of the milestone
|
||||
func MilestoneIssuesAndPulls(ctx *context.Context) {
|
||||
milestoneID := ctx.PathParamInt64("id")
|
||||
projectID := ctx.FormInt64("project")
|
||||
projectIDs := parseProjectIDsFromQuery(ctx)
|
||||
milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
|
||||
if err != nil {
|
||||
if issues_model.IsErrMilestoneNotExist(err) {
|
||||
@@ -260,7 +260,7 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
|
||||
ctx.Data["Title"] = milestone.Name
|
||||
ctx.Data["Milestone"] = milestone
|
||||
|
||||
prepareIssueFilterAndList(ctx, milestoneID, projectID, optional.None[bool]())
|
||||
prepareIssueFilterAndList(ctx, milestoneID, projectIDs, optional.None[bool]())
|
||||
|
||||
ret := issue.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
|
||||
ctx.Data["NewIssueChooseTemplate"] = len(ret.IssueTemplates) > 0
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -447,13 +448,12 @@ func UpdateIssueProject(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
projectID := ctx.FormInt64("id")
|
||||
projectIDs := ctx.FormStringInt64s("id")
|
||||
var failedIssues []int64
|
||||
for _, issue := range issues {
|
||||
if issue.Project != nil && issue.Project.ID == projectID {
|
||||
continue
|
||||
}
|
||||
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectID, 0); err != nil {
|
||||
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectIDs); err != nil {
|
||||
if errors.Is(err, util.ErrPermissionDenied) {
|
||||
failedIssues = append(failedIssues, issue.ID)
|
||||
continue
|
||||
}
|
||||
ctx.ServerError("IssueAssignOrRemoveProject", err)
|
||||
@@ -461,6 +461,10 @@ func UpdateIssueProject(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(failedIssues) > 0 {
|
||||
log.Warn("Failed to assign projects to %d issues due to permission denied: %v", len(failedIssues), failedIssues)
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
@@ -477,12 +481,12 @@ func UpdateIssueProjectColumn(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := issue.LoadProject(ctx); err != nil {
|
||||
ctx.ServerError("LoadProject", err)
|
||||
if err := issue.LoadProjects(ctx); err != nil {
|
||||
ctx.ServerError("LoadProjects", err)
|
||||
return
|
||||
}
|
||||
|
||||
issueProjects := []*project_model.Project{issue.Project} // TODO: this is for the multiple project support in the future
|
||||
issueProjects := issue.Projects
|
||||
|
||||
// it must make sure the requested column is in this issue's projects
|
||||
var columnProject *project_model.Project
|
||||
|
||||
@@ -1301,7 +1301,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
labelIDs, assigneeIDs, milestoneID, projectID := validateRet.LabelIDs, validateRet.AssigneeIDs, validateRet.MilestoneID, validateRet.ProjectID
|
||||
labelIDs, assigneeIDs, milestoneID, projectIDs := validateRet.LabelIDs, validateRet.AssigneeIDs, validateRet.MilestoneID, validateRet.ProjectIDs
|
||||
|
||||
var attachments []string
|
||||
if setting.Attachment.Enabled {
|
||||
@@ -1368,7 +1368,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
||||
AssigneeIDs: assigneeIDs,
|
||||
Reviewers: validateRet.Reviewers,
|
||||
TeamReviewers: validateRet.TeamReviewers,
|
||||
ProjectID: projectID,
|
||||
ProjectIDs: projectIDs,
|
||||
}
|
||||
if err := pull_service.NewPullRequest(ctx, prOpts); err != nil {
|
||||
switch {
|
||||
|
||||
Reference in New Issue
Block a user