Lunny Xiao 1 week ago
parent
commit
d1282abeaf
No account linked to committer's email address

+ 1
- 0
models/db/engine.go View File

@@ -57,6 +57,7 @@ type Engine interface {
SumInt(bean any, columnName string) (res int64, err error)
Sync(...any) error
Select(string) *xorm.Session
SetExpr(string, any) *xorm.Session
NotIn(string, ...any) *xorm.Session
OrderBy(any, ...any) *xorm.Session
Exist(...any) (bool, error)

+ 26
- 16
models/issues/issue_project.go View File

@@ -90,22 +90,16 @@ func LoadIssuesFromBoardList(ctx context.Context, bs project_model.BoardList) (m
return issuesMap, nil
}

// ChangeProjectAssign changes the project associated with an issue
func ChangeProjectAssign(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()

if err := addUpdateIssueProject(ctx, issue, doer, newProjectID); err != nil {
return err
}

return committer.Commit()
// ChangeProjectAssign changes the project associated with an issue, if newProjectID is 0, the issue is removed from the project
func ChangeProjectAssign(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID, newColumnID int64) error {
return db.WithTx(ctx, func(ctx context.Context) error {
return addUpdateIssueProject(ctx, issue, doer, newProjectID, newColumnID)
})
}

func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error {
// addUpdateIssueProject adds or updates the project the default column associated with an issue
// If newProjectID is 0, the issue is removed from the project
func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID, newColumnID int64) error {
oldProjectID := issue.projectID(ctx)

if err := issue.LoadRepo(ctx); err != nil {
@@ -139,9 +133,25 @@ func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.U
return err
}
}
if newProjectID == 0 || newColumnID == 0 {
return nil
}

var maxSorting int64
if _, err := db.GetEngine(ctx).Select("Max(sorting)").Table("project_issue").
Where("project_id=?", newProjectID).
And("project_board_id=?", newColumnID).
Get(&maxSorting); err != nil {
return err
}
if maxSorting > 0 {
maxSorting++
}

return db.Insert(ctx, &project_model.ProjectIssue{
IssueID: issue.ID,
ProjectID: newProjectID,
IssueID: issue.ID,
ProjectID: newProjectID,
ProjectBoardID: newColumnID,
Sorting: maxSorting,
})
}

+ 33
- 11
models/project/board.go View File

@@ -156,6 +156,15 @@ func NewBoard(ctx context.Context, board *Board) error {
return fmt.Errorf("bad color code: %s", board.Color)
}

var maxSorting int8
if _, err := db.GetEngine(ctx).Select("Max(sorting)").Table("project_board").
Where("project_id=?", board.ProjectID).Get(&maxSorting); err != nil {
return err
}
if maxSorting > 0 {
board.Sorting = maxSorting
}

_, err := db.GetEngine(ctx).Insert(board)
return err
}
@@ -189,7 +198,17 @@ func deleteBoardByID(ctx context.Context, boardID int64) error {
return fmt.Errorf("deleteBoardByID: cannot delete default board")
}

if err = board.removeIssues(ctx); err != nil {
// move all issues to the default column
project, err := GetProjectByID(ctx, board.ProjectID)
if err != nil {
return err
}
defaultBoard, err := project.GetDefaultBoard(ctx)
if err != nil {
return err
}

if err = board.moveIssuesToDefault(ctx, defaultBoard.ID); err != nil {
return err
}

@@ -242,21 +261,15 @@ func UpdateBoard(ctx context.Context, board *Board) error {
// GetBoards fetches all boards related to a project
func (p *Project) GetBoards(ctx context.Context) (BoardList, error) {
boards := make([]*Board, 0, 5)

if err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, false).OrderBy("sorting").Find(&boards); err != nil {
return nil, err
}

defaultB, err := p.getDefaultBoard(ctx)
if err != nil {
if err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting").Find(&boards); err != nil {
return nil, err
}

return append([]*Board{defaultB}, boards...), nil
return boards, nil
}

// getDefaultBoard return default board and ensure only one exists
func (p *Project) getDefaultBoard(ctx context.Context) (*Board, error) {
// GetDefaultBoard return default board and ensure only one exists
func (p *Project) GetDefaultBoard(ctx context.Context) (*Board, error) {
var board Board
has, err := db.GetEngine(ctx).
Where("project_id=? AND `default` = ?", p.ID, true).
@@ -316,3 +329,12 @@ func UpdateBoardSorting(ctx context.Context, bs BoardList) error {
return nil
})
}

func GetColumnsByIDs(ctx context.Context, columnsIDs []int64) (BoardList, error) {
columns := make([]*Board, 0, 5)
if err := db.GetEngine(ctx).In("id", columnsIDs).OrderBy("sorting").Find(&columns); err != nil {
return nil, err
}

return columns, nil
}

+ 30
- 3
models/project/issue.go View File

@@ -17,7 +17,7 @@ type ProjectIssue struct { //revive:disable-line:exported
IssueID int64 `xorm:"INDEX"`
ProjectID int64 `xorm:"INDEX"`

// If 0, then it has not been added to a specific board in the project
// If this should not be zero from 1.22. If it's zero, it will not be displayed on UI and maybe result in errors.
ProjectBoardID int64 `xorm:"INDEX"`

// the sorting order on the board
@@ -102,7 +102,34 @@ func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs
})
}

func (b *Board) removeIssues(ctx context.Context) error {
_, err := db.GetEngine(ctx).Exec("UPDATE `project_issue` SET project_board_id = 0 WHERE project_board_id = ? ", b.ID)
func (b *Board) moveIssuesToDefault(ctx context.Context, defaultBoardID int64) error {
_, err := db.GetEngine(ctx).Exec("UPDATE `project_issue` SET project_board_id = ? WHERE project_board_id = ? ", defaultBoardID, b.ID)
return err
}

// MoveColumnsOnProject moves or keeps issues in a column and sorts them inside that column
func MoveColumnsOnProject(ctx context.Context, project *Project, sortedColumnIDs map[int64]int64) error {
return db.WithTx(ctx, func(ctx context.Context) error {
sess := db.GetEngine(ctx)

columnIDs := make([]int64, 0, len(sortedColumnIDs))
for _, columnID := range sortedColumnIDs {
columnIDs = append(columnIDs, columnID)
}
count, err := sess.Table(new(Board)).Where("project_id=?", project.ID).In("id", columnIDs).Count()
if err != nil {
return err
}
if int(count) != len(sortedColumnIDs) {
return fmt.Errorf("all issues have to be added to a project first")
}

for sorting, columnID := range sortedColumnIDs {
_, err = sess.Exec("UPDATE `project_board` SET sorting=? WHERE id=?", sorting, columnID)
if err != nil {
return err
}
}
return nil
})
}

+ 79
- 1
routers/web/org/projects.go View File

@@ -442,6 +442,21 @@ func UpdateIssueProject(ctx *context.Context) {
}

projectID := ctx.FormInt64("id")
var dstColumnID int64
if projectID > 0 {
dstProject, err := project_model.GetProjectByID(ctx, projectID)
if err != nil {
ctx.ServerError("GetProjectByID", err)
return
}
dstDefaultColumn, err := dstProject.GetDefaultBoard(ctx)
if err != nil {
ctx.ServerError("GetDefaultBoard", err)
return
}
dstColumnID = dstDefaultColumn.ID
}

for _, issue := range issues {
if issue.Project != nil {
if issue.Project.ID == projectID {
@@ -449,7 +464,7 @@ func UpdateIssueProject(ctx *context.Context) {
}
}

if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil {
if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID, dstColumnID); err != nil {
ctx.ServerError("ChangeProjectAssign", err)
return
}
@@ -678,3 +693,66 @@ func MoveIssues(ctx *context.Context) {

ctx.JSONOK()
}

// MoveColumns moves or keeps columns in a project and sorts them inside that project
func MoveColumns(ctx *context.Context) {
if ctx.Doer == nil {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only signed in users are allowed to perform this action.",
})
return
}

project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
return
}
if project.OwnerID != ctx.ContextUser.ID {
ctx.NotFound("InvalidRepoID", nil)
return
}

type movedColumnsForm struct {
Columns []struct {
ColumnID int64 `json:"columnID"`
Sorting int64 `json:"sorting"`
} `json:"columns"`
}

form := &movedColumnsForm{}
if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
ctx.ServerError("DecodeMovedColumnsForm", err)
}

columnIDs := make([]int64, 0, len(form.Columns))
sortedColumnIDs := make(map[int64]int64)
for _, column := range form.Columns {
columnIDs = append(columnIDs, column.ColumnID)
sortedColumnIDs[column.Sorting] = column.ColumnID
}
movedColumns, err := project_model.GetColumnsByIDs(ctx, columnIDs)
if err != nil {
ctx.NotFoundOrServerError("GetColumnsByIDs", issues_model.IsErrIssueNotExist, err)
return
}

if len(movedColumns) != len(form.Columns) {
ctx.ServerError("some columns do not exist", errors.New("some columns do not exist"))
return
}

for _, column := range movedColumns {
if column.ProjectID != project.ID {
ctx.ServerError("Some column's projectID is not equal to project's ID", errors.New("Some column's projectID is not equal to project's ID"))
return
}
}

if err = project_model.MoveColumnsOnProject(ctx, project, sortedColumnIDs); err != nil {
ctx.ServerError("MoveColumnsOnProject", err)
return
}

ctx.JSONOK()
}

+ 83
- 1
routers/web/repo/projects.go View File

@@ -385,6 +385,21 @@ func UpdateIssueProject(ctx *context.Context) {
}

projectID := ctx.FormInt64("id")
var dstColumnID int64
if projectID > 0 {
dstProject, err := project_model.GetProjectByID(ctx, projectID)
if err != nil {
ctx.ServerError("GetProjectByID", err)
return
}
dstDefaultColumn, err := dstProject.GetDefaultBoard(ctx)
if err != nil {
ctx.ServerError("GetDefaultBoard", err)
return
}
dstColumnID = dstDefaultColumn.ID
}

for _, issue := range issues {
if issue.Project != nil {
if issue.Project.ID == projectID {
@@ -392,7 +407,7 @@ func UpdateIssueProject(ctx *context.Context) {
}
}

if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil {
if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID, dstColumnID); err != nil {
ctx.ServerError("ChangeProjectAssign", err)
return
}
@@ -666,3 +681,70 @@ func MoveIssues(ctx *context.Context) {

ctx.JSONOK()
}

// MoveColumns moves or keeps columns in a project and sorts them inside that project
func MoveColumns(ctx *context.Context) {
if ctx.Doer == nil {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only signed in users are allowed to perform this action.",
})
return
}

project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
if project_model.IsErrProjectNotExist(err) {
ctx.NotFound("ProjectNotExist", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return
}
if project.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound("InvalidRepoID", nil)
return
}

type movedColumnsForm struct {
Columns []struct {
ColumnID int64 `json:"columnID"`
Sorting int64 `json:"sorting"`
} `json:"columns"`
}

form := &movedColumnsForm{}
if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
ctx.ServerError("DecodeMovedColumnsForm", err)
}

columnIDs := make([]int64, 0, len(form.Columns))
sortedColumnIDs := make(map[int64]int64)
for _, column := range form.Columns {
columnIDs = append(columnIDs, column.ColumnID)
sortedColumnIDs[column.Sorting] = column.ColumnID
}
movedColumns, err := project_model.GetColumnsByIDs(ctx, columnIDs)
if err != nil {
ctx.NotFoundOrServerError("GetColumnsByIDs", issues_model.IsErrIssueNotExist, err)
return
}

if len(movedColumns) != len(form.Columns) {
ctx.ServerError("some columns do not exist", errors.New("some columns do not exist"))
return
}

for _, column := range movedColumns {
if column.ProjectID != project.ID {
ctx.ServerError("Some column's projectID is not equal to project's ID", errors.New("Some column's projectID is not equal to project's ID"))
return
}
}

if err = project_model.MoveColumnsOnProject(ctx, project, sortedColumnIDs); err != nil {
ctx.ServerError("MoveColumnsOnProject", err)
return
}

ctx.JSONOK()
}

+ 13
- 2
routers/web/repo/pull.go View File

@@ -20,6 +20,7 @@ import (
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
project_model "code.gitea.io/gitea/models/project"
pull_model "code.gitea.io/gitea/models/pull"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
@@ -1331,10 +1332,20 @@ func CompareAndPullRequestPost(ctx *context.Context) {

if projectID > 0 {
if !ctx.Repo.CanWrite(unit.TypeProjects) {
ctx.Error(http.StatusBadRequest, "user hasn't the permission to write to projects")
log.Error("user hasn't the permission to write to projects")
return
}
if err := issues_model.ChangeProjectAssign(ctx, pullIssue, ctx.Doer, projectID); err != nil {
dstProject, err := project_model.GetProjectByID(ctx, projectID)
if err != nil {
ctx.ServerError("GetProjectByID", err)
return
}
dstDefaultColumn, err := dstProject.GetDefaultBoard(ctx)
if err != nil {
ctx.ServerError("GetDefaultBoard", err)
return
}
if err := issues_model.ChangeProjectAssign(ctx, pullIssue, ctx.Doer, projectID, dstDefaultColumn.ID); err != nil {
ctx.ServerError("ChangeProjectAssign", err)
return
}

+ 2
- 0
routers/web/web.go View File

@@ -999,6 +999,7 @@ func registerRoutes(m *web.Route) {
m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost)
m.Group("/{id}", func() {
m.Post("", web.Bind(forms.EditProjectBoardForm{}), org.AddBoardToProjectPost)
m.Post("/move", org.MoveColumns)
m.Post("/delete", org.DeleteProject)

m.Get("/edit", org.RenderEditProject)
@@ -1354,6 +1355,7 @@ func registerRoutes(m *web.Route) {
m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost)
m.Group("/{id}", func() {
m.Post("", web.Bind(forms.EditProjectBoardForm{}), repo.AddBoardToProjectPost)
m.Post("/move", repo.MoveColumns)
m.Post("/delete", repo.DeleteProject)

m.Get("/edit", repo.RenderEditProject)

+ 9
- 1
services/issue/issue.go View File

@@ -42,7 +42,15 @@ 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 {
project, err := project_model.GetProjectByID(ctx, projectID)
if err != nil {
return err
}
defaultBoard, err := project.GetDefaultBoard(ctx)
if err != nil {
return err
}
if err := issues_model.ChangeProjectAssign(ctx, issue, issue.Poster, projectID, defaultBoard.ID); err != nil {
return err
}
}

+ 2
- 2
templates/projects/view.tmpl View File

@@ -64,7 +64,7 @@
</div>

<div id="project-board">
<div class="board {{if .CanWriteProjects}}sortable{{end}}">
<div class="board {{if .CanWriteProjects}}sortable{{end}}"{{if .CanWriteProjects}} data-url="{{$.Link}}/move"{{end}}>
{{range .Columns}}
<div class="ui segment project-column"{{if .Color}} style="background: {{.Color}} !important; color: {{ContrastColor .Color}} !important"{{end}} data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}">
<div class="project-column-header{{if $canWriteProject}} tw-cursor-grab{{end}}">
@@ -90,7 +90,7 @@
data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.set_default"}}"
data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.set_default_desc"}}"
data-url="{{$.Link}}/{{.ID}}/default">
{{svg "octicon-pin"}}
{{svg "octicon-star"}}
{{ctx.Locale.Tr "repo.projects.column.set_default"}}
</a>
<a class="item show-modal button show-delete-project-column-modal"

+ 14
- 12
web_src/js/features/repo-projects.js View File

@@ -2,7 +2,6 @@ import $ from 'jquery';
import {contrastColor} from '../utils/color.js';
import {createSortable} from '../modules/sortable.js';
import {POST, DELETE, PUT} from '../modules/fetch.js';
import tinycolor from 'tinycolor2';

function updateIssueCount(cards) {
const parent = cards.parentElement;
@@ -63,17 +62,20 @@ async function initRepoProjectSortable() {
delay: 500,
onSort: async () => {
boardColumns = mainBoard.getElementsByClassName('project-column');
for (let i = 0; i < boardColumns.length; i++) {
const column = boardColumns[i];
if (parseInt(column.getAttribute('data-sorting')) !== i) {
try {
const bgColor = column.style.backgroundColor; // will be rgb() string
const color = bgColor ? tinycolor(bgColor).toHexString() : '';
await PUT(column.getAttribute('data-url'), {data: {sorting: i, color}});
} catch (error) {
console.error(error);
}
}

const columnSorting = {
columns: Array.from(boardColumns, (column, i) => ({
columnID: parseInt(column.getAttribute('data-id')),
sorting: i,
})),
};

try {
await POST(mainBoard.getAttribute('data-url'), {
data: columnSorting,
});
} catch (error) {
console.error(error);
}
},
});

Loading…
Cancel
Save