mirror of
https://github.com/go-gitea/gitea.git
synced 2025-07-17 21:52:53 +02:00
Merge 0b78ae4161257ce7ab352d70adc4b5ee04930e19 into 32152a0ac03e912567a9d9a243729a9ed6288255
This commit is contained in:
commit
b40431c1ba
@ -74,17 +74,17 @@ type Issue struct {
|
|||||||
PosterID int64 `xorm:"INDEX"`
|
PosterID int64 `xorm:"INDEX"`
|
||||||
Poster *user_model.User `xorm:"-"`
|
Poster *user_model.User `xorm:"-"`
|
||||||
OriginalAuthor string
|
OriginalAuthor string
|
||||||
OriginalAuthorID int64 `xorm:"index"`
|
OriginalAuthorID int64 `xorm:"index"`
|
||||||
Title string `xorm:"name"`
|
Title string `xorm:"name"`
|
||||||
Content string `xorm:"LONGTEXT"`
|
Content string `xorm:"LONGTEXT"`
|
||||||
RenderedContent template.HTML `xorm:"-"`
|
RenderedContent template.HTML `xorm:"-"`
|
||||||
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
|
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
|
||||||
Labels []*Label `xorm:"-"`
|
Labels []*Label `xorm:"-"`
|
||||||
isLabelsLoaded bool `xorm:"-"`
|
isLabelsLoaded bool `xorm:"-"`
|
||||||
MilestoneID int64 `xorm:"INDEX"`
|
MilestoneID int64 `xorm:"INDEX"`
|
||||||
Milestone *Milestone `xorm:"-"`
|
Milestone *Milestone `xorm:"-"`
|
||||||
isMilestoneLoaded bool `xorm:"-"`
|
isMilestoneLoaded bool `xorm:"-"`
|
||||||
Project *project_model.Project `xorm:"-"`
|
Projects []*project_model.Project `xorm:"-"`
|
||||||
Priority int
|
Priority int
|
||||||
AssigneeID int64 `xorm:"-"`
|
AssigneeID int64 `xorm:"-"`
|
||||||
Assignee *user_model.User `xorm:"-"`
|
Assignee *user_model.User `xorm:"-"`
|
||||||
@ -327,7 +327,7 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = issue.LoadProject(ctx); err != nil {
|
if err = issue.LoadProjects(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,14 +207,19 @@ func (issues IssueList) LoadProjects(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, project := range projects {
|
for _, project := range projects {
|
||||||
projectMaps[project.IssueID] = project.Project
|
projectMaps[project.ID] = project.Project
|
||||||
}
|
}
|
||||||
left -= limit
|
left -= limit
|
||||||
issueIDs = issueIDs[limit:]
|
issueIDs = issueIDs[limit:]
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, issue := range issues {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -66,10 +66,10 @@ func TestIssueList_LoadAttributes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
if issue.ID == int64(1) {
|
if issue.ID == int64(1) {
|
||||||
assert.Equal(t, int64(400), issue.TotalTrackedTime)
|
assert.Equal(t, int64(400), issue.TotalTrackedTime)
|
||||||
assert.NotNil(t, issue.Project)
|
assert.NotNil(t, issue.Projects[0])
|
||||||
assert.Equal(t, int64(1), issue.Project.ID)
|
assert.Equal(t, int64(1), issue.Projects[0].ID)
|
||||||
} else {
|
} else {
|
||||||
assert.Nil(t, issue.Project)
|
assert.Nil(t, issue.Projects)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,28 +13,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// LoadProject load the project the issue was assigned to
|
// LoadProject load the project the issue was assigned to
|
||||||
func (issue *Issue) LoadProject(ctx context.Context) (err error) {
|
func (issue *Issue) LoadProjects(ctx context.Context) (err error) {
|
||||||
if issue.Project == nil {
|
if issue.Projects == nil {
|
||||||
var p project_model.Project
|
err = db.GetEngine(ctx).Table("project").
|
||||||
has, err := db.GetEngine(ctx).Table("project").
|
|
||||||
Join("INNER", "project_issue", "project.id=project_issue.project_id").
|
Join("INNER", "project_issue", "project.id=project_issue.project_id").
|
||||||
Where("project_issue.issue_id = ?", issue.ID).Get(&p)
|
Where("project_issue.issue_id = ?", issue.ID).Find(&issue.Projects)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if has {
|
|
||||||
issue.Project = &p
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (issue *Issue) projectID(ctx context.Context) int64 {
|
func (issue *Issue) projectIDs(ctx context.Context) []int64 {
|
||||||
var ip project_model.ProjectIssue
|
var ids []int64
|
||||||
has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip)
|
if err := db.GetEngine(ctx).Table("project_issue").Where("issue_id=?", issue.ID).Select("project_id").Find(&ids); err != nil {
|
||||||
if err != nil || !has {
|
return nil
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
return ip.ProjectID
|
|
||||||
|
return ids
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProjectColumnID return project column id if issue was assigned to one
|
// ProjectColumnID return project column id if issue was assigned to one
|
||||||
@ -68,7 +62,7 @@ func LoadProjectIssueColumnMap(ctx context.Context, projectID, defaultColumnID i
|
|||||||
func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *IssuesOptions) (IssueList, error) {
|
func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *IssuesOptions) (IssueList, error) {
|
||||||
issueList, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) {
|
issueList, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) {
|
||||||
o.ProjectColumnID = b.ID
|
o.ProjectColumnID = b.ID
|
||||||
o.ProjectID = b.ProjectID
|
o.ProjectIDs = []int64{b.ProjectID}
|
||||||
o.SortType = "project-column-sorting"
|
o.SortType = "project-column-sorting"
|
||||||
}))
|
}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -78,7 +72,7 @@ func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *Is
|
|||||||
if b.Default {
|
if b.Default {
|
||||||
issues, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) {
|
issues, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) {
|
||||||
o.ProjectColumnID = db.NoConditionID
|
o.ProjectColumnID = db.NoConditionID
|
||||||
o.ProjectID = b.ProjectID
|
o.ProjectIDs = []int64{b.ProjectID}
|
||||||
o.SortType = "project-column-sorting"
|
o.SortType = "project-column-sorting"
|
||||||
}))
|
}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -96,71 +90,88 @@ func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *Is
|
|||||||
|
|
||||||
// IssueAssignOrRemoveProject changes the project associated with an issue
|
// IssueAssignOrRemoveProject changes the project associated with an issue
|
||||||
// If newProjectID is 0, the issue is removed from the project
|
// 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 {
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
oldProjectID := issue.projectID(ctx)
|
oldProjectIDs := issue.projectIDs(ctx)
|
||||||
|
|
||||||
if err := issue.LoadRepo(ctx); err != nil {
|
if err := issue.LoadRepo(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only check if we add a new project and not remove it.
|
projectDB := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID)
|
||||||
if newProjectID > 0 {
|
newProjectIDs, oldProjectIDs := util.DiffSlice(oldProjectIDs, newProjectIDs)
|
||||||
newProject, err := project_model.GetProjectByID(ctx, newProjectID)
|
|
||||||
if err != nil {
|
if len(oldProjectIDs) > 0 {
|
||||||
|
if _, err := projectDB.Where("issue_id=?", issue.ID).In("project_id", oldProjectIDs).Delete(&project_model.ProjectIssue{}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !newProject.CanBeAccessedByOwnerRepo(issue.Repo.OwnerID, issue.Repo) {
|
for _, pID := range oldProjectIDs {
|
||||||
return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID)
|
if _, err := CreateComment(ctx, &CreateCommentOptions{
|
||||||
}
|
Type: CommentTypeProject,
|
||||||
if newColumnID == 0 {
|
Doer: doer,
|
||||||
newDefaultColumn, err := newProject.MustDefaultColumn(ctx)
|
Repo: issue.Repo,
|
||||||
if err != nil {
|
Issue: issue,
|
||||||
|
OldProjectID: pID,
|
||||||
|
ProjectID: 0,
|
||||||
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
newColumnID = newDefaultColumn.ID
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil {
|
if len(newProjectIDs) == 0 {
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if oldProjectID > 0 || newProjectID > 0 {
|
|
||||||
if _, err := CreateComment(ctx, &CreateCommentOptions{
|
|
||||||
Type: CommentTypeProject,
|
|
||||||
Doer: doer,
|
|
||||||
Repo: issue.Repo,
|
|
||||||
Issue: issue,
|
|
||||||
OldProjectID: oldProjectID,
|
|
||||||
ProjectID: newProjectID,
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if newProjectID == 0 {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if newColumnID == 0 {
|
|
||||||
panic("newColumnID must not be zero") // shouldn't happen
|
|
||||||
}
|
|
||||||
|
|
||||||
res := struct {
|
res := struct {
|
||||||
MaxSorting int64
|
MaxSorting int64
|
||||||
IssueCount int64
|
IssueCount int64
|
||||||
}{}
|
}{}
|
||||||
if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as issue_count").Table("project_issue").
|
if _, err := projectDB.Select("max(sorting) as max_sorting, count(*) as issue_count").Table("project_issue").
|
||||||
Where("project_id=?", newProjectID).
|
In("project_id", newProjectIDs).
|
||||||
And("project_board_id=?", newColumnID).
|
And("project_board_id=?", newColumnID).
|
||||||
Get(&res); err != nil {
|
Get(&res); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0)
|
newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0)
|
||||||
return db.Insert(ctx, &project_model.ProjectIssue{
|
|
||||||
IssueID: issue.ID,
|
pi := make([]*project_model.ProjectIssue, 0, len(newProjectIDs))
|
||||||
ProjectID: newProjectID,
|
|
||||||
ProjectColumnID: newColumnID,
|
for _, pID := range newProjectIDs {
|
||||||
Sorting: newSorting,
|
if pID == 0 {
|
||||||
})
|
continue
|
||||||
|
}
|
||||||
|
newProject, err := project_model.GetProjectByID(ctx, pID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !newProject.CanBeAccessedByOwnerRepo(issue.Repo.OwnerID, issue.Repo) {
|
||||||
|
return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
pi = append(pi, &project_model.ProjectIssue{
|
||||||
|
IssueID: issue.ID,
|
||||||
|
ProjectID: pID,
|
||||||
|
ProjectColumnID: newColumnID,
|
||||||
|
Sorting: newSorting,
|
||||||
|
})
|
||||||
|
|
||||||
|
if _, err := CreateComment(ctx, &CreateCommentOptions{
|
||||||
|
Type: CommentTypeProject,
|
||||||
|
Doer: doer,
|
||||||
|
Repo: issue.Repo,
|
||||||
|
Issue: issue,
|
||||||
|
OldProjectID: 0,
|
||||||
|
ProjectID: pID,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pi) > 0 {
|
||||||
|
return db.Insert(ctx, pi)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/container"
|
"code.gitea.io/gitea/modules/container"
|
||||||
"code.gitea.io/gitea/modules/optional"
|
"code.gitea.io/gitea/modules/optional"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
@ -36,7 +37,7 @@ type IssuesOptions struct { //nolint:revive // export stutter
|
|||||||
ReviewedID int64
|
ReviewedID int64
|
||||||
SubscriberID int64
|
SubscriberID int64
|
||||||
MilestoneIDs []int64
|
MilestoneIDs []int64
|
||||||
ProjectID int64
|
ProjectIDs []int64
|
||||||
ProjectColumnID int64
|
ProjectColumnID int64
|
||||||
IsClosed optional.Option[bool]
|
IsClosed optional.Option[bool]
|
||||||
IsPull optional.Option[bool]
|
IsPull optional.Option[bool]
|
||||||
@ -198,11 +199,12 @@ func applyMilestoneCondition(sess *xorm.Session, opts *IssuesOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func applyProjectCondition(sess *xorm.Session, opts *IssuesOptions) {
|
func applyProjectCondition(sess *xorm.Session, opts *IssuesOptions) {
|
||||||
if opts.ProjectID > 0 { // specific project
|
opts.ProjectIDs = util.RemoveValue(opts.ProjectIDs, 0)
|
||||||
|
if len(opts.ProjectIDs) == 1 && opts.ProjectIDs[0] == db.NoConditionID { // show those that are in no project
|
||||||
|
sess.And(builder.NotIn("issue.id", builder.Select("issue_id").From("project_issue")))
|
||||||
|
} else if len(opts.ProjectIDs) > 0 { // specific project
|
||||||
sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id").
|
sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id").
|
||||||
And("project_issue.project_id=?", opts.ProjectID)
|
In("project_issue.project_id", opts.ProjectIDs)
|
||||||
} else if opts.ProjectID == db.NoConditionID { // show those that are in no project
|
|
||||||
sess.And(builder.NotIn("issue.id", builder.Select("issue_id").From("project_issue").And(builder.Neq{"project_id": 0})))
|
|
||||||
}
|
}
|
||||||
// opts.ProjectID == 0 means all projects,
|
// opts.ProjectID == 0 means all projects,
|
||||||
// do not need to apply any condition
|
// do not need to apply any condition
|
||||||
|
@ -418,10 +418,10 @@ func TestIssueLoadAttributes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
if issue.ID == int64(1) {
|
if issue.ID == int64(1) {
|
||||||
assert.Equal(t, int64(400), issue.TotalTrackedTime)
|
assert.Equal(t, int64(400), issue.TotalTrackedTime)
|
||||||
assert.NotNil(t, issue.Project)
|
assert.NotNil(t, issue.Projects[0])
|
||||||
assert.Equal(t, int64(1), issue.Project.ID)
|
assert.Equal(t, int64(1), issue.Projects[0].ID)
|
||||||
} else {
|
} else {
|
||||||
assert.Nil(t, issue.Project)
|
assert.Nil(t, issue.Projects)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -241,9 +241,14 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
|||||||
queries = append(queries, bleve.NewDisjunctionQuery(milestoneQueries...))
|
queries = append(queries, bleve.NewDisjunctionQuery(milestoneQueries...))
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.ProjectID.Has() {
|
if len(options.ProjectIDs) > 0 {
|
||||||
queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectID.Value(), "project_id"))
|
var projectQueries []query.Query
|
||||||
|
for _, projectID := range options.ProjectIDs {
|
||||||
|
projectQueries = append(projectQueries, inner_bleve.NumericEqualityQuery(projectID, "project_id"))
|
||||||
|
}
|
||||||
|
queries = append(queries, bleve.NewDisjunctionQuery(projectQueries...))
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.ProjectColumnID.Has() {
|
if options.ProjectColumnID.Has() {
|
||||||
queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectColumnID.Value(), "project_board_id"))
|
queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectColumnID.Value(), "project_board_id"))
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,6 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
|
|||||||
ReviewRequestedID: convertID(options.ReviewRequestedID),
|
ReviewRequestedID: convertID(options.ReviewRequestedID),
|
||||||
ReviewedID: convertID(options.ReviewedID),
|
ReviewedID: convertID(options.ReviewedID),
|
||||||
SubscriberID: convertID(options.SubscriberID),
|
SubscriberID: convertID(options.SubscriberID),
|
||||||
ProjectID: convertID(options.ProjectID),
|
|
||||||
ProjectColumnID: convertID(options.ProjectColumnID),
|
ProjectColumnID: convertID(options.ProjectColumnID),
|
||||||
IsClosed: options.IsClosed,
|
IsClosed: options.IsClosed,
|
||||||
IsPull: options.IsPull,
|
IsPull: options.IsPull,
|
||||||
@ -88,6 +87,10 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
|
|||||||
opts.MilestoneIDs = options.MilestoneIDs
|
opts.MilestoneIDs = options.MilestoneIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(options.ProjectIDs) > 0 {
|
||||||
|
opts.ProjectIDs = options.ProjectIDs
|
||||||
|
}
|
||||||
|
|
||||||
if options.NoLabelOnly {
|
if options.NoLabelOnly {
|
||||||
opts.LabelIDs = []int64{0} // Be careful, it's zero, not db.NoConditionID
|
opts.LabelIDs = []int64{0} // Be careful, it's zero, not db.NoConditionID
|
||||||
} else {
|
} else {
|
||||||
|
@ -46,10 +46,8 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
|
|||||||
searchOpt.MilestoneIDs = opts.MilestoneIDs
|
searchOpt.MilestoneIDs = opts.MilestoneIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.ProjectID > 0 {
|
if len(opts.ProjectIDs) > 0 {
|
||||||
searchOpt.ProjectID = optional.Some(opts.ProjectID)
|
searchOpt.ProjectIDs = opts.ProjectIDs
|
||||||
} else if opts.ProjectID == db.NoConditionID { // FIXME: this is inconsistent from other places
|
|
||||||
searchOpt.ProjectID = optional.Some[int64](0) // Those issues with no project(projectid==0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
searchOpt.AssigneeID = opts.AssigneeID
|
searchOpt.AssigneeID = opts.AssigneeID
|
||||||
|
@ -204,8 +204,8 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
|||||||
query.Must(elastic.NewTermsQuery("milestone_id", toAnySlice(options.MilestoneIDs)...))
|
query.Must(elastic.NewTermsQuery("milestone_id", toAnySlice(options.MilestoneIDs)...))
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.ProjectID.Has() {
|
if len(options.ProjectIDs) > 0 {
|
||||||
query.Must(elastic.NewTermQuery("project_id", options.ProjectID.Value()))
|
query.Must(elastic.NewTermsQuery("project_id", toAnySlice(options.ProjectIDs)...))
|
||||||
}
|
}
|
||||||
if options.ProjectColumnID.Has() {
|
if options.ProjectColumnID.Has() {
|
||||||
query.Must(elastic.NewTermQuery("project_board_id", options.ProjectColumnID.Value()))
|
query.Must(elastic.NewTermQuery("project_board_id", options.ProjectColumnID.Value()))
|
||||||
|
@ -416,7 +416,7 @@ func searchIssueInProject(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
SearchOptions{
|
SearchOptions{
|
||||||
ProjectID: optional.Some(int64(1)),
|
ProjectIDs: optional.Some(int64(1)),
|
||||||
},
|
},
|
||||||
[]int64{5, 3, 2, 1},
|
[]int64{5, 3, 2, 1},
|
||||||
},
|
},
|
||||||
|
@ -30,7 +30,7 @@ type IndexerData struct {
|
|||||||
LabelIDs []int64 `json:"label_ids"`
|
LabelIDs []int64 `json:"label_ids"`
|
||||||
NoLabel bool `json:"no_label"` // True if LabelIDs is empty
|
NoLabel bool `json:"no_label"` // True if LabelIDs is empty
|
||||||
MilestoneID int64 `json:"milestone_id"`
|
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
|
ProjectColumnID int64 `json:"project_board_id"` // the key should be kept as project_board_id to keep compatible
|
||||||
PosterID int64 `json:"poster_id"`
|
PosterID int64 `json:"poster_id"`
|
||||||
AssigneeID int64 `json:"assignee_id"`
|
AssigneeID int64 `json:"assignee_id"`
|
||||||
@ -94,7 +94,7 @@ type SearchOptions struct {
|
|||||||
|
|
||||||
MilestoneIDs []int64 // milestones the issues have
|
MilestoneIDs []int64 // milestones the issues have
|
||||||
|
|
||||||
ProjectID optional.Option[int64] // project the issues belong to
|
ProjectIDs []int64 // project the issues belong to
|
||||||
ProjectColumnID optional.Option[int64] // project column the issues belong to
|
ProjectColumnID optional.Option[int64] // project column the issues belong to
|
||||||
|
|
||||||
PosterID string // poster of the issues, "(none)" or "(any)" or a user ID
|
PosterID string // poster of the issues, "(none)" or "(any)" or a user ID
|
||||||
|
@ -301,20 +301,25 @@ var cases = []*testIndexerCase{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "ProjectID",
|
Name: "ProjectIDs",
|
||||||
SearchOptions: &internal.SearchOptions{
|
SearchOptions: &internal.SearchOptions{
|
||||||
Paginator: &db.ListOptions{
|
Paginator: &db.ListOptions{
|
||||||
PageSize: 5,
|
PageSize: 5,
|
||||||
},
|
},
|
||||||
ProjectID: optional.Some(int64(1)),
|
ProjectIDs: []int64{1},
|
||||||
},
|
},
|
||||||
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
|
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
|
||||||
assert.Len(t, result.Hits, 5)
|
assert.Len(t, result.Hits, 5)
|
||||||
for _, v := range result.Hits {
|
for _, v := range result.Hits {
|
||||||
assert.Equal(t, int64(1), data[v.ID].ProjectID)
|
if len(data[v.ID].ProjectIDs) > 0 {
|
||||||
|
assert.Equal(t, int64(1), data[v.ID].ProjectIDs[0])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
|
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
|
||||||
return v.ProjectID == 1
|
if len(data[v.ID].ProjectIDs) > 0 {
|
||||||
|
return v.ProjectIDs[0] == 1
|
||||||
|
}
|
||||||
|
return false
|
||||||
}), result.Total)
|
}), result.Total)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -324,15 +329,20 @@ var cases = []*testIndexerCase{
|
|||||||
Paginator: &db.ListOptions{
|
Paginator: &db.ListOptions{
|
||||||
PageSize: 5,
|
PageSize: 5,
|
||||||
},
|
},
|
||||||
ProjectID: optional.Some(int64(0)),
|
ProjectIDs: []int64{0},
|
||||||
},
|
},
|
||||||
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
|
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
|
||||||
assert.Len(t, result.Hits, 5)
|
assert.Len(t, result.Hits, 5)
|
||||||
for _, v := range result.Hits {
|
for _, v := range result.Hits {
|
||||||
assert.Equal(t, int64(0), data[v.ID].ProjectID)
|
if len(data[v.ID].ProjectIDs) > 0 {
|
||||||
|
assert.Equal(t, int64(0), data[v.ID].ProjectIDs[0])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
|
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
|
||||||
return v.ProjectID == 0
|
if len(data[v.ID].ProjectIDs) > 0 {
|
||||||
|
return v.ProjectIDs[0] == 1
|
||||||
|
}
|
||||||
|
return false
|
||||||
}), result.Total)
|
}), result.Total)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -719,7 +729,7 @@ func generateDefaultIndexerData() []*internal.IndexerData {
|
|||||||
LabelIDs: labelIDs,
|
LabelIDs: labelIDs,
|
||||||
NoLabel: len(labelIDs) == 0,
|
NoLabel: len(labelIDs) == 0,
|
||||||
MilestoneID: issueIndex % 4,
|
MilestoneID: issueIndex % 4,
|
||||||
ProjectID: issueIndex % 5,
|
ProjectIDs: []int64{issueIndex % 5},
|
||||||
ProjectColumnID: issueIndex % 6,
|
ProjectColumnID: issueIndex % 6,
|
||||||
PosterID: id%10 + 1, // PosterID should not be 0
|
PosterID: id%10 + 1, // PosterID should not be 0
|
||||||
AssigneeID: issueIndex % 10,
|
AssigneeID: issueIndex % 10,
|
||||||
|
@ -180,8 +180,8 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
|||||||
query.And(inner_meilisearch.NewFilterIn("milestone_id", options.MilestoneIDs...))
|
query.And(inner_meilisearch.NewFilterIn("milestone_id", options.MilestoneIDs...))
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.ProjectID.Has() {
|
if len(options.ProjectIDs) > 0 {
|
||||||
query.And(inner_meilisearch.NewFilterEq("project_id", options.ProjectID.Value()))
|
query.And(inner_meilisearch.NewFilterIn("project_id", options.ProjectIDs...))
|
||||||
}
|
}
|
||||||
if options.ProjectColumnID.Has() {
|
if options.ProjectColumnID.Has() {
|
||||||
query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectColumnID.Value()))
|
query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectColumnID.Value()))
|
||||||
|
@ -87,9 +87,9 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD
|
|||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var projectID int64
|
projectIDs := make([]int64, 0, len(issue.Projects))
|
||||||
if issue.Project != nil {
|
for _, project := range issue.Projects {
|
||||||
projectID = issue.Project.ID
|
projectIDs = append(projectIDs, project.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
projectColumnID, err := issue.ProjectColumnID(ctx)
|
projectColumnID, err := issue.ProjectColumnID(ctx)
|
||||||
@ -110,7 +110,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD
|
|||||||
LabelIDs: labels,
|
LabelIDs: labels,
|
||||||
NoLabel: len(labels) == 0,
|
NoLabel: len(labels) == 0,
|
||||||
MilestoneID: issue.MilestoneID,
|
MilestoneID: issue.MilestoneID,
|
||||||
ProjectID: projectID,
|
ProjectIDs: projectIDs,
|
||||||
ProjectColumnID: projectColumnID,
|
ProjectColumnID: projectColumnID,
|
||||||
PosterID: issue.PosterID,
|
PosterID: issue.PosterID,
|
||||||
AssigneeID: issue.AssigneeID,
|
AssigneeID: issue.AssigneeID,
|
||||||
|
@ -253,3 +253,49 @@ func ReserveLineBreakForTextarea(input string) string {
|
|||||||
// Other than this, we should respect the original content, even leading or trailing spaces.
|
// Other than this, we should respect the original content, even leading or trailing spaces.
|
||||||
return strings.ReplaceAll(input, "\r\n", "\n")
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveValue[T comparable](a []T, target T) []T {
|
||||||
|
n := 0
|
||||||
|
for _, v := range a {
|
||||||
|
if v != target {
|
||||||
|
a[n] = v
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a[:n]
|
||||||
|
}
|
||||||
|
|
||||||
|
func JoinSlice[T any](items []T, toString func(T) string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
sep := ""
|
||||||
|
for _, item := range items {
|
||||||
|
b.WriteString(sep)
|
||||||
|
b.WriteString(toString(item))
|
||||||
|
sep = ","
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
@ -721,7 +721,7 @@ func CreateIssue(ctx *context.APIContext) {
|
|||||||
form.Labels = make([]int64, 0)
|
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) {
|
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||||
ctx.APIError(http.StatusBadRequest, err)
|
ctx.APIError(http.StatusBadRequest, err)
|
||||||
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
||||||
|
@ -189,7 +189,7 @@ func SearchIssues(ctx *context.Context) {
|
|||||||
IsClosed: isClosed,
|
IsClosed: isClosed,
|
||||||
IncludedAnyLabelIDs: includedAnyLabels,
|
IncludedAnyLabelIDs: includedAnyLabels,
|
||||||
MilestoneIDs: includedMilestones,
|
MilestoneIDs: includedMilestones,
|
||||||
ProjectID: projectID,
|
ProjectIDs: projectID,
|
||||||
SortBy: issue_indexer.SortByCreatedDesc,
|
SortBy: issue_indexer.SortByCreatedDesc,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,12 +345,12 @@ func SearchRepoIssuesJSON(ctx *context.Context) {
|
|||||||
Page: ctx.FormInt("page"),
|
Page: ctx.FormInt("page"),
|
||||||
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
|
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
|
||||||
},
|
},
|
||||||
Keyword: keyword,
|
Keyword: keyword,
|
||||||
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
||||||
IsPull: isPull,
|
IsPull: isPull,
|
||||||
IsClosed: isClosed,
|
IsClosed: isClosed,
|
||||||
ProjectID: projectID,
|
ProjectIDs: projectID,
|
||||||
SortBy: issue_indexer.SortByCreatedDesc,
|
SortBy: issue_indexer.SortByCreatedDesc,
|
||||||
}
|
}
|
||||||
if since != 0 {
|
if since != 0 {
|
||||||
searchOpt.UpdatedAfterUnix = optional.Some(since)
|
searchOpt.UpdatedAfterUnix = optional.Some(since)
|
||||||
@ -542,7 +542,6 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6
|
|||||||
RepoIDs: []int64{repo.ID},
|
RepoIDs: []int64{repo.ID},
|
||||||
LabelIDs: preparedLabelFilter.SelectedLabelIDs,
|
LabelIDs: preparedLabelFilter.SelectedLabelIDs,
|
||||||
MilestoneIDs: mileIDs,
|
MilestoneIDs: mileIDs,
|
||||||
ProjectID: projectID,
|
|
||||||
AssigneeID: assigneeID,
|
AssigneeID: assigneeID,
|
||||||
MentionedID: mentionedID,
|
MentionedID: mentionedID,
|
||||||
PosterID: posterUserID,
|
PosterID: posterUserID,
|
||||||
@ -551,6 +550,11 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6
|
|||||||
IsPull: isPullOption,
|
IsPull: isPullOption,
|
||||||
IssueIDs: nil,
|
IssueIDs: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if projectID != 0 {
|
||||||
|
statsOpts.ProjectIDs = []int64{projectID}
|
||||||
|
}
|
||||||
|
|
||||||
if keyword != "" {
|
if keyword != "" {
|
||||||
keywordMatchedIssueIDs, _, err = issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, statsOpts))
|
keywordMatchedIssueIDs, _, err = issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, statsOpts))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -629,7 +633,7 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6
|
|||||||
ReviewRequestedID: reviewRequestedID,
|
ReviewRequestedID: reviewRequestedID,
|
||||||
ReviewedID: reviewedID,
|
ReviewedID: reviewedID,
|
||||||
MilestoneIDs: mileIDs,
|
MilestoneIDs: mileIDs,
|
||||||
ProjectID: projectID,
|
ProjectIDs: []int64{projectID},
|
||||||
IsClosed: isShowClosed,
|
IsClosed: isShowClosed,
|
||||||
IsPull: isPullOption,
|
IsPull: isPullOption,
|
||||||
LabelIDs: preparedLabelFilter.SelectedLabelIDs,
|
LabelIDs: preparedLabelFilter.SelectedLabelIDs,
|
||||||
|
@ -121,8 +121,8 @@ func NewIssue(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pageMetaData.MilestonesData.SelectedMilestoneID = ctx.FormInt64("milestone")
|
pageMetaData.MilestonesData.SelectedMilestoneID = ctx.FormInt64("milestone")
|
||||||
pageMetaData.ProjectsData.SelectedProjectID = ctx.FormInt64("project")
|
pageMetaData.ProjectsData.SelectedProjectID = ctx.FormString("project")
|
||||||
if pageMetaData.ProjectsData.SelectedProjectID > 0 {
|
if len(pageMetaData.ProjectsData.SelectedProjectID) > 0 {
|
||||||
if len(ctx.Req.URL.Query().Get("project")) > 0 {
|
if len(ctx.Req.URL.Query().Get("project")) > 0 {
|
||||||
ctx.Data["redirect_after_creation"] = "project"
|
ctx.Data["redirect_after_creation"] = "project"
|
||||||
}
|
}
|
||||||
@ -239,8 +239,9 @@ func toSet[ItemType any, KeyType comparable](slice []ItemType, keyFunc func(Item
|
|||||||
|
|
||||||
// ValidateRepoMetasForNewIssue check and returns repository's meta information
|
// ValidateRepoMetasForNewIssue check and returns repository's meta information
|
||||||
func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueForm, isPull bool) (ret struct {
|
func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueForm, isPull bool) (ret struct {
|
||||||
LabelIDs, AssigneeIDs []int64
|
LabelIDs, AssigneeIDs []int64
|
||||||
MilestoneID, ProjectID int64
|
MilestoneID int64
|
||||||
|
ProjectIDs []int64
|
||||||
|
|
||||||
Reviewers []*user_model.User
|
Reviewers []*user_model.User
|
||||||
TeamReviewers []*organization.Team
|
TeamReviewers []*organization.Team
|
||||||
@ -269,11 +270,13 @@ func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueFo
|
|||||||
|
|
||||||
allProjects := append(slices.Clone(pageMetaData.ProjectsData.OpenProjects), pageMetaData.ProjectsData.ClosedProjects...)
|
allProjects := append(slices.Clone(pageMetaData.ProjectsData.OpenProjects), pageMetaData.ProjectsData.ClosedProjects...)
|
||||||
candidateProjects := toSet(allProjects, func(project *project_model.Project) int64 { return project.ID })
|
candidateProjects := toSet(allProjects, func(project *project_model.Project) int64 { return project.ID })
|
||||||
if form.ProjectID > 0 && !candidateProjects.Contains(form.ProjectID) {
|
inputProjectIDs, _ := base.StringsToInt64s(strings.Split(form.ProjectIDs, ","))
|
||||||
ctx.NotFound(nil)
|
pageMetaData.ProjectsData.SelectedProjectID = util.JoinSlice(inputProjectIDs, func(v int64) string {
|
||||||
return ret
|
if candidateProjects.Contains(v) {
|
||||||
}
|
return strconv.FormatInt(v, 10)
|
||||||
pageMetaData.ProjectsData.SelectedProjectID = form.ProjectID
|
}
|
||||||
|
return ""
|
||||||
|
})
|
||||||
|
|
||||||
// prepare assignees
|
// prepare assignees
|
||||||
candidateAssignees := toSet(pageMetaData.AssigneesData.CandidateAssignees, func(user *user_model.User) int64 { return user.ID })
|
candidateAssignees := toSet(pageMetaData.AssigneesData.CandidateAssignees, func(user *user_model.User) int64 { return user.ID })
|
||||||
@ -318,7 +321,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
|
ret.Reviewers, ret.TeamReviewers = reviewers, teamReviewers
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
@ -343,9 +346,9 @@ func NewIssuePost(ctx *context.Context) {
|
|||||||
return
|
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) {
|
if !ctx.Repo.CanRead(unit.TypeProjects) {
|
||||||
// User must also be able to see the project.
|
// User must also be able to see the project.
|
||||||
ctx.HTTPError(http.StatusBadRequest, "user hasn't permissions to read projects")
|
ctx.HTTPError(http.StatusBadRequest, "user hasn't permissions to read projects")
|
||||||
@ -385,7 +388,7 @@ func NewIssuePost(ctx *context.Context) {
|
|||||||
Ref: form.Ref,
|
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) {
|
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||||
ctx.HTTPError(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
|
ctx.HTTPError(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
|
||||||
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
||||||
@ -397,8 +400,8 @@ func NewIssuePost(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
|
log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
|
||||||
if ctx.FormString("redirect_after_creation") == "project" && projectID > 0 {
|
if ctx.FormString("redirect_after_creation") == "project" && len(projectIDs) > 0 {
|
||||||
project, err := project_model.GetProjectByID(ctx, projectID)
|
project, err := project_model.GetProjectByID(ctx, projectIDs[0])
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if project.Type == project_model.TypeOrganization {
|
if project.Type == project_model.TypeOrganization {
|
||||||
ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.Repo.Owner, project.ID))
|
ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.Repo.Owner, project.ID))
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/container"
|
"code.gitea.io/gitea/modules/container"
|
||||||
"code.gitea.io/gitea/modules/optional"
|
"code.gitea.io/gitea/modules/optional"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
issue_service "code.gitea.io/gitea/services/issue"
|
issue_service "code.gitea.io/gitea/services/issue"
|
||||||
@ -34,7 +35,7 @@ type issueSidebarAssigneesData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type issueSidebarProjectsData struct {
|
type issueSidebarProjectsData struct {
|
||||||
SelectedProjectID int64
|
SelectedProjectID string
|
||||||
OpenProjects []*project_model.Project
|
OpenProjects []*project_model.Project
|
||||||
ClosedProjects []*project_model.Project
|
ClosedProjects []*project_model.Project
|
||||||
}
|
}
|
||||||
@ -160,8 +161,10 @@ func (d *IssuePageMetaData) retrieveAssigneesData(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *IssuePageMetaData) retrieveProjectsDataForIssueWriter(ctx *context.Context) {
|
func (d *IssuePageMetaData) retrieveProjectsDataForIssueWriter(ctx *context.Context) {
|
||||||
if d.Issue != nil && d.Issue.Project != nil {
|
if d.Issue != nil && len(d.Issue.Projects) > 0 {
|
||||||
d.ProjectsData.SelectedProjectID = d.Issue.Project.ID
|
d.ProjectsData.SelectedProjectID = util.JoinSlice(d.Issue.Projects, func(v *project_model.Project) string {
|
||||||
|
return strconv.FormatInt(v.ID, 10)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
d.ProjectsData.OpenProjects, d.ProjectsData.ClosedProjects = retrieveProjectsInternal(ctx, ctx.Repo.Repository)
|
d.ProjectsData.OpenProjects, d.ProjectsData.ClosedProjects = retrieveProjectsInternal(ctx, ctx.Repo.Repository)
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
"code.gitea.io/gitea/models/renderhelper"
|
"code.gitea.io/gitea/models/renderhelper"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
"code.gitea.io/gitea/modules/markup/markdown"
|
"code.gitea.io/gitea/modules/markup/markdown"
|
||||||
"code.gitea.io/gitea/modules/optional"
|
"code.gitea.io/gitea/modules/optional"
|
||||||
@ -441,12 +442,9 @@ func UpdateIssueProject(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
projectID := ctx.FormInt64("id")
|
projectIDs, _ := base.StringsToInt64s(strings.Split(ctx.FormString("id"), ","))
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
if issue.Project != nil && issue.Project.ID == projectID {
|
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectIDs, 0); err != nil {
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectID, 0); err != nil {
|
|
||||||
if errors.Is(err, util.ErrPermissionDenied) {
|
if errors.Is(err, util.ErrPermissionDenied) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -1303,7 +1303,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
|||||||
return
|
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 {
|
if setting.Attachment.Enabled {
|
||||||
attachments = form.Files
|
attachments = form.Files
|
||||||
@ -1409,8 +1409,8 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if projectID > 0 && ctx.Repo.CanWrite(unit.TypeProjects) {
|
if ctx.Repo.CanWrite(unit.TypeProjects) {
|
||||||
if err := issues_model.IssueAssignOrRemoveProject(ctx, pullIssue, ctx.Doer, projectID, 0); err != nil {
|
if err := issues_model.IssueAssignOrRemoveProject(ctx, pullIssue, ctx.Doer, projectIDs, 0); err != nil {
|
||||||
if !errors.Is(err, util.ErrPermissionDenied) {
|
if !errors.Is(err, util.ErrPermissionDenied) {
|
||||||
ctx.ServerError("IssueAssignOrRemoveProject", err)
|
ctx.ServerError("IssueAssignOrRemoveProject", err)
|
||||||
return
|
return
|
||||||
|
@ -425,7 +425,7 @@ type CreateIssueForm struct {
|
|||||||
ReviewerIDs string `form:"reviewer_ids"`
|
ReviewerIDs string `form:"reviewer_ids"`
|
||||||
Ref string `form:"ref"`
|
Ref string `form:"ref"`
|
||||||
MilestoneID int64
|
MilestoneID int64
|
||||||
ProjectID int64
|
ProjectIDs string `form:"project_ids"`
|
||||||
Content string
|
Content string
|
||||||
Files []string
|
Files []string
|
||||||
AllowMaintainerEdit bool
|
AllowMaintainerEdit bool
|
||||||
|
@ -23,7 +23,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// NewIssue creates new issue with labels for repository.
|
// 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 {
|
if err := issue.LoadPoster(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -41,12 +41,7 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_mo
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if projectID > 0 {
|
return issues_model.IssueAssignOrRemoveProject(ctx, issue, issue.Poster, projectIDs, 0)
|
||||||
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, issue.Poster, projectID, 0); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,12 @@ func MoveIssuesOnProjectColumn(ctx context.Context, doer *user_model.User, colum
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.Exec(ctx, "UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", column.ID, sorting, issueID)
|
_, err = db.GetEngine(ctx).Table("project_issue").
|
||||||
|
Where("issue_id = ? AND project_id = ?", issueID, column.ProjectID).
|
||||||
|
Update(map[string]any{
|
||||||
|
"project_board_id": column.ID,
|
||||||
|
"sorting": sorting,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -89,7 +94,7 @@ func MoveIssuesOnProjectColumn(ctx context.Context, doer *user_model.User, colum
|
|||||||
// LoadIssuesFromProject load issues assigned to each project column inside the given project
|
// LoadIssuesFromProject load issues assigned to each project column inside the given project
|
||||||
func LoadIssuesFromProject(ctx context.Context, project *project_model.Project, opts *issues_model.IssuesOptions) (map[int64]issues_model.IssueList, error) {
|
func LoadIssuesFromProject(ctx context.Context, project *project_model.Project, opts *issues_model.IssuesOptions) (map[int64]issues_model.IssueList, error) {
|
||||||
issueList, err := issues_model.Issues(ctx, opts.Copy(func(o *issues_model.IssuesOptions) {
|
issueList, err := issues_model.Issues(ctx, opts.Copy(func(o *issues_model.IssuesOptions) {
|
||||||
o.ProjectID = project.ID
|
o.ProjectIDs = []int64{project.ID}
|
||||||
o.SortType = "project-column-sorting"
|
o.SortType = "project-column-sorting"
|
||||||
}))
|
}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -180,10 +185,10 @@ func LoadIssueNumbersForProject(ctx context.Context, project *project_model.Proj
|
|||||||
|
|
||||||
// for user or org projects, we need to check access permissions
|
// for user or org projects, we need to check access permissions
|
||||||
opts := issues_model.IssuesOptions{
|
opts := issues_model.IssuesOptions{
|
||||||
ProjectID: project.ID,
|
ProjectIDs: []int64{project.ID},
|
||||||
Doer: doer,
|
Doer: doer,
|
||||||
AllPublic: doer == nil,
|
AllPublic: doer == nil,
|
||||||
Owner: project.Owner,
|
Owner: project.Owner,
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
@ -117,12 +117,12 @@ func Test_Projects(t *testing.T) {
|
|||||||
|
|
||||||
// issue 6 belongs to private repo 3 under org 3
|
// issue 6 belongs to private repo 3 under org 3
|
||||||
issue6 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 6})
|
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)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// issue 16 belongs to public repo 16 under org 3
|
// issue 16 belongs to public repo 16 under org 3
|
||||||
issue16 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 16})
|
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)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
projects, err := db.Find[project_model.Project](db.DefaultContext, project_model.SearchOptions{
|
projects, err := db.Find[project_model.Project](db.DefaultContext, project_model.SearchOptions{
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
{{$pageMeta := .}}
|
{{$pageMeta := .}}
|
||||||
{{$data := .ProjectsData}}
|
{{$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}}
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="issue-sidebar-combo" data-selection-mode="single" data-update-algo="all"
|
<div class="issue-sidebar-combo" data-selection-mode="multiple" data-update-algo="all"
|
||||||
{{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/projects?issue_ids={{$pageMeta.Issue.ID}}"{{end}}
|
{{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/projects?issue_ids={{$pageMeta.Issue.ID}}"{{end}}
|
||||||
>
|
>
|
||||||
<input class="combo-value" name="project_id" type="hidden" value="{{$data.SelectedProjectID}}">
|
<input class="combo-value" name="project_ids" type="hidden" value="{{$data.SelectedProjectID}}">
|
||||||
<div class="ui dropdown full-width {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}">
|
<div class="ui dropdown full-width {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}">
|
||||||
<a class="fixed-text muted">
|
<a class="fixed-text muted">
|
||||||
<strong>{{ctx.Locale.Tr "repo.issues.new.projects"}}</strong> {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}}
|
<strong>{{ctx.Locale.Tr "repo.issues.new.projects"}}</strong> {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}}
|
||||||
@ -24,6 +24,7 @@
|
|||||||
<div class="header">{{ctx.Locale.Tr "repo.issues.new.open_projects"}}</div>
|
<div class="header">{{ctx.Locale.Tr "repo.issues.new.open_projects"}}</div>
|
||||||
{{range $data.OpenProjects}}
|
{{range $data.OpenProjects}}
|
||||||
<a class="item muted" data-value="{{.ID}}" href="{{.Link ctx}}">
|
<a class="item muted" data-value="{{.ID}}" href="{{.Link ctx}}">
|
||||||
|
<span class="item-check-mark">{{svg "octicon-check"}}</span>
|
||||||
{{svg .IconName 18}} {{.Title}}
|
{{svg .IconName 18}} {{.Title}}
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
@ -33,6 +34,7 @@
|
|||||||
<div class="header">{{ctx.Locale.Tr "repo.issues.new.closed_projects"}}</div>
|
<div class="header">{{ctx.Locale.Tr "repo.issues.new.closed_projects"}}</div>
|
||||||
{{range $data.ClosedProjects}}
|
{{range $data.ClosedProjects}}
|
||||||
<a class="item muted" data-value="{{.ID}}" href="{{.Link ctx}}">
|
<a class="item muted" data-value="{{.ID}}" href="{{.Link ctx}}">
|
||||||
|
<span class="item-check-mark">{{svg "octicon-check"}}</span>
|
||||||
{{svg .IconName 18}} {{.Title}}
|
{{svg .IconName 18}} {{.Title}}
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
@ -42,9 +44,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="ui list muted-links flex-items-block">
|
<div class="ui list muted-links flex-items-block">
|
||||||
<span class="item empty-list {{if $issueProject}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span>
|
<span class="item empty-list {{if $issueProject}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span>
|
||||||
{{if $issueProject}}
|
{{range $issueProject}}
|
||||||
<a class="item" href="{{$issueProject.Link ctx}}">
|
<a class="item" href="{{.Link ctx}}">
|
||||||
{{svg $issueProject.IconName 18}} {{$issueProject.Title}}
|
{{svg .IconName 18}} {{.Title}}
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -76,10 +76,10 @@
|
|||||||
<span class="gt-ellipsis">{{.Milestone.Name}}</span>
|
<span class="gt-ellipsis">{{.Milestone.Name}}</span>
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .Project}}
|
{{range .Projects}}
|
||||||
<a class="project flex-text-inline tw-max-w-[300px]" href="{{.Project.Link ctx}}">
|
<a class="project flex-text-inline tw-max-w-[300px]" href="{{.Link ctx}}">
|
||||||
{{svg .Project.IconName 14}}
|
{{svg .IconName 14}}
|
||||||
<span class="gt-ellipsis">{{.Project.Title}}</span>
|
<span class="gt-ellipsis">{{.Title}}</span>
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .Ref}}{{/* TODO: RemoveIssueRef: see "repo/issue/branch_selector_field.tmpl" */}}
|
{{if .Ref}}{{/* TODO: RemoveIssueRef: see "repo/issue/branch_selector_field.tmpl" */}}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user