mirror of
https://github.com/go-gitea/gitea.git
synced 2026-11-09 17:37:20 -06:00
fix(projects): move only the dragged card on project boards (#40182)
The board reported every card of the target column on each drag, and the server wrote all of them into that column. A stale board in another browser therefore pulled cards moved by someone else back to their old column. The client now sends only the dragged issue and its predecessor, and the server computes the position in one transaction: - Only the dragged issue changes column, the others are just renumbered. - A predecessor that is no longer in the column puts the card at the end. Added an e2e test. Fixes https://github.com/go-gitea/gitea/issues/21243 Fixes https://github.com/go-gitea/gitea/issues/32070 Fixes https://github.com/go-gitea/gitea/issues/13498 --- Assisted-by: Codet:claude --------- Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
@@ -99,10 +99,35 @@ func RemoveIssueFromColumn(ctx context.Context, doer *user_model.User, issue *is
|
||||
return nil
|
||||
}
|
||||
|
||||
// MoveIssuesOnProjectColumn moves or keeps issues in a column and sorts them inside that column
|
||||
func MoveIssuesOnProjectColumn(ctx context.Context, doer *user_model.User, column *project_model.Column, sortedIssueIDs map[int64]int64) error {
|
||||
// MoveIssueAfter places the issue after prevIssueID, first for 0, last when it left the column.
|
||||
func MoveIssueAfter(ctx context.Context, doer *user_model.User, column *project_model.Column, issue *issues_model.Issue, prevIssueID int64) error {
|
||||
moves, err := db.WithTx2(ctx, func(ctx context.Context) ([]columnMove, error) {
|
||||
return moveIssuesOnProjectColumn(ctx, doer, column, sortedIssueIDs)
|
||||
if err := project_model.LockProject(ctx, column.ProjectID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
others, err := project_model.GetColumnIssueSortings(ctx, column, issue.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pos := util.Iif(prevIssueID == 0, 0, len(others))
|
||||
if i := slices.IndexFunc(others, func(row project_model.ColumnIssueSorting) bool { return row.IssueID == prevIssueID }); i >= 0 {
|
||||
pos = i + 1
|
||||
}
|
||||
|
||||
moves, err := moveIssuesOnProjectColumn(ctx, doer, column, map[int64]int64{int64(pos): issue.ID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, other := range others {
|
||||
newSorting := int64(i + util.Iif(i >= pos, 1, 0))
|
||||
if other.Sorting == newSorting {
|
||||
continue
|
||||
}
|
||||
if err := project_model.SetIssueSorting(ctx, column, other.IssueID, newSorting); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return moves, nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -156,7 +156,7 @@ func Test_Projects(t *testing.T) {
|
||||
// move the issue into a non-default column of the first project only
|
||||
targetColumn := &project_model.Column{Title: "target", ProjectID: projects[0].ID}
|
||||
assert.NoError(t, project_model.NewColumn(t.Context(), targetColumn))
|
||||
assert.NoError(t, MoveIssuesOnProjectColumn(t.Context(), user2, targetColumn, map[int64]int64{0: issue11.ID}))
|
||||
assert.NoError(t, MoveIssueAfter(t.Context(), user2, targetColumn, issue11, 0))
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &project_model.ProjectIssue{
|
||||
IssueID: issue11.ID, ProjectID: projects[0].ID, ProjectColumnID: targetColumn.ID,
|
||||
@@ -166,6 +166,44 @@ func Test_Projects(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Moving an issue never moves other cards", func(t *testing.T) {
|
||||
project := &project_model.Project{Title: "incremental move", RepoID: 1, Type: project_model.TypeRepository}
|
||||
assert.NoError(t, project_model.NewProject(t.Context(), project))
|
||||
defer func() {
|
||||
assert.NoError(t, project_model.DeleteProjectByID(t.Context(), project.ID))
|
||||
}()
|
||||
colA := &project_model.Column{Title: "A", ProjectID: project.ID}
|
||||
colB := &project_model.Column{Title: "B", ProjectID: project.ID}
|
||||
assert.NoError(t, project_model.NewColumn(t.Context(), colA))
|
||||
assert.NoError(t, project_model.NewColumn(t.Context(), colB))
|
||||
for i, id := range []int64{1, 2, 3, 5} {
|
||||
assert.NoError(t, db.Insert(t.Context(), &project_model.ProjectIssue{IssueID: id, ProjectID: project.ID, ProjectColumnID: colA.ID, Sorting: int64(i)}))
|
||||
}
|
||||
issue := func(id int64) *issues_model.Issue {
|
||||
return unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: id})
|
||||
}
|
||||
order := func(col *project_model.Column) (ids []int64) {
|
||||
rows, err := project_model.GetColumnIssueSortings(t.Context(), col, 0)
|
||||
assert.NoError(t, err)
|
||||
for i, row := range rows {
|
||||
assert.EqualValues(t, i, row.Sorting)
|
||||
ids = append(ids, row.IssueID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
assert.NoError(t, MoveIssueAfter(t.Context(), user2, colB, issue(1), 0))
|
||||
assert.NoError(t, MoveIssueAfter(t.Context(), user2, colA, issue(2), 1))
|
||||
assert.Equal(t, []int64{3, 5, 2}, order(colA))
|
||||
assert.NoError(t, MoveIssueAfter(t.Context(), user2, colA, issue(5), 0))
|
||||
assert.Equal(t, []int64{5, 3, 2}, order(colA))
|
||||
assert.NoError(t, MoveIssueAfter(t.Context(), user2, colA, issue(5), 3))
|
||||
assert.Equal(t, []int64{3, 5, 2}, order(colA))
|
||||
assert.Equal(t, []int64{1}, order(colB))
|
||||
assert.NoError(t, project_model.SetIssueSorting(t.Context(), colA, 1, 99))
|
||||
assert.Equal(t, []int64{1}, order(colB))
|
||||
})
|
||||
|
||||
t.Run("Repository projects", func(t *testing.T) {
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user