Skip to content

Commit 084bec8

Browse files
GiteaBotlunnysilverwindwxiaoguang
authored
Fix various problems around projects board view (#30696) (#30902)
Backport #30696 by @lunny # The problem The previous implementation will start multiple POST requests from the frontend when moving a column and another bug is moving the default column will never be remembered in fact. # What's changed - [x] This PR will allow the default column to move to a non-first position - [x] And it also uses one request instead of multiple requests when moving the columns - [x] Use a star instead of a pin as the icon for setting the default column action - [x] Inserted new column will be append to the end - [x] Fix #30701 the newly added issue will be append to the end of the default column - [x] Fix when deleting a column, all issues in it will be displayed from UI but database records exist. - [x] Add a limitation for columns in a project to 20. So the sorting will not be overflow because it's int8. Co-authored-by: Lunny Xiao <[email protected]> Co-authored-by: silverwind <[email protected]> Co-authored-by: wxiaoguang <[email protected]>
1 parent 271e874 commit 084bec8

File tree

16 files changed

+430
-167
lines changed

16 files changed

+430
-167
lines changed

models/db/engine.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ type Engine interface {
5757
SumInt(bean any, columnName string) (res int64, err error)
5858
Sync(...any) error
5959
Select(string) *xorm.Session
60+
SetExpr(string, any) *xorm.Session
6061
NotIn(string, ...any) *xorm.Session
6162
OrderBy(any, ...any) *xorm.Session
6263
Exist(...any) (bool, error)

models/issues/issue_project.go

Lines changed: 60 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ package issues
55

66
import (
77
"context"
8-
"fmt"
98

109
"code.gitea.io/gitea/models/db"
1110
project_model "code.gitea.io/gitea/models/project"
1211
user_model "code.gitea.io/gitea/models/user"
12+
"code.gitea.io/gitea/modules/util"
1313
)
1414

1515
// LoadProject load the project the issue was assigned to
@@ -90,58 +90,73 @@ func LoadIssuesFromBoardList(ctx context.Context, bs project_model.BoardList) (m
9090
return issuesMap, nil
9191
}
9292

93-
// ChangeProjectAssign changes the project associated with an issue
94-
func ChangeProjectAssign(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error {
95-
ctx, committer, err := db.TxContext(ctx)
96-
if err != nil {
97-
return err
98-
}
99-
defer committer.Close()
100-
101-
if err := addUpdateIssueProject(ctx, issue, doer, newProjectID); err != nil {
102-
return err
103-
}
104-
105-
return committer.Commit()
106-
}
93+
// IssueAssignOrRemoveProject changes the project associated with an issue
94+
// If newProjectID is 0, the issue is removed from the project
95+
func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID, newColumnID int64) error {
96+
return db.WithTx(ctx, func(ctx context.Context) error {
97+
oldProjectID := issue.projectID(ctx)
10798

108-
func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error {
109-
oldProjectID := issue.projectID(ctx)
99+
if err := issue.LoadRepo(ctx); err != nil {
100+
return err
101+
}
110102

111-
if err := issue.LoadRepo(ctx); err != nil {
112-
return err
113-
}
103+
// Only check if we add a new project and not remove it.
104+
if newProjectID > 0 {
105+
newProject, err := project_model.GetProjectByID(ctx, newProjectID)
106+
if err != nil {
107+
return err
108+
}
109+
if !newProject.CanBeAccessedByOwnerRepo(issue.Repo.OwnerID, issue.Repo) {
110+
return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID)
111+
}
112+
if newColumnID == 0 {
113+
newDefaultColumn, err := newProject.GetDefaultBoard(ctx)
114+
if err != nil {
115+
return err
116+
}
117+
newColumnID = newDefaultColumn.ID
118+
}
119+
}
114120

115-
// Only check if we add a new project and not remove it.
116-
if newProjectID > 0 {
117-
newProject, err := project_model.GetProjectByID(ctx, newProjectID)
118-
if err != nil {
121+
if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil {
119122
return err
120123
}
121-
if newProject.RepoID != issue.RepoID && newProject.OwnerID != issue.Repo.OwnerID {
122-
return fmt.Errorf("issue's repository is not the same as project's repository")
123-
}
124-
}
125124

126-
if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil {
127-
return err
128-
}
125+
if oldProjectID > 0 || newProjectID > 0 {
126+
if _, err := CreateComment(ctx, &CreateCommentOptions{
127+
Type: CommentTypeProject,
128+
Doer: doer,
129+
Repo: issue.Repo,
130+
Issue: issue,
131+
OldProjectID: oldProjectID,
132+
ProjectID: newProjectID,
133+
}); err != nil {
134+
return err
135+
}
136+
}
137+
if newProjectID == 0 {
138+
return nil
139+
}
140+
if newColumnID == 0 {
141+
panic("newColumnID must not be zero") // shouldn't happen
142+
}
129143

130-
if oldProjectID > 0 || newProjectID > 0 {
131-
if _, err := CreateComment(ctx, &CreateCommentOptions{
132-
Type: CommentTypeProject,
133-
Doer: doer,
134-
Repo: issue.Repo,
135-
Issue: issue,
136-
OldProjectID: oldProjectID,
137-
ProjectID: newProjectID,
138-
}); err != nil {
144+
res := struct {
145+
MaxSorting int64
146+
IssueCount int64
147+
}{}
148+
if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as issue_count").Table("project_issue").
149+
Where("project_id=?", newProjectID).
150+
And("project_board_id=?", newColumnID).
151+
Get(&res); err != nil {
139152
return err
140153
}
141-
}
142-
143-
return db.Insert(ctx, &project_model.ProjectIssue{
144-
IssueID: issue.ID,
145-
ProjectID: newProjectID,
154+
newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0)
155+
return db.Insert(ctx, &project_model.ProjectIssue{
156+
IssueID: issue.ID,
157+
ProjectID: newProjectID,
158+
ProjectBoardID: newColumnID,
159+
Sorting: newSorting,
160+
})
146161
})
147162
}

models/project/board.go

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ package project
55

66
import (
77
"context"
8+
"errors"
89
"fmt"
910
"regexp"
1011

1112
"code.gitea.io/gitea/models/db"
1213
"code.gitea.io/gitea/modules/setting"
1314
"code.gitea.io/gitea/modules/timeutil"
15+
"code.gitea.io/gitea/modules/util"
1416

1517
"xorm.io/builder"
1618
)
@@ -82,6 +84,17 @@ func (b *Board) NumIssues(ctx context.Context) int {
8284
return int(c)
8385
}
8486

87+
func (b *Board) GetIssues(ctx context.Context) ([]*ProjectIssue, error) {
88+
issues := make([]*ProjectIssue, 0, 5)
89+
if err := db.GetEngine(ctx).Where("project_id=?", b.ProjectID).
90+
And("project_board_id=?", b.ID).
91+
OrderBy("sorting, id").
92+
Find(&issues); err != nil {
93+
return nil, err
94+
}
95+
return issues, nil
96+
}
97+
8598
func init() {
8699
db.RegisterModel(new(Board))
87100
}
@@ -150,12 +163,27 @@ func createBoardsForProjectsType(ctx context.Context, project *Project) error {
150163
return db.Insert(ctx, boards)
151164
}
152165

166+
// maxProjectColumns max columns allowed in a project, this should not bigger than 127
167+
// because sorting is int8 in database
168+
const maxProjectColumns = 20
169+
153170
// NewBoard adds a new project board to a given project
154171
func NewBoard(ctx context.Context, board *Board) error {
155172
if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) {
156173
return fmt.Errorf("bad color code: %s", board.Color)
157174
}
158-
175+
res := struct {
176+
MaxSorting int64
177+
ColumnCount int64
178+
}{}
179+
if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as column_count").Table("project_board").
180+
Where("project_id=?", board.ProjectID).Get(&res); err != nil {
181+
return err
182+
}
183+
if res.ColumnCount >= maxProjectColumns {
184+
return fmt.Errorf("NewBoard: maximum number of columns reached")
185+
}
186+
board.Sorting = int8(util.Iif(res.ColumnCount > 0, res.MaxSorting+1, 0))
159187
_, err := db.GetEngine(ctx).Insert(board)
160188
return err
161189
}
@@ -189,7 +217,17 @@ func deleteBoardByID(ctx context.Context, boardID int64) error {
189217
return fmt.Errorf("deleteBoardByID: cannot delete default board")
190218
}
191219

192-
if err = board.removeIssues(ctx); err != nil {
220+
// move all issues to the default column
221+
project, err := GetProjectByID(ctx, board.ProjectID)
222+
if err != nil {
223+
return err
224+
}
225+
defaultColumn, err := project.GetDefaultBoard(ctx)
226+
if err != nil {
227+
return err
228+
}
229+
230+
if err = board.moveIssuesToAnotherColumn(ctx, defaultColumn); err != nil {
193231
return err
194232
}
195233

@@ -242,21 +280,15 @@ func UpdateBoard(ctx context.Context, board *Board) error {
242280
// GetBoards fetches all boards related to a project
243281
func (p *Project) GetBoards(ctx context.Context) (BoardList, error) {
244282
boards := make([]*Board, 0, 5)
245-
246-
if err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, false).OrderBy("sorting").Find(&boards); err != nil {
283+
if err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Find(&boards); err != nil {
247284
return nil, err
248285
}
249286

250-
defaultB, err := p.getDefaultBoard(ctx)
251-
if err != nil {
252-
return nil, err
253-
}
254-
255-
return append([]*Board{defaultB}, boards...), nil
287+
return boards, nil
256288
}
257289

258-
// getDefaultBoard return default board and ensure only one exists
259-
func (p *Project) getDefaultBoard(ctx context.Context) (*Board, error) {
290+
// GetDefaultBoard return default board and ensure only one exists
291+
func (p *Project) GetDefaultBoard(ctx context.Context) (*Board, error) {
260292
var board Board
261293
has, err := db.GetEngine(ctx).
262294
Where("project_id=? AND `default` = ?", p.ID, true).
@@ -316,3 +348,42 @@ func UpdateBoardSorting(ctx context.Context, bs BoardList) error {
316348
return nil
317349
})
318350
}
351+
352+
func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (BoardList, error) {
353+
columns := make([]*Board, 0, 5)
354+
if err := db.GetEngine(ctx).
355+
Where("project_id =?", projectID).
356+
In("id", columnsIDs).
357+
OrderBy("sorting").Find(&columns); err != nil {
358+
return nil, err
359+
}
360+
return columns, nil
361+
}
362+
363+
// MoveColumnsOnProject sorts columns in a project
364+
func MoveColumnsOnProject(ctx context.Context, project *Project, sortedColumnIDs map[int64]int64) error {
365+
return db.WithTx(ctx, func(ctx context.Context) error {
366+
sess := db.GetEngine(ctx)
367+
columnIDs := util.ValuesOfMap(sortedColumnIDs)
368+
movedColumns, err := GetColumnsByIDs(ctx, project.ID, columnIDs)
369+
if err != nil {
370+
return err
371+
}
372+
if len(movedColumns) != len(sortedColumnIDs) {
373+
return errors.New("some columns do not exist")
374+
}
375+
376+
for _, column := range movedColumns {
377+
if column.ProjectID != project.ID {
378+
return fmt.Errorf("column[%d]'s projectID is not equal to project's ID [%d]", column.ProjectID, project.ID)
379+
}
380+
}
381+
382+
for sorting, columnID := range sortedColumnIDs {
383+
if _, err := sess.Exec("UPDATE `project_board` SET sorting=? WHERE id=?", sorting, columnID); err != nil {
384+
return err
385+
}
386+
}
387+
return nil
388+
})
389+
}

0 commit comments

Comments
 (0)