diff --git a/models/issues/issue.go b/models/issues/issue.go index a86d50ca9d..4044677f7f 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -74,17 +74,17 @@ type Issue struct { PosterID int64 `xorm:"INDEX"` Poster *user_model.User `xorm:"-"` OriginalAuthor string - OriginalAuthorID int64 `xorm:"index"` - Title string `xorm:"name"` - Content string `xorm:"LONGTEXT"` - RenderedContent template.HTML `xorm:"-"` - ContentVersion int `xorm:"NOT NULL DEFAULT 0"` - Labels []*Label `xorm:"-"` - isLabelsLoaded bool `xorm:"-"` - MilestoneID int64 `xorm:"INDEX"` - Milestone *Milestone `xorm:"-"` - isMilestoneLoaded bool `xorm:"-"` - Project *project_model.Project `xorm:"-"` + OriginalAuthorID int64 `xorm:"index"` + Title string `xorm:"name"` + Content string `xorm:"LONGTEXT"` + RenderedContent template.HTML `xorm:"-"` + ContentVersion int `xorm:"NOT NULL DEFAULT 0"` + Labels []*Label `xorm:"-"` + isLabelsLoaded bool `xorm:"-"` + MilestoneID int64 `xorm:"INDEX"` + Milestone *Milestone `xorm:"-"` + isMilestoneLoaded bool `xorm:"-"` + Projects []*project_model.Project `xorm:"-"` Priority int AssigneeID int64 `xorm:"-"` Assignee *user_model.User `xorm:"-"` diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go index 6c74b533b3..1faf9dacaf 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -219,14 +219,19 @@ func (issues IssueList) LoadProjects(ctx context.Context) error { return err } for _, project := range projects { - projectMaps[project.IssueID] = project.Project + projectMaps[project.ID] = project.Project } left -= limit issueIDs = issueIDs[limit:] } for _, issue := range issues { - issue.Project = projectMaps[issue.ID] + projectIDs := issue.projectIDs(ctx) + for _, i := range projectIDs { + if projectMaps[i] != nil { + issue.Projects = append(issue.Projects, projectMaps[i]) + } + } } return nil } diff --git a/models/issues/issue_list_test.go b/models/issues/issue_list_test.go index 5b4d2ca5ab..de97ff9ae7 100644 --- a/models/issues/issue_list_test.go +++ b/models/issues/issue_list_test.go @@ -66,10 +66,10 @@ func TestIssueList_LoadAttributes(t *testing.T) { } if issue.ID == int64(1) { assert.Equal(t, int64(400), issue.TotalTrackedTime) - assert.NotNil(t, issue.Project) - assert.Equal(t, int64(1), issue.Project.ID) + assert.NotNil(t, issue.Projects[0]) + assert.Equal(t, int64(1), issue.Projects[0].ID) } else { - assert.Nil(t, issue.Project) + assert.Nil(t, issue.Projects[0]) } } } diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index 0185244783..ced2f9c087 100644 --- a/models/issues/issue_project.go +++ b/models/issues/issue_project.go @@ -14,27 +14,21 @@ import ( // LoadProject load the project the issue was assigned to func (issue *Issue) LoadProject(ctx context.Context) (err error) { - if issue.Project == nil { - var p project_model.Project - has, err := db.GetEngine(ctx).Table("project"). + if len(issue.Projects) == 0 { + err = db.GetEngine(ctx).Table("project"). Join("INNER", "project_issue", "project.id=project_issue.project_id"). - Where("project_issue.issue_id = ?", issue.ID).Get(&p) - if err != nil { - return err - } else if has { - issue.Project = &p - } + Where("project_issue.issue_id = ?", issue.ID).Find(&issue.Projects) } return err } -func (issue *Issue) projectID(ctx context.Context) int64 { - var ip project_model.ProjectIssue - has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip) - if err != nil || !has { - return 0 +func (issue *Issue) projectIDs(ctx context.Context) []int64 { + var ids []int64 + if err := db.GetEngine(ctx).Table("project_issue").Where("issue_id=?", issue.ID).Cols("project_id").Find(&ids); err != nil { + return nil } - return ip.ProjectID + + return ids } // ProjectColumnID return project column id if issue was assigned to one @@ -96,17 +90,52 @@ func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *Is // IssueAssignOrRemoveProject changes the project associated with an issue // If newProjectID is 0, the issue is removed from the project -func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID, newColumnID int64) error { +func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectIDs []int64, newColumnID int64) error { return db.WithTx(ctx, func(ctx context.Context) error { - oldProjectID := issue.projectID(ctx) + oldProjectIDs := issue.projectIDs(ctx) if err := issue.LoadRepo(ctx); err != nil { return err } - // Only check if we add a new project and not remove it. - if newProjectID > 0 { - newProject, err := project_model.GetProjectByID(ctx, newProjectID) + projectDB := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID) + newProjectIDs, oldProjectIDs := util.DiffSlice(oldProjectIDs, newProjectIDs) + + if len(oldProjectIDs) > 0 { + if _, err := projectDB.Where("issue_id=?", issue.ID).In("project_id", oldProjectIDs).Delete(&project_model.ProjectIssue{}); err != nil { + return err + } + for _, pID := range oldProjectIDs { + if _, err := CreateComment(ctx, &CreateCommentOptions{ + Type: CommentTypeProject, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + OldProjectID: pID, + ProjectID: 0, + }); err != nil { + return err + } + } + return nil + } + + res := struct { + MaxSorting int64 + IssueCount int64 + }{} + if _, err := projectDB.Select("max(sorting) as max_sorting, count(*) as issue_count").Table("project_issue"). + In("project_id", newProjectIDs). + And("project_board_id=?", newColumnID). + Get(&res); err != nil { + return err + } + newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0) + + pi := make([]*project_model.ProjectIssue, 0, len(newProjectIDs)) + + for _, pID := range newProjectIDs { + newProject, err := project_model.GetProjectByID(ctx, pID) if err != nil { return err } @@ -119,48 +148,34 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo return err } newColumnID = newDefaultColumn.ID + if newColumnID == 0 { + panic("newColumnID must not be zero") // shouldn't happen + } } - } - if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil { - return err - } + pi = append(pi, &project_model.ProjectIssue{ + IssueID: issue.ID, + ProjectID: pID, + ProjectColumnID: newColumnID, + Sorting: newSorting, + }) - if oldProjectID > 0 || newProjectID > 0 { if _, err := CreateComment(ctx, &CreateCommentOptions{ Type: CommentTypeProject, Doer: doer, Repo: issue.Repo, Issue: issue, - OldProjectID: oldProjectID, - ProjectID: newProjectID, + OldProjectID: 0, + ProjectID: pID, }); err != nil { return err } } - if newProjectID == 0 { - return nil - } - if newColumnID == 0 { - panic("newColumnID must not be zero") // shouldn't happen + + if len(pi) > 0 { + return db.Insert(ctx, pi) } - res := struct { - MaxSorting int64 - IssueCount int64 - }{} - if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as issue_count").Table("project_issue"). - Where("project_id=?", newProjectID). - And("project_board_id=?", newColumnID). - Get(&res); err != nil { - return err - } - newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0) - return db.Insert(ctx, &project_model.ProjectIssue{ - IssueID: issue.ID, - ProjectID: newProjectID, - ProjectColumnID: newColumnID, - Sorting: newSorting, - }) + return nil }) } diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go index 18571e3aaa..8b20e6d69f 100644 --- a/models/issues/issue_test.go +++ b/models/issues/issue_test.go @@ -417,10 +417,10 @@ func TestIssueLoadAttributes(t *testing.T) { } if issue.ID == int64(1) { assert.Equal(t, int64(400), issue.TotalTrackedTime) - assert.NotNil(t, issue.Project) - assert.Equal(t, int64(1), issue.Project.ID) + assert.NotNil(t, issue.Projects[0]) + assert.Equal(t, int64(1), issue.Projects[0].ID) } else { - assert.Nil(t, issue.Project) + assert.Nil(t, issue.Projects[0]) } } } diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go index 0d4f0f727d..e0ef52a437 100644 --- a/modules/indexer/issues/internal/model.go +++ b/modules/indexer/issues/internal/model.go @@ -30,7 +30,7 @@ type IndexerData struct { LabelIDs []int64 `json:"label_ids"` NoLabel bool `json:"no_label"` // True if LabelIDs is empty MilestoneID int64 `json:"milestone_id"` - ProjectID int64 `json:"project_id"` + ProjectIDs []int64 `json:"project_id"` ProjectColumnID int64 `json:"project_board_id"` // the key should be kept as project_board_id to keep compatible PosterID int64 `json:"poster_id"` AssigneeID int64 `json:"assignee_id"` diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go index a42ec9a2bc..dc082eacd4 100644 --- a/modules/indexer/issues/internal/tests/tests.go +++ b/modules/indexer/issues/internal/tests/tests.go @@ -302,7 +302,7 @@ var cases = []*testIndexerCase{ }, }, { - Name: "ProjectID", + Name: "ProjectIDs", SearchOptions: &internal.SearchOptions{ Paginator: &db.ListOptions{ PageSize: 5, @@ -312,10 +312,10 @@ var cases = []*testIndexerCase{ Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Len(t, result.Hits, 5) for _, v := range result.Hits { - assert.Equal(t, int64(1), data[v.ID].ProjectID) + assert.Equal(t, int64(1), data[v.ID].ProjectIDs[0]) } assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return v.ProjectID == 1 + return v.ProjectIDs[0] == 1 }), result.Total) }, }, @@ -330,10 +330,10 @@ var cases = []*testIndexerCase{ Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Len(t, result.Hits, 5) for _, v := range result.Hits { - assert.Equal(t, int64(0), data[v.ID].ProjectID) + assert.Equal(t, int64(0), data[v.ID].ProjectIDs[0]) } assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return v.ProjectID == 0 + return v.ProjectIDs[0] == 0 }), result.Total) }, }, @@ -691,6 +691,10 @@ func generateDefaultIndexerData() []*internal.IndexerData { for i := range labelIDs { labelIDs[i] = int64(i) + 1 // LabelID should not be 0 } + projectIDs := make([]int64, id%5) + for i := range projectIDs { + projectIDs[i] = int64(i) + 1 // projectIDs should not be 0 + } mentionIDs := make([]int64, id%6) for i := range mentionIDs { mentionIDs[i] = int64(i) + 1 // MentionID should not be 0 @@ -720,7 +724,7 @@ func generateDefaultIndexerData() []*internal.IndexerData { LabelIDs: labelIDs, NoLabel: len(labelIDs) == 0, MilestoneID: issueIndex % 4, - ProjectID: issueIndex % 5, + ProjectIDs: projectIDs, ProjectColumnID: issueIndex % 6, PosterID: id%10 + 1, // PosterID should not be 0 AssigneeID: issueIndex % 10, diff --git a/modules/indexer/issues/util.go b/modules/indexer/issues/util.go index 19d835a1d8..e8603c9542 100644 --- a/modules/indexer/issues/util.go +++ b/modules/indexer/issues/util.go @@ -87,9 +87,9 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD return nil, false, err } - var projectID int64 - if issue.Project != nil { - projectID = issue.Project.ID + projectIDs := make([]int64, 0, len(issue.Projects)) + for _, project := range issue.Projects { + projectIDs = append(projectIDs, project.ID) } projectColumnID, err := issue.ProjectColumnID(ctx) @@ -110,7 +110,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD LabelIDs: labels, NoLabel: len(labels) == 0, MilestoneID: issue.MilestoneID, - ProjectID: projectID, + ProjectIDs: projectIDs, ProjectColumnID: projectColumnID, PosterID: issue.PosterID, AssigneeID: issue.AssigneeID, diff --git a/modules/util/util.go b/modules/util/util.go index dd8e073888..714c6bda7d 100644 --- a/modules/util/util.go +++ b/modules/util/util.go @@ -253,3 +253,27 @@ func ReserveLineBreakForTextarea(input string) string { // Other than this, we should respect the original content, even leading or trailing spaces. return strings.ReplaceAll(input, "\r\n", "\n") } + +func DiffSlice[T comparable](oldSlice, newSlice []T) (added, removed []T) { + oldSet := make(map[T]struct{}, len(oldSlice)) + newSet := make(map[T]struct{}, len(newSlice)) + + for _, v := range oldSlice { + oldSet[v] = struct{}{} + } + for _, v := range newSlice { + newSet[v] = struct{}{} + } + + for v := range newSet { + if _, found := oldSet[v]; !found { + added = append(added, v) + } + } + for v := range oldSet { + if _, found := newSet[v]; !found { + removed = append(removed, v) + } + } + return added, removed +} diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index e678db5262..25219fa702 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -721,7 +721,7 @@ 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 err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, nil); err != nil { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { ctx.APIError(http.StatusBadRequest, err) } else if errors.Is(err, user_model.ErrBlockedUser) { diff --git a/routers/web/repo/issue_new.go b/routers/web/repo/issue_new.go index d8863961ff..0ca4bbd1c2 100644 --- a/routers/web/repo/issue_new.go +++ b/routers/web/repo/issue_new.go @@ -120,8 +120,8 @@ func NewIssue(ctx *context.Context) { } pageMetaData.MilestonesData.SelectedMilestoneID = ctx.FormInt64("milestone") - pageMetaData.ProjectsData.SelectedProjectID = ctx.FormInt64("project") - if pageMetaData.ProjectsData.SelectedProjectID > 0 { + pageMetaData.ProjectsData.SelectedProjectID = ctx.FormString("project") + if len(pageMetaData.ProjectsData.SelectedProjectID) > 0 { if len(ctx.Req.URL.Query().Get("project")) > 0 { ctx.Data["redirect_after_creation"] = "project" } @@ -240,8 +240,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 @@ -270,11 +271,14 @@ func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueFo 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 + inputProjectIDs, _ := base.StringsToInt64s(strings.Split(form.ProjectIDs, ",")) + var projectIDStrings []string + for _, inputProjectID := range inputProjectIDs { + if candidateProjects.Contains(inputProjectID) { + projectIDStrings = append(projectIDStrings, strconv.FormatInt(inputProjectID, 10)) + } } - pageMetaData.ProjectsData.SelectedProjectID = form.ProjectID + pageMetaData.ProjectsData.SelectedProjectID = strings.Join(projectIDStrings, ",") // prepare assignees candidateAssignees := toSet(pageMetaData.AssigneesData.CandidateAssignees, func(user *user_model.User) int64 { return user.ID }) @@ -319,7 +323,7 @@ func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueFo } } - ret.LabelIDs, ret.AssigneeIDs, ret.MilestoneID, ret.ProjectID = inputLabelIDs, inputAssigneeIDs, form.MilestoneID, form.ProjectID + ret.LabelIDs, ret.AssigneeIDs, ret.MilestoneID, ret.ProjectIDs = inputLabelIDs, inputAssigneeIDs, form.MilestoneID, inputProjectIDs ret.Reviewers, ret.TeamReviewers = reviewers, teamReviewers return ret } @@ -344,9 +348,9 @@ func NewIssuePost(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 - if projectID > 0 { + if len(projectIDs) > 0 { if !ctx.Repo.CanRead(unit.TypeProjects) { // User must also be able to see the project. ctx.HTTPError(http.StatusBadRequest, "user hasn't permissions to read projects") @@ -386,7 +390,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) { @@ -398,15 +402,17 @@ 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 err == nil { - if project.Type == project_model.TypeOrganization { - ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.Repo.Owner, project.ID)) - } else { - ctx.JSONRedirect(project_model.ProjectLinkForRepo(repo, project.ID)) + if ctx.FormString("redirect_after_creation") == "project" && len(projectIDs) > 0 { + for _, projectID := range projectIDs { + project, err := project_model.GetProjectByID(ctx, projectID) + if err == nil { + if project.Type == project_model.TypeOrganization { + ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.Repo.Owner, project.ID)) + } else { + ctx.JSONRedirect(project_model.ProjectLinkForRepo(repo, project.ID)) + } + return } - return } } ctx.JSONRedirect(issue.Link()) diff --git a/routers/web/repo/issue_page_meta.go b/routers/web/repo/issue_page_meta.go index 93cc38bffa..0eb14cf957 100644 --- a/routers/web/repo/issue_page_meta.go +++ b/routers/web/repo/issue_page_meta.go @@ -34,7 +34,7 @@ type issueSidebarAssigneesData struct { } type issueSidebarProjectsData struct { - SelectedProjectID int64 + SelectedProjectID string OpenProjects []*project_model.Project ClosedProjects []*project_model.Project } @@ -160,8 +160,12 @@ func (d *IssuePageMetaData) retrieveAssigneesData(ctx *context.Context) { } func (d *IssuePageMetaData) retrieveProjectsDataForIssueWriter(ctx *context.Context) { - if d.Issue != nil && d.Issue.Project != nil { - d.ProjectsData.SelectedProjectID = d.Issue.Project.ID + if d.Issue != nil && len(d.Issue.Projects) > 0 { + ids := make([]string, 0, len(d.Issue.Projects)) + for _, a := range d.Issue.Projects { + ids = append(ids, strconv.FormatInt(a.ID, 10)) + } + d.ProjectsData.SelectedProjectID = strings.Join(ids, ",") } d.ProjectsData.OpenProjects, d.ProjectsData.ClosedProjects = retrieveProjectsInternal(ctx, ctx.Repo.Repository) } diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 0bf1f64d09..2c46440641 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/models/renderhelper" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/optional" @@ -444,12 +445,9 @@ func UpdateIssueProject(ctx *context.Context) { return } - projectID := ctx.FormInt64("id") + projectIDs, _ := base.StringsToInt64s(strings.Split(ctx.FormString("id"), ",")) 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, 0); err != nil { if errors.Is(err, util.ErrPermissionDenied) { continue } diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 43ddc265cf..656255a649 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1305,7 +1305,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 if setting.Attachment.Enabled { attachments = form.Files @@ -1411,8 +1411,8 @@ func CompareAndPullRequestPost(ctx *context.Context) { return } - if projectID > 0 && ctx.Repo.CanWrite(unit.TypeProjects) { - if err := issues_model.IssueAssignOrRemoveProject(ctx, pullIssue, ctx.Doer, projectID, 0); err != nil { + if ctx.Repo.CanWrite(unit.TypeProjects) { + if err := issues_model.IssueAssignOrRemoveProject(ctx, pullIssue, ctx.Doer, projectIDs, 0); err != nil { if !errors.Is(err, util.ErrPermissionDenied) { ctx.ServerError("IssueAssignOrRemoveProject", err) return diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index a2827e516a..9277472a52 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -425,7 +425,7 @@ type CreateIssueForm struct { ReviewerIDs string `form:"reviewer_ids"` Ref string `form:"ref"` MilestoneID int64 - ProjectID int64 + ProjectIDs string `form:"project_ids"` Content string Files []string AllowMaintainerEdit bool diff --git a/services/issue/issue.go b/services/issue/issue.go index 455a1ec297..54dfa67cac 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -23,7 +23,7 @@ import ( ) // NewIssue creates new issue with labels for repository. -func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64, projectID int64) error { +func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs, projectIDs []int64) error { if err := issue.LoadPoster(ctx); err != nil { return err } @@ -41,12 +41,7 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_mo return err } } - if projectID > 0 { - if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, issue.Poster, projectID, 0); err != nil { - return err - } - } - return nil + return issues_model.IssueAssignOrRemoveProject(ctx, issue, issue.Poster, projectIDs, 0) }); err != nil { return err } diff --git a/services/projects/issue_test.go b/services/projects/issue_test.go index e76d31e757..b05bb99210 100644 --- a/services/projects/issue_test.go +++ b/services/projects/issue_test.go @@ -117,12 +117,12 @@ func Test_Projects(t *testing.T) { // issue 6 belongs to private repo 3 under org 3 issue6 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 6}) - err = issues_model.IssueAssignOrRemoveProject(db.DefaultContext, issue6, user2, project1.ID, column1.ID) + err = issues_model.IssueAssignOrRemoveProject(db.DefaultContext, issue6, user2, []int64{project1.ID}, column1.ID) assert.NoError(t, err) // issue 16 belongs to public repo 16 under org 3 issue16 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 16}) - err = issues_model.IssueAssignOrRemoveProject(db.DefaultContext, issue16, user2, project1.ID, column1.ID) + err = issues_model.IssueAssignOrRemoveProject(db.DefaultContext, issue16, user2, []int64{project1.ID}, column1.ID) assert.NoError(t, err) projects, err := db.Find[project_model.Project](db.DefaultContext, project_model.SearchOptions{ diff --git a/templates/repo/issue/sidebar/project_list.tmpl b/templates/repo/issue/sidebar/project_list.tmpl index a212261a22..b260c7ec66 100644 --- a/templates/repo/issue/sidebar/project_list.tmpl +++ b/templates/repo/issue/sidebar/project_list.tmpl @@ -1,11 +1,11 @@ {{$pageMeta := .}} {{$data := .ProjectsData}} -{{$issueProject := NIL}}{{if and $pageMeta.Issue $pageMeta.Issue.Project}}{{$issueProject = $pageMeta.Issue.Project}}{{end}} +{{$issueProject := NIL}}{{if and $pageMeta.Issue $pageMeta.Issue.Projects}}{{$issueProject = $pageMeta.Issue.Projects}}{{end}}
-
- + diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl index 30670c3b0f..2c392877c1 100644 --- a/templates/shared/issuelist.tmpl +++ b/templates/shared/issuelist.tmpl @@ -73,10 +73,10 @@ {{.Milestone.Name}} {{end}} - {{if .Project}} - - {{svg .Project.IconName 14}} - {{.Project.Title}} + {{range .Projects}} + + {{svg .IconName 14}} + {{.Title}} {{end}} {{if .Ref}}{{/* TODO: RemoveIssueRef: see "repo/issue/branch_selector_field.tmpl" */}}