refactor
This commit is contained in:
@@ -185,7 +185,7 @@ func deleteColumnByID(ctx context.Context, columnID int64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = column.moveIssuesToAnotherColumn(ctx, defaultColumn); err != nil {
|
if err = moveIssuesToAnotherColumn(ctx, column, defaultColumn); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,26 +257,12 @@ func (p *Project) GetColumns(ctx context.Context) (ColumnList, error) {
|
|||||||
return columns, nil
|
return columns, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CountColumns returns the total number of columns for a project
|
// getDefaultColumnWithFallback return default column if one exists
|
||||||
func (p *Project) CountColumns(ctx context.Context) (int64, error) {
|
// otherwise return the first column by sorting and set it as default column
|
||||||
return db.GetEngine(ctx).Where("project_id=?", p.ID).Count(&Column{})
|
func (p *Project) getDefaultColumnWithFallback(ctx context.Context) (*Column, error) {
|
||||||
}
|
|
||||||
|
|
||||||
// GetColumnsPaginated fetches a page of columns for a project
|
|
||||||
func (p *Project) GetColumnsPaginated(ctx context.Context, opts db.ListOptions) (ColumnList, error) {
|
|
||||||
columns := make([]*Column, 0, opts.PageSize)
|
|
||||||
if err := db.SetSessionPagination(db.GetEngine(ctx), &opts).
|
|
||||||
Where("project_id=?", p.ID).
|
|
||||||
OrderBy("sorting, id").
|
|
||||||
Find(&columns); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return columns, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getDefaultColumn return default column and ensure only one exists
|
|
||||||
func (p *Project) getDefaultColumn(ctx context.Context) (*Column, error) {
|
|
||||||
var column Column
|
var column Column
|
||||||
|
|
||||||
|
// try to find a column "default=true"
|
||||||
has, err := db.GetEngine(ctx).
|
has, err := db.GetEngine(ctx).
|
||||||
Where("project_id=? AND `default` = ?", p.ID, true).
|
Where("project_id=? AND `default` = ?", p.ID, true).
|
||||||
Desc("id").Get(&column)
|
Desc("id").Get(&column)
|
||||||
@@ -287,23 +273,9 @@ func (p *Project) getDefaultColumn(ctx context.Context) (*Column, error) {
|
|||||||
if has {
|
if has {
|
||||||
return &column, nil
|
return &column, nil
|
||||||
}
|
}
|
||||||
return nil, ErrProjectColumnNotExist{ColumnID: 0}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustDefaultColumn returns the default column for a project.
|
// try to find the first column by sorting
|
||||||
// If one exists, it is returned
|
has, err = db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Get(&column)
|
||||||
// If none exists, the first column will be elevated to the default column of this project
|
|
||||||
func (p *Project) MustDefaultColumn(ctx context.Context) (*Column, error) {
|
|
||||||
c, err := p.getDefaultColumn(ctx)
|
|
||||||
if err != nil && !IsErrProjectColumnNotExist(err) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if c != nil {
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var column Column
|
|
||||||
has, err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Get(&column)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -315,8 +287,24 @@ func (p *Project) MustDefaultColumn(ctx context.Context) (*Column, error) {
|
|||||||
return &column, nil
|
return &column, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil, ErrProjectColumnNotExist{ColumnID: 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustDefaultColumn returns the default column for a project.
|
||||||
|
// If one exists, it is returned
|
||||||
|
// If none exists, the first column will be elevated to the default column of this project
|
||||||
|
// If there is no column, it creates a default column and returns it
|
||||||
|
func (p *Project) MustDefaultColumn(ctx context.Context) (*Column, error) {
|
||||||
|
c, err := p.getDefaultColumnWithFallback(ctx)
|
||||||
|
if err != nil && !IsErrProjectColumnNotExist(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if c != nil {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
// create a default column if none is found
|
// create a default column if none is found
|
||||||
column = Column{
|
column := Column{
|
||||||
ProjectID: p.ID,
|
ProjectID: p.ID,
|
||||||
Default: true,
|
Default: true,
|
||||||
Title: "Uncategorized",
|
Title: "Uncategorized",
|
||||||
@@ -349,20 +337,6 @@ func SetDefaultColumn(ctx context.Context, projectID, columnID int64) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateColumnSorting update project column sorting
|
|
||||||
func UpdateColumnSorting(ctx context.Context, cl ColumnList) error {
|
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
|
||||||
for i := range cl {
|
|
||||||
if _, err := db.GetEngine(ctx).ID(cl[i].ID).Cols(
|
|
||||||
"sorting",
|
|
||||||
).Update(cl[i]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (ColumnList, error) {
|
func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (ColumnList, error) {
|
||||||
columns := make([]*Column, 0, 5)
|
columns := make([]*Column, 0, 5)
|
||||||
if len(columnsIDs) == 0 {
|
if len(columnsIDs) == 0 {
|
||||||
|
|||||||
42
models/project/column_list.go
Normal file
42
models/project/column_list.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package project
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CountColumns returns the total number of columns for a project
|
||||||
|
func CountProjectColumns(ctx context.Context, projectID int64) (int64, error) {
|
||||||
|
return db.GetEngine(ctx).Where("project_id=?", projectID).Count(&Column{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProjectColumns returns a list of columns for a project with pagination
|
||||||
|
func GetProjectColumns(ctx context.Context, projectID int64, opts db.ListOptions) (ColumnList, error) {
|
||||||
|
columns := make([]*Column, 0, opts.PageSize)
|
||||||
|
s := db.GetEngine(ctx).Where("project_id=?", projectID).OrderBy("sorting, id")
|
||||||
|
if !opts.IsListAll() {
|
||||||
|
db.SetSessionPagination(s, &opts)
|
||||||
|
}
|
||||||
|
if err := s.Find(&columns); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return columns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (ColumnList, error) {
|
||||||
|
columns := make([]*Column, 0, 5)
|
||||||
|
if len(columnsIDs) == 0 {
|
||||||
|
return columns, nil
|
||||||
|
}
|
||||||
|
if err := db.GetEngine(ctx).
|
||||||
|
Where("project_id =?", projectID).
|
||||||
|
In("id", columnsIDs).
|
||||||
|
OrderBy("sorting").Find(&columns); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return columns, nil
|
||||||
|
}
|
||||||
49
models/project/column_list_test.go
Normal file
49
models/project/column_list_test.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package project
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCountColumns(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
project, err := GetProjectByID(t.Context(), 1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
count, err := CountProjectColumns(t.Context(), project.ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, 3, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetColumnsPaginated(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
project, err := GetProjectByID(t.Context(), 1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Page 1, limit 2 — returns first 2 columns
|
||||||
|
page1, err := GetProjectColumns(t.Context(), project.ID, db.ListOptions{Page: 1, PageSize: 2})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, page1, 2)
|
||||||
|
|
||||||
|
// Page 2, limit 2 — returns remaining column
|
||||||
|
page2, err := GetProjectColumns(t.Context(), project.ID, db.ListOptions{Page: 2, PageSize: 2})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, page2, 1)
|
||||||
|
|
||||||
|
// Page 1 and page 2 together cover all columns with no overlap
|
||||||
|
allIDs := make(map[int64]bool)
|
||||||
|
for _, c := range append(page1, page2...) {
|
||||||
|
assert.False(t, allIDs[c.ID], "duplicate column ID %d across pages", c.ID)
|
||||||
|
allIDs[c.ID] = true
|
||||||
|
}
|
||||||
|
assert.Len(t, allIDs, 3)
|
||||||
|
}
|
||||||
@@ -80,7 +80,7 @@ func Test_MoveColumnsOnProject(t *testing.T) {
|
|||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1})
|
project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1})
|
||||||
columns, err := project1.GetColumns(t.Context())
|
columns, err := GetProjectColumns(t.Context(), project1.ID, db.ListOptionsAll)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, columns, 3)
|
assert.Len(t, columns, 3)
|
||||||
assert.EqualValues(t, 0, columns[0].Sorting) // even if there is no default sorting, the code should also work
|
assert.EqualValues(t, 0, columns[0].Sorting) // even if there is no default sorting, the code should also work
|
||||||
@@ -94,7 +94,7 @@ func Test_MoveColumnsOnProject(t *testing.T) {
|
|||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
columnsAfter, err := project1.GetColumns(t.Context())
|
columnsAfter, err := GetProjectColumns(t.Context(), project1.ID, db.ListOptionsAll)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, columnsAfter, 3)
|
assert.Len(t, columnsAfter, 3)
|
||||||
assert.Equal(t, columns[1].ID, columnsAfter[0].ID)
|
assert.Equal(t, columns[1].ID, columnsAfter[0].ID)
|
||||||
@@ -106,7 +106,7 @@ func Test_NewColumn(t *testing.T) {
|
|||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1})
|
project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1})
|
||||||
columns, err := project1.GetColumns(t.Context())
|
columns, err := GetProjectColumns(t.Context(), project1.ID, db.ListOptionsAll)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, columns, 3)
|
assert.Len(t, columns, 3)
|
||||||
|
|
||||||
@@ -124,39 +124,3 @@ func Test_NewColumn(t *testing.T) {
|
|||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "maximum number of columns reached")
|
assert.Contains(t, err.Error(), "maximum number of columns reached")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCountColumns(t *testing.T) {
|
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
|
|
||||||
project, err := GetProjectByID(t.Context(), 1)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
count, err := project.CountColumns(t.Context())
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.EqualValues(t, 3, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetColumnsPaginated(t *testing.T) {
|
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
|
|
||||||
project, err := GetProjectByID(t.Context(), 1)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Page 1, limit 2 — returns first 2 columns
|
|
||||||
page1, err := project.GetColumnsPaginated(t.Context(), db.ListOptions{Page: 1, PageSize: 2})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Len(t, page1, 2)
|
|
||||||
|
|
||||||
// Page 2, limit 2 — returns remaining column
|
|
||||||
page2, err := project.GetColumnsPaginated(t.Context(), db.ListOptions{Page: 2, PageSize: 2})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Len(t, page2, 1)
|
|
||||||
|
|
||||||
// Page 1 and page 2 together cover all columns with no overlap
|
|
||||||
allIDs := make(map[int64]bool)
|
|
||||||
for _, c := range append(page1, page2...) {
|
|
||||||
assert.False(t, allIDs[c.ID], "duplicate column ID %d across pages", c.ID)
|
|
||||||
allIDs[c.ID] = true
|
|
||||||
}
|
|
||||||
assert.Len(t, allIDs, 3)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -385,14 +385,14 @@ func ListProjectColumns(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
total, err := project.CountColumns(ctx)
|
total, err := project_model.CountProjectColumns(ctx, project.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
listOptions := utils.GetListOptions(ctx)
|
listOptions := utils.GetListOptions(ctx)
|
||||||
columns, err := project.GetColumnsPaginated(ctx, listOptions)
|
columns, err := project_model.GetProjectColumns(ctx, project.ID, listOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ func ViewProject(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
columns, err := project.GetColumns(ctx)
|
columns, err := project_model.GetProjectColumns(ctx, project.ID, db.ListOptionsAll)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetProjectColumns", err)
|
ctx.ServerError("GetProjectColumns", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -33,15 +33,12 @@ type issueSidebarAssigneesData struct {
|
|||||||
CandidateAssignees []*user_model.User
|
CandidateAssignees []*user_model.User
|
||||||
}
|
}
|
||||||
|
|
||||||
type issueSidebarProjectCardData struct {
|
|
||||||
Project *project_model.Project
|
|
||||||
Columns []*project_model.Column
|
|
||||||
SelectedColumn *project_model.Column
|
|
||||||
}
|
|
||||||
|
|
||||||
type issueSidebarProjectsData struct {
|
type issueSidebarProjectsData struct {
|
||||||
SelectedProjectIDs []int64
|
SelectedProjectIDs []int64 // TODO: support multiple projects in the future
|
||||||
ProjectCards []*issueSidebarProjectCardData
|
|
||||||
|
// the "selected" fields are only valid when len(SelectedProjectIDs)==1
|
||||||
|
SelectedProjectColumns []*project_model.Column
|
||||||
|
SelectedProjectColumn *project_model.Column
|
||||||
|
|
||||||
OpenProjects []*project_model.Project
|
OpenProjects []*project_model.Project
|
||||||
ClosedProjects []*project_model.Project
|
ClosedProjects []*project_model.Project
|
||||||
@@ -110,7 +107,7 @@ func retrieveRepoIssueMetaData(ctx *context.Context, repo *repo_model.Repository
|
|||||||
// A reader(creator) could update some meta (eg: target branch), but can't change assignees anymore.
|
// A reader(creator) could update some meta (eg: target branch), but can't change assignees anymore.
|
||||||
// For non-creator users, only writers could update some meta (eg: assignees, milestone, project)
|
// For non-creator users, only writers could update some meta (eg: assignees, milestone, project)
|
||||||
// Need to clarify the logic and add some tests in the future
|
// Need to clarify the logic and add some tests in the future
|
||||||
data.CanModifyIssueOrPull = ctx.Repo.Permission.CanWriteIssuesOrPulls(isPull) && !ctx.Repo.Repository.IsArchived
|
data.CanModifyIssueOrPull = ctx.Repo.CanWriteIssuesOrPulls(isPull) && !ctx.Repo.Repository.IsArchived
|
||||||
if !data.CanModifyIssueOrPull {
|
if !data.CanModifyIssueOrPull {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
@@ -171,80 +168,34 @@ func (d *IssuePageMetaData) retrieveAssigneesData(ctx *context.Context) {
|
|||||||
ctx.Data["Assignees"] = d.AssigneesData.CandidateAssignees
|
ctx.Data["Assignees"] = d.AssigneesData.CandidateAssignees
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *IssuePageMetaData) retrieveProjectCardsForExistingIssue(ctx *context.Context) {
|
|
||||||
if err := d.Issue.LoadProjects(ctx); err != nil {
|
|
||||||
ctx.ServerError("LoadProjects", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load column mappings for all projects
|
|
||||||
projectColumnMap, err := d.Issue.ProjectColumnMap(ctx)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("ProjectColumnMap", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build project cards for each project
|
|
||||||
d.ProjectsData.ProjectCards = make([]*issueSidebarProjectCardData, 0, len(d.Issue.Projects))
|
|
||||||
for _, project := range d.Issue.Projects {
|
|
||||||
columns, err := project.GetColumns(ctx)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("GetProjectColumns", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedColumn *project_model.Column
|
|
||||||
columnID := projectColumnMap[project.ID]
|
|
||||||
for _, col := range columns {
|
|
||||||
if col.ID == columnID {
|
|
||||||
selectedColumn = col
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if selectedColumn == nil {
|
|
||||||
selectedColumn, err = project.MustDefaultColumn(ctx)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("MustDefaultColumn", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
d.ProjectsData.ProjectCards = append(d.ProjectsData.ProjectCards, &issueSidebarProjectCardData{
|
|
||||||
Project: project,
|
|
||||||
Columns: columns,
|
|
||||||
SelectedColumn: selectedColumn,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
d.ProjectsData.SelectedProjectIDs = make([]int64, 0, len(d.ProjectsData.ProjectCards))
|
|
||||||
for _, card := range d.ProjectsData.ProjectCards {
|
|
||||||
d.ProjectsData.SelectedProjectIDs = append(d.ProjectsData.SelectedProjectIDs, card.Project.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *IssuePageMetaData) retrieveProjectData(ctx *context.Context) {
|
func (d *IssuePageMetaData) retrieveProjectData(ctx *context.Context) {
|
||||||
if d.Issue == nil {
|
if d.Issue == nil || d.Issue.Project == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
d.retrieveProjectCardsForExistingIssue(ctx)
|
d.ProjectsData.SelectedProjectIDs = []int64{d.Issue.Project.ID}
|
||||||
}
|
columns, err := project_model.GetProjectColumns(ctx, d.Issue.Project.ID, db.ListOptionsAll)
|
||||||
|
if err != nil {
|
||||||
func (d *IssuePageMetaData) SetSelectedProjectIDs(ids []int64) {
|
ctx.ServerError("GetProjectColumns", err)
|
||||||
allProjects := map[int64]*project_model.Project{}
|
return
|
||||||
for _, p := range d.ProjectsData.OpenProjects {
|
|
||||||
allProjects[p.ID] = p
|
|
||||||
}
|
}
|
||||||
for _, p := range d.ProjectsData.ClosedProjects {
|
d.ProjectsData.SelectedProjectColumns = columns
|
||||||
allProjects[p.ID] = p
|
columnID, err := d.Issue.ProjectColumnID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("ProjectColumnID", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
for _, id := range ids {
|
for _, col := range columns {
|
||||||
if project, ok := allProjects[id]; ok {
|
if col.ID == columnID {
|
||||||
d.ProjectsData.ProjectCards = append(d.ProjectsData.ProjectCards, &issueSidebarProjectCardData{Project: project})
|
d.ProjectsData.SelectedProjectColumn = col
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
d.ProjectsData.SelectedProjectIDs = ids
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *IssuePageMetaData) retrieveProjectsDataForIssueWriter(ctx *context.Context) {
|
func (d *IssuePageMetaData) retrieveProjectsDataForIssueWriter(ctx *context.Context) {
|
||||||
|
if d.Issue != nil && d.Issue.Project != nil {
|
||||||
|
d.ProjectsData.SelectedProjectIDs = []int64{d.Issue.Project.ID}
|
||||||
|
}
|
||||||
d.ProjectsData.OpenProjects, d.ProjectsData.ClosedProjects = retrieveProjectsInternal(ctx, ctx.Repo.Repository)
|
d.ProjectsData.OpenProjects, d.ProjectsData.ClosedProjects = retrieveProjectsInternal(ctx, ctx.Repo.Repository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ func ViewProject(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
columns, err := project.GetColumns(ctx)
|
columns, err := project_model.GetProjectColumns(ctx, project.ID, db.ListOptionsAll)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetProjectColumns", err)
|
ctx.ServerError("GetProjectColumns", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
project_model "code.gitea.io/gitea/models/project"
|
project_model "code.gitea.io/gitea/models/project"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
@@ -60,7 +60,7 @@ func TestMoveRepoProjectColumns(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
columns, err := project1.GetColumns(t.Context())
|
columns, err := project_model.GetProjectColumns(t.Context(), project1.ID, db.ListOptionsAll)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, columns, 3)
|
assert.Len(t, columns, 3)
|
||||||
assert.EqualValues(t, 0, columns[0].Sorting)
|
assert.EqualValues(t, 0, columns[0].Sorting)
|
||||||
@@ -80,7 +80,7 @@ func TestMoveRepoProjectColumns(t *testing.T) {
|
|||||||
})
|
})
|
||||||
sess.MakeRequest(t, req, http.StatusOK)
|
sess.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
columnsAfter, err := project1.GetColumns(t.Context())
|
columnsAfter, err := project_model.GetProjectColumns(t.Context(), project1.ID, db.ListOptionsAll)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, columnsAfter, 3)
|
assert.Len(t, columnsAfter, 3)
|
||||||
assert.Equal(t, columns[1].ID, columnsAfter[0].ID)
|
assert.Equal(t, columns[1].ID, columnsAfter[0].ID)
|
||||||
@@ -90,128 +90,6 @@ func TestMoveRepoProjectColumns(t *testing.T) {
|
|||||||
assert.NoError(t, project_model.DeleteProjectByID(t.Context(), project1.ID))
|
assert.NoError(t, project_model.DeleteProjectByID(t.Context(), project1.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateIssueProject(t *testing.T) {
|
|
||||||
defer tests.PrepareTestEnv(t)()
|
|
||||||
|
|
||||||
sess := loginUser(t, "user2")
|
|
||||||
|
|
||||||
t.Run("AssignAndRemove", func(t *testing.T) {
|
|
||||||
req := NewRequestWithValues(t, "POST", "/user2/repo1/issues/projects?issue_ids=2", map[string]string{
|
|
||||||
"id": "1",
|
|
||||||
})
|
|
||||||
sess.MakeRequest(t, req, http.StatusOK)
|
|
||||||
unittest.AssertExistsAndLoadBean(t, &project_model.ProjectIssue{IssueID: 2, ProjectID: 1})
|
|
||||||
|
|
||||||
req = NewRequestWithValues(t, "POST", "/user2/repo1/issues/projects?issue_ids=2", map[string]string{
|
|
||||||
"id": "",
|
|
||||||
})
|
|
||||||
sess.MakeRequest(t, req, http.StatusOK)
|
|
||||||
unittest.AssertNotExistsBean(t, &project_model.ProjectIssue{IssueID: 2, ProjectID: 1})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUpdateIssueProjectColumn(t *testing.T) {
|
|
||||||
defer tests.PrepareTestEnv(t)()
|
|
||||||
|
|
||||||
// fixture: issue 3 is in project 1 of repo user2/repo1, column "In Progress" (id=2)
|
|
||||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
|
|
||||||
assert.EqualValues(t, 1, issue.RepoID)
|
|
||||||
|
|
||||||
sess := loginUser(t, "user2")
|
|
||||||
|
|
||||||
t.Run("MoveColumn", func(t *testing.T) {
|
|
||||||
req := NewRequestWithValues(t, "POST", "/user2/repo1/issues/projects/column", map[string]string{
|
|
||||||
"issue_id": "3",
|
|
||||||
"id": "3",
|
|
||||||
})
|
|
||||||
sess.MakeRequest(t, req, http.StatusOK)
|
|
||||||
|
|
||||||
pi := unittest.AssertExistsAndLoadBean(t, &project_model.ProjectIssue{IssueID: 3})
|
|
||||||
assert.EqualValues(t, 3, pi.ProjectColumnID)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("InvalidIssueID", func(t *testing.T) {
|
|
||||||
req := NewRequestWithValues(t, "POST", "/user2/repo1/issues/projects/column", map[string]string{
|
|
||||||
"issue_id": "0",
|
|
||||||
"id": "3",
|
|
||||||
})
|
|
||||||
sess.MakeRequest(t, req, http.StatusNotFound)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("WrongRepo", func(t *testing.T) {
|
|
||||||
req := NewRequestWithValues(t, "POST", "/user2/repo1/issues/projects/column", map[string]string{
|
|
||||||
"issue_id": "6",
|
|
||||||
"id": "3",
|
|
||||||
})
|
|
||||||
sess.MakeRequest(t, req, http.StatusNotFound)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("WrongProject", func(t *testing.T) {
|
|
||||||
project2 := project_model.Project{
|
|
||||||
Title: "second project on repo1",
|
|
||||||
RepoID: 1,
|
|
||||||
Type: project_model.TypeRepository,
|
|
||||||
TemplateType: project_model.TemplateTypeNone,
|
|
||||||
}
|
|
||||||
require.NoError(t, project_model.NewProject(t.Context(), &project2))
|
|
||||||
require.NoError(t, project_model.NewColumn(t.Context(), &project_model.Column{
|
|
||||||
Title: "other column",
|
|
||||||
ProjectID: project2.ID,
|
|
||||||
}))
|
|
||||||
columns, err := project2.GetColumns(t.Context())
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotEmpty(t, columns)
|
|
||||||
|
|
||||||
req := NewRequestWithValues(t, "POST", "/user2/repo1/issues/projects/column", map[string]string{
|
|
||||||
"issue_id": "1",
|
|
||||||
"id": strconv.FormatInt(columns[0].ID, 10),
|
|
||||||
})
|
|
||||||
sess.MakeRequest(t, req, http.StatusNotFound)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIssueSidebarProjectColumn(t *testing.T) {
|
|
||||||
defer tests.PrepareTestEnv(t)()
|
|
||||||
|
|
||||||
// fixture: issue 5 (index=4) is in project 1 of repo user2/repo1, column "Done" (id=3)
|
|
||||||
sess := loginUser(t, "user2")
|
|
||||||
|
|
||||||
req := NewRequest(t, "GET", "/user2/repo1/issues/4")
|
|
||||||
resp := sess.MakeRequest(t, req, http.StatusOK)
|
|
||||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
||||||
|
|
||||||
cards := htmlDoc.Find(".flex-relaxed-list > .item.sidebar-project-card")
|
|
||||||
assert.Equal(t, 1, cards.Length())
|
|
||||||
|
|
||||||
title := cards.Find("a span.gt-ellipsis")
|
|
||||||
assert.Contains(t, strings.TrimSpace(title.Text()), "First project")
|
|
||||||
|
|
||||||
columnCombo := cards.Find(".issue-sidebar-combo.sidebar-project-column-combo")
|
|
||||||
assert.Equal(t, 1, columnCombo.Length())
|
|
||||||
|
|
||||||
defaultItem := columnCombo.Find(`.menu .item[data-value="1"]`)
|
|
||||||
assert.Equal(t, 1, defaultItem.Length())
|
|
||||||
|
|
||||||
inProgressItem := columnCombo.Find(`.menu .item[data-value="2"]`)
|
|
||||||
assert.Equal(t, 1, inProgressItem.Length())
|
|
||||||
doneItem := columnCombo.Find(`.menu .item[data-value="3"]`)
|
|
||||||
assert.Equal(t, 1, doneItem.Length())
|
|
||||||
|
|
||||||
comboVal, exists := columnCombo.Find("input.combo-value").Attr("value")
|
|
||||||
assert.True(t, exists)
|
|
||||||
assert.Equal(t, "3", comboVal)
|
|
||||||
|
|
||||||
req = NewRequestWithValues(t, "POST", "/user2/repo1/issues/projects?issue_ids=5", map[string]string{"id": ""})
|
|
||||||
sess.MakeRequest(t, req, http.StatusOK)
|
|
||||||
|
|
||||||
req = NewRequest(t, "GET", "/user2/repo1/issues/4")
|
|
||||||
resp = sess.MakeRequest(t, req, http.StatusOK)
|
|
||||||
htmlDoc = NewHTMLParser(t, resp.Body)
|
|
||||||
|
|
||||||
cards = htmlDoc.Find(".flex-relaxed-list > .item.sidebar-project-card")
|
|
||||||
assert.Equal(t, 0, cards.Length())
|
|
||||||
}
|
|
||||||
|
|
||||||
// getProjectIssueIDs returns the set of issue IDs rendered as cards on the project board page.
|
// getProjectIssueIDs returns the set of issue IDs rendered as cards on the project board page.
|
||||||
func getProjectIssueIDs(t *testing.T, htmlDoc *HTMLDoc) map[int64]struct{} {
|
func getProjectIssueIDs(t *testing.T, htmlDoc *HTMLDoc) map[int64]struct{} {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
@@ -311,9 +189,15 @@ func TestOrgProjectFilterByMilestone(t *testing.T) {
|
|||||||
}
|
}
|
||||||
require.NoError(t, project_model.NewProject(t.Context(), &project))
|
require.NoError(t, project_model.NewProject(t.Context(), &project))
|
||||||
|
|
||||||
|
// Get the default column
|
||||||
|
columns, err := project_model.GetProjectColumns(t.Context(), project.ID, db.ListOptionsAll)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, columns)
|
||||||
|
defaultColumnID := columns[0].ID
|
||||||
|
|
||||||
// Add issues to the project
|
// Add issues to the project
|
||||||
require.NoError(t, issues_model.IssueAssignOrRemoveProject(t.Context(), issue16, user1, []int64{project.ID}))
|
require.NoError(t, issues_model.IssueAssignOrRemoveProject(t.Context(), issue16, user1, project.ID, defaultColumnID))
|
||||||
require.NoError(t, issues_model.IssueAssignOrRemoveProject(t.Context(), issue17, user1, []int64{project.ID}))
|
require.NoError(t, issues_model.IssueAssignOrRemoveProject(t.Context(), issue17, user1, project.ID, defaultColumnID))
|
||||||
|
|
||||||
sess := loginUser(t, "user1")
|
sess := loginUser(t, "user1")
|
||||||
projectURL := fmt.Sprintf("/org3/-/projects/%d", project.ID)
|
projectURL := fmt.Sprintf("/org3/-/projects/%d", project.ID)
|
||||||
|
|||||||
Reference in New Issue
Block a user