@@ -30,6 +30,7 @@ const ( | |||
SearchOrderByStarsReverse SearchOrderBy = "num_stars DESC" | |||
SearchOrderByForks SearchOrderBy = "num_forks ASC" | |||
SearchOrderByForksReverse SearchOrderBy = "num_forks DESC" | |||
SearchOrderByTitle SearchOrderBy = "title ASC" | |||
) | |||
const ( |
@@ -103,14 +103,14 @@ 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:"-"` | |||
Labels []*Label `xorm:"-"` | |||
MilestoneID int64 `xorm:"INDEX"` | |||
Milestone *Milestone `xorm:"-"` | |||
Project *project_model.Project `xorm:"-"` | |||
OriginalAuthorID int64 `xorm:"index"` | |||
Title string `xorm:"name"` | |||
Content string `xorm:"LONGTEXT"` | |||
RenderedContent template.HTML `xorm:"-"` | |||
Labels []*Label `xorm:"-"` | |||
MilestoneID int64 `xorm:"INDEX"` | |||
Milestone *Milestone `xorm:"-"` | |||
Projects []*project_model.Project `xorm:"-"` | |||
Priority int | |||
AssigneeID int64 `xorm:"-"` | |||
Assignee *user_model.User `xorm:"-"` | |||
@@ -311,7 +311,7 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) { | |||
return err | |||
} | |||
if err = issue.LoadProject(ctx); err != nil { | |||
if err = issue.LoadProjects(ctx); err != nil { | |||
return err | |||
} | |||
@@ -251,14 +251,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 | |||
} |
@@ -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) | |||
assert.Equal(t, int64(1), issue.Projects[0].ID) | |||
} else { | |||
assert.Nil(t, issue.Project) | |||
assert.Nil(t, issue.Projects) | |||
} | |||
} | |||
} |
@@ -13,28 +13,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"). | |||
func (issue *Issue) LoadProjects(ctx context.Context) (err error) { | |||
if issue.Projects == nil { | |||
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 ips []int64 | |||
if err := db.GetEngine(ctx).Table("project_issue").Select("project_id").Where("issue_id=?", issue.ID).Find(&ips); err != nil { | |||
return nil | |||
} | |||
return ip.ProjectID | |||
return ips | |||
} | |||
// ProjectBoardID return project board id if issue was assigned to one | |||
@@ -91,24 +84,25 @@ func LoadIssuesFromBoardList(ctx context.Context, bs project_model.BoardList) (m | |||
} | |||
// ChangeProjectAssign changes the project associated with an issue | |||
func ChangeProjectAssign(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error { | |||
func ChangeProjectAssign(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64, action string) error { | |||
ctx, committer, err := db.TxContext(ctx) | |||
if err != nil { | |||
return err | |||
} | |||
defer committer.Close() | |||
if err := addUpdateIssueProject(ctx, issue, doer, newProjectID); err != nil { | |||
if err := addUpdateIssueProject(ctx, issue, doer, newProjectID, action); err != nil { | |||
return err | |||
} | |||
return committer.Commit() | |||
} | |||
func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error { | |||
oldProjectID := issue.projectID(ctx) | |||
func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64, action string) error { | |||
var oldProjectIDs []int64 | |||
var err error | |||
if err := issue.LoadRepo(ctx); err != nil { | |||
if err = issue.LoadRepo(ctx); err != nil { | |||
return err | |||
} | |||
@@ -123,25 +117,51 @@ func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.U | |||
} | |||
} | |||
if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil { | |||
return err | |||
if action == "null" { | |||
if newProjectID == 0 { | |||
action = "clear" | |||
} else { | |||
action = "attach" | |||
count, err := db.GetEngine(ctx).Table("project_issue").Where("issue_id=? AND project_id=?", issue.ID, newProjectID).Count() | |||
if err != nil { | |||
return err | |||
} | |||
if count > 0 { | |||
action = "detach" | |||
} | |||
} | |||
} | |||
if action == "attach" { | |||
err = db.Insert(ctx, &project_model.ProjectIssue{ | |||
IssueID: issue.ID, | |||
ProjectID: newProjectID, | |||
}) | |||
oldProjectIDs = append(oldProjectIDs, 0) | |||
} else if action == "detach" { | |||
_, err = db.GetEngine(ctx).Where("issue_id=? AND project_id=?", issue.ID, newProjectID).Delete(&project_model.ProjectIssue{}) | |||
oldProjectIDs = append(oldProjectIDs, newProjectID) | |||
newProjectID = 0 | |||
} else if action == "clear" { | |||
if err = db.GetEngine(ctx).Table("project_issue").Select("project_id").Where("issue_id=?", issue.ID).Find(&oldProjectIDs); err != nil { | |||
return err | |||
} | |||
_, err = db.GetEngine(ctx).Where("issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}) | |||
newProjectID = 0 | |||
} | |||
if oldProjectID > 0 || newProjectID > 0 { | |||
for i := range oldProjectIDs { | |||
if _, err := CreateComment(ctx, &CreateCommentOptions{ | |||
Type: CommentTypeProject, | |||
Doer: doer, | |||
Repo: issue.Repo, | |||
Issue: issue, | |||
OldProjectID: oldProjectID, | |||
OldProjectID: oldProjectIDs[i], | |||
ProjectID: newProjectID, | |||
}); err != nil { | |||
return err | |||
} | |||
} | |||
return db.Insert(ctx, &project_model.ProjectIssue{ | |||
IssueID: issue.ID, | |||
ProjectID: newProjectID, | |||
}) | |||
return err | |||
} |
@@ -174,6 +174,8 @@ func applyProjectBoardCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.S | |||
// do not need to apply any condition | |||
if opts.ProjectBoardID > 0 { | |||
sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectBoardID})) | |||
} else if opts.ProjectID > 0 { | |||
sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0, "project_id": opts.ProjectID})) | |||
} else if opts.ProjectBoardID == db.NoConditionID { | |||
sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0})) | |||
} |
@@ -418,10 +418,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) | |||
assert.Equal(t, int64(1), issue.Projects[0].ID) | |||
} else { | |||
assert.Nil(t, issue.Project) | |||
assert.Nil(t, issue.Projects) | |||
} | |||
} | |||
} |
@@ -76,7 +76,7 @@ func (p *Project) NumOpenIssues(ctx context.Context) int { | |||
} | |||
// MoveIssuesOnProjectBoard moves or keeps issues in a column and sorts them inside that column | |||
func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs map[int64]int64) error { | |||
func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs map[int64]int64, projectID int64) error { | |||
return db.WithTx(ctx, func(ctx context.Context) error { | |||
sess := db.GetEngine(ctx) | |||
@@ -93,7 +93,7 @@ func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs | |||
} | |||
for sorting, issueID := range sortedIssueIDs { | |||
_, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", board.ID, sorting, issueID) | |||
_, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=? AND project_id=?", board.ID, sorting, issueID, projectID) | |||
if err != nil { | |||
return err | |||
} |
@@ -237,6 +237,8 @@ func GetSearchOrderByBySortType(sortType string) db.SearchOrderBy { | |||
return db.SearchOrderByRecentUpdated | |||
case "leastupdate": | |||
return db.SearchOrderByLeastUpdated | |||
case "title": | |||
return db.SearchOrderByTitle | |||
default: | |||
return db.SearchOrderByNewest | |||
} |
@@ -23,7 +23,7 @@ import ( | |||
const ( | |||
issueIndexerAnalyzer = "issueIndexer" | |||
issueIndexerDocType = "issueIndexerDocType" | |||
issueIndexerLatestVersion = 4 | |||
issueIndexerLatestVersion = 5 | |||
) | |||
const unicodeNormalizeName = "unicodeNormalize" | |||
@@ -78,7 +78,8 @@ func generateIssueIndexMapping() (mapping.IndexMapping, error) { | |||
docMapping.AddFieldMappingsAt("label_ids", numberFieldMapping) | |||
docMapping.AddFieldMappingsAt("no_label", boolFieldMapping) | |||
docMapping.AddFieldMappingsAt("milestone_id", numberFieldMapping) | |||
docMapping.AddFieldMappingsAt("project_id", numberFieldMapping) | |||
docMapping.AddFieldMappingsAt("project_ids", numberFieldMapping) | |||
docMapping.AddFieldMappingsAt("no_project", boolFieldMapping) | |||
docMapping.AddFieldMappingsAt("project_board_id", numberFieldMapping) | |||
docMapping.AddFieldMappingsAt("poster_id", numberFieldMapping) | |||
docMapping.AddFieldMappingsAt("assignee_id", numberFieldMapping) | |||
@@ -222,7 +223,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( | |||
} | |||
if options.ProjectID.Has() { | |||
queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectID.Value(), "project_id")) | |||
if v := options.ProjectID.Value(); v != 0 { | |||
queries = append(queries, inner_bleve.NumericEqualityQuery(v, "project_ids")) | |||
} else { | |||
queries = append(queries, inner_bleve.BoolFieldQuery(true, "no_project")) | |||
} | |||
} | |||
if options.ProjectBoardID.Has() { | |||
queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectBoardID.Value(), "project_board_id")) |
@@ -18,7 +18,7 @@ import ( | |||
) | |||
const ( | |||
issueIndexerLatestVersion = 1 | |||
issueIndexerLatestVersion = 2 | |||
// multi-match-types, currently only 2 types are used | |||
// Reference: https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-dsl-multi-match-query.html#multi-match-types | |||
esMultiMatchTypeBestFields = "best_fields" | |||
@@ -61,7 +61,8 @@ const ( | |||
"label_ids": { "type": "integer", "index": true }, | |||
"no_label": { "type": "boolean", "index": true }, | |||
"milestone_id": { "type": "integer", "index": true }, | |||
"project_id": { "type": "integer", "index": true }, | |||
"project_ids": { "type": "integer", "index": true }, | |||
"no_project": { "type": "boolean", "index": true }, | |||
"project_board_id": { "type": "integer", "index": true }, | |||
"poster_id": { "type": "integer", "index": true }, | |||
"assignee_id": { "type": "integer", "index": true }, | |||
@@ -195,7 +196,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( | |||
} | |||
if options.ProjectID.Has() { | |||
query.Must(elastic.NewTermQuery("project_id", options.ProjectID.Value())) | |||
if v := options.ProjectID.Value(); v != 0 { | |||
query.Must(elastic.NewTermQuery("project_ids", v)) | |||
} else { | |||
query.Must(elastic.NewTermQuery("no_project", true)) | |||
} | |||
} | |||
if options.ProjectBoardID.Has() { | |||
query.Must(elastic.NewTermQuery("project_board_id", options.ProjectBoardID.Value())) |
@@ -361,12 +361,6 @@ func searchIssueInProject(t *testing.T) { | |||
opts SearchOptions | |||
expectedIDs []int64 | |||
}{ | |||
{ | |||
SearchOptions{ | |||
ProjectID: optional.Some(int64(1)), | |||
}, | |||
[]int64{5, 3, 2, 1}, | |||
}, | |||
{ | |||
SearchOptions{ | |||
ProjectBoardID: optional.Some(int64(1)), |
@@ -26,7 +26,8 @@ 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_ids"` | |||
NoProject bool `json:"no_project"` // True if ProjectIDs is empty | |||
ProjectBoardID int64 `json:"project_board_id"` | |||
PosterID int64 `json:"poster_id"` | |||
AssigneeID int64 `json:"assignee_id"` | |||
@@ -89,7 +90,7 @@ type SearchOptions struct { | |||
MilestoneIDs []int64 // milestones the issues have | |||
ProjectID optional.Option[int64] // project the issues belong to | |||
ProjectID optional.Option[int64] // project the issues belong to, zero means no project | |||
ProjectBoardID optional.Option[int64] // project board the issues belong to | |||
PosterID optional.Option[int64] // poster of the issues |
@@ -312,10 +312,10 @@ var cases = []*testIndexerCase{ | |||
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { | |||
assert.Equal(t, 5, len(result.Hits)) | |||
for _, v := range result.Hits { | |||
assert.Equal(t, int64(1), data[v.ID].ProjectID) | |||
assert.Contains(t, data[v.ID].ProjectIDs, int64(1)) | |||
} | |||
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { | |||
return v.ProjectID == 1 | |||
return slices.Contains(v.ProjectIDs, 1) | |||
}), result.Total) | |||
}, | |||
}, | |||
@@ -330,10 +330,10 @@ var cases = []*testIndexerCase{ | |||
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { | |||
assert.Equal(t, 5, len(result.Hits)) | |||
for _, v := range result.Hits { | |||
assert.Equal(t, int64(0), data[v.ID].ProjectID) | |||
assert.Empty(t, data[v.ID].ProjectIDs) | |||
} | |||
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { | |||
return v.ProjectID == 0 | |||
return len(v.ProjectIDs) == 0 | |||
}), result.Total) | |||
}, | |||
}, | |||
@@ -692,6 +692,10 @@ func generateDefaultIndexerData() []*internal.IndexerData { | |||
for i := range subscriberIDs { | |||
subscriberIDs[i] = int64(i) + 1 // SubscriberID should not be 0 | |||
} | |||
projectIDs := make([]int64, id%5) | |||
for i := range projectIDs { | |||
projectIDs[i] = int64(i) + 1 // ProjectID should not be 0 | |||
} | |||
data = append(data, &internal.IndexerData{ | |||
ID: id, | |||
@@ -705,7 +709,8 @@ func generateDefaultIndexerData() []*internal.IndexerData { | |||
LabelIDs: labelIDs, | |||
NoLabel: len(labelIDs) == 0, | |||
MilestoneID: issueIndex % 4, | |||
ProjectID: issueIndex % 5, | |||
ProjectIDs: projectIDs, | |||
NoProject: len(projectIDs) == 0, | |||
ProjectBoardID: issueIndex % 6, | |||
PosterID: id%10 + 1, // PosterID should not be 0 | |||
AssigneeID: issueIndex % 10, |
@@ -18,7 +18,7 @@ import ( | |||
) | |||
const ( | |||
issueIndexerLatestVersion = 3 | |||
issueIndexerLatestVersion = 4 | |||
// TODO: make this configurable if necessary | |||
maxTotalHits = 10000 | |||
@@ -64,7 +64,8 @@ func NewIndexer(url, apiKey, indexerName string) *Indexer { | |||
"label_ids", | |||
"no_label", | |||
"milestone_id", | |||
"project_id", | |||
"project_ids", | |||
"no_project", | |||
"project_board_id", | |||
"poster_id", | |||
"assignee_id", | |||
@@ -172,7 +173,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( | |||
} | |||
if options.ProjectID.Has() { | |||
query.And(inner_meilisearch.NewFilterEq("project_id", options.ProjectID.Value())) | |||
if v := options.ProjectID.Value(); v != 0 { | |||
query.And(inner_meilisearch.NewFilterEq("project_ids", v)) | |||
} else { | |||
query.And(inner_meilisearch.NewFilterEq("no_label", true)) | |||
} | |||
} | |||
if options.ProjectBoardID.Has() { | |||
query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectBoardID.Value())) |
@@ -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) | |||
} | |||
return &internal.IndexerData{ | |||
@@ -104,7 +104,8 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD | |||
LabelIDs: labels, | |||
NoLabel: len(labels) == 0, | |||
MilestoneID: issue.MilestoneID, | |||
ProjectID: projectID, | |||
ProjectIDs: projectIDs, | |||
NoProject: len(projectIDs) == 0, | |||
ProjectBoardID: issue.ProjectBoardID(ctx), | |||
PosterID: issue.PosterID, | |||
AssigneeID: issue.AssigneeID, |
@@ -442,14 +442,9 @@ func UpdateIssueProject(ctx *context.Context) { | |||
} | |||
projectID := ctx.FormInt64("id") | |||
action := ctx.FormString("action") | |||
for _, issue := range issues { | |||
if issue.Project != nil { | |||
if issue.Project.ID == projectID { | |||
continue | |||
} | |||
} | |||
if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil { | |||
if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID, action); err != nil { | |||
ctx.ServerError("ChangeProjectAssign", err) | |||
return | |||
} | |||
@@ -671,7 +666,7 @@ func MoveIssues(ctx *context.Context) { | |||
} | |||
} | |||
if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs); err != nil { | |||
if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs, project.ID); err != nil { | |||
ctx.ServerError("MoveIssuesOnProjectBoard", err) | |||
return | |||
} |
@@ -385,14 +385,9 @@ func UpdateIssueProject(ctx *context.Context) { | |||
} | |||
projectID := ctx.FormInt64("id") | |||
action := ctx.FormString("action") | |||
for _, issue := range issues { | |||
if issue.Project != nil { | |||
if issue.Project.ID == projectID { | |||
continue | |||
} | |||
} | |||
if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil { | |||
if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID, action); err != nil { | |||
ctx.ServerError("ChangeProjectAssign", err) | |||
return | |||
} | |||
@@ -659,7 +654,7 @@ func MoveIssues(ctx *context.Context) { | |||
} | |||
} | |||
if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs); err != nil { | |||
if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs, project.ID); err != nil { | |||
ctx.ServerError("MoveIssuesOnProjectBoard", err) | |||
return | |||
} |
@@ -1334,7 +1334,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { | |||
ctx.Error(http.StatusBadRequest, "user hasn't the permission to write to projects") | |||
return | |||
} | |||
if err := issues_model.ChangeProjectAssign(ctx, pullIssue, ctx.Doer, projectID); err != nil { | |||
if err := issues_model.ChangeProjectAssign(ctx, pullIssue, ctx.Doer, projectID, "attach"); err != nil { | |||
ctx.ServerError("ChangeProjectAssign", err) | |||
return | |||
} |
@@ -358,7 +358,6 @@ func Issues(ctx *context.Context) { | |||
ctx.Status(http.StatusNotFound) | |||
return | |||
} | |||
ctx.Data["Title"] = ctx.Tr("issues") | |||
ctx.Data["PageIsIssues"] = true | |||
buildIssueOverview(ctx, unit.TypeIssues) |
@@ -42,7 +42,7 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_mo | |||
} | |||
} | |||
if projectID > 0 { | |||
if err := issues_model.ChangeProjectAssign(ctx, issue, issue.Poster, projectID); err != nil { | |||
if err := issues_model.ChangeProjectAssign(ctx, issue, issue.Poster, projectID, "attach"); err != nil { | |||
return err | |||
} | |||
} |
@@ -154,7 +154,7 @@ | |||
{{if .IsProjectsEnabled}} | |||
<div class="divider"></div> | |||
<div class="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-project dropdown"> | |||
<div class="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-projects dropdown"> | |||
<a class="text muted flex-text-block"> | |||
<strong>{{ctx.Locale.Tr "repo.issues.new.projects"}}</strong> | |||
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}} | |||
@@ -175,8 +175,19 @@ | |||
{{ctx.Locale.Tr "repo.issues.new.open_projects"}} | |||
</div> | |||
{{range .OpenProjects}} | |||
<a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}"> | |||
{{$ProjectID := .ID}} | |||
{{$checked := false}} | |||
{{range $.Issue.Projects}} | |||
{{if eq .ID $ProjectID}} | |||
{{$checked = true}} | |||
{{break}} | |||
{{end}} | |||
{{end}} | |||
<a class="item muted sidebar-item-link{{if $checked}} checked{{end}}" data-id="{{.ID}}" data-href="{{.Link ctx}}"> | |||
<span class="octicon-check{{if not $checked}} tw-invisible{{end}}">{{svg "octicon-check"}}</span> | |||
<span class="text"> | |||
{{svg .IconName 18 "tw-mr-2"}}{{.Title}} | |||
</span> | |||
</a> | |||
{{end}} | |||
{{end}} | |||
@@ -186,20 +197,33 @@ | |||
{{ctx.Locale.Tr "repo.issues.new.closed_projects"}} | |||
</div> | |||
{{range .ClosedProjects}} | |||
<a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}"> | |||
{{$ProjectID := $.Projects.IssueID}} | |||
{{$checked := false}} | |||
{{range $.Issue.Projects}} | |||
{{if eq .IssueID $ProjectID}} | |||
{{$checked = true}} | |||
{{break}} | |||
{{end}} | |||
{{end}} | |||
<a class="item muted sidebar-item-link{{if $checked}} checked{{end}}" data-id="{{.ID}}" data-href="{{.Link ctx}}"> | |||
<span class="octicon-check{{if not $checked}} tw-invisible{{end}}">{{svg "octicon-check"}}</span> | |||
<span class="text"> | |||
{{svg .IconName 18 "tw-mr-2"}}{{.Title}} | |||
</span> | |||
</a> | |||
{{end}} | |||
{{end}} | |||
</div> | |||
</div> | |||
<div class="ui select-project list"> | |||
<span class="no-select item {{if .Issue.Project}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span> | |||
<div class="ui projects list"> | |||
<span class="no-select item {{if .Issue.Projects}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span> | |||
<div class="selected"> | |||
{{if .Issue.Project}} | |||
<a class="item muted sidebar-item-link" href="{{.Issue.Project.Link ctx}}"> | |||
{{svg .Issue.Project.IconName 18 "tw-mr-2"}}{{.Issue.Project.Title}} | |||
</a> | |||
{{range .Issue.Projects}} | |||
<div class="item"> | |||
<a class="muted sidebar-item-link" href="{{.Link ctx}}"> | |||
{{svg .IconName 18 "tw-mr-2"}}{{.Title}} | |||
</a> | |||
</div> | |||
{{end}} | |||
</div> | |||
</div> |
@@ -93,10 +93,10 @@ | |||
<span class="gt-ellipsis">{{.Milestone.Name}}</span> | |||
</a> | |||
{{end}} | |||
{{if .Project}} | |||
<a class="project flex-text-inline tw-max-w-[300px]" href="{{.Project.Link ctx}}"> | |||
{{svg .Project.IconName 14}} | |||
<span class="gt-ellipsis">{{.Project.Title}}</span> | |||
{{range .Projects}} | |||
<a class="project flex-text-inline tw-max-w-[300px]" href="{{.Link ctx}}"> | |||
{{svg .IconName 14}} | |||
<span class="gt-ellipsis">{{.Title}}</span> | |||
</a> | |||
{{end}} | |||
{{if .Ref}} |
@@ -240,6 +240,7 @@ export function initRepoCommentForm() { | |||
// Init labels and assignees | |||
initListSubmits('select-label', 'labels'); | |||
initListSubmits('select-projects', 'projects'); | |||
initListSubmits('select-assignees', 'assignees'); | |||
initListSubmits('select-assignees-modify', 'assignees'); | |||
initListSubmits('select-reviewers-modify', 'assignees'); |