From 70b367341bcfe6973ef841716c2e0c45566a2772 Mon Sep 17 00:00:00 2001 From: iminfinity Date: Fri, 12 Jan 2024 00:42:54 +0530 Subject: [PATCH 1/3] add close on move in create oard, and close issue on move --- models/project/board.go | 11 ++++++----- models/project/issue.go | 3 ++- routers/web/org/projects.go | 24 ++++++++++++++++++++---- routers/web/repo/projects.go | 25 +++++++++++++++++++++---- services/forms/repo_form.go | 1 + services/issue/project.go | 28 ++++++++++++++++++++++++++++ templates/projects/view.tmpl | 11 ++++++++++- web_src/js/features/repo-projects.js | 9 +++++---- 8 files changed, 93 insertions(+), 19 deletions(-) create mode 100644 services/issue/project.go diff --git a/models/project/board.go b/models/project/board.go index 3e2d8e0472c51..9c5859b390ee4 100644 --- a/models/project/board.go +++ b/models/project/board.go @@ -50,11 +50,12 @@ var BoardColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$") // Board is used to represent boards on a project type Board struct { - ID int64 `xorm:"pk autoincr"` - Title string - Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific board will be assigned to this board - Sorting int8 `xorm:"NOT NULL DEFAULT 0"` - Color string `xorm:"VARCHAR(7)"` + ID int64 `xorm:"pk autoincr"` + Title string + Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific board will be assigned to this board + Sorting int8 `xorm:"NOT NULL DEFAULT 0"` + Color string `xorm:"VARCHAR(7)"` + CloseIssueOnMove bool `xorm:"DEFAULT false"` ProjectID int64 `xorm:"INDEX NOT NULL"` CreatorID int64 `xorm:"NOT NULL"` diff --git a/models/project/issue.go b/models/project/issue.go index ebc9719de55d0..50428a735f2b1 100644 --- a/models/project/issue.go +++ b/models/project/issue.go @@ -21,7 +21,8 @@ type ProjectIssue struct { //revive:disable-line:exported ProjectBoardID int64 `xorm:"INDEX"` // the sorting order on the board - Sorting int64 `xorm:"NOT NULL DEFAULT 0"` + Sorting int64 `xorm:"NOT NULL DEFAULT 0"` + CloseIssueOnMove bool `xorm:"DEFAULT false"` } func init() { diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 03798a712c2ee..d9a6e7ce3c508 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -24,6 +24,7 @@ import ( "code.gitea.io/gitea/modules/web" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/forms" + issue_service "code.gitea.io/gitea/services/issue" ) const ( @@ -542,10 +543,11 @@ func AddBoardToProjectPost(ctx *context.Context) { } if err := project_model.NewBoard(ctx, &project_model.Board{ - ProjectID: project.ID, - Title: form.Title, - Color: form.Color, - CreatorID: ctx.Doer.ID, + ProjectID: project.ID, + Title: form.Title, + Color: form.Color, + CreatorID: ctx.Doer.ID, + CloseIssueOnMove: form.Close, }); err != nil { ctx.ServerError("NewProjectBoard", err) return @@ -747,5 +749,19 @@ func MoveIssues(ctx *context.Context) { return } + if board.CloseIssueOnMove { + issueID, err := strconv.ParseInt(ctx.FormString("issueID"), 10, 64) + if err != nil { + ctx.ServerError("moved issueID is required", err) + return + } + + err = issue_service.CloseIssue(ctx, issueID) + if err != nil { + ctx.ServerError("MoveIssuesOnProjectBoard unable to close issue", err) + return + } + } + ctx.JSONOK() } diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 5694575b468de..c9baca0993178 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -8,6 +8,7 @@ import ( "fmt" "net/http" "net/url" + "strconv" "strings" "code.gitea.io/gitea/models/db" @@ -25,6 +26,7 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/forms" + issue_service "code.gitea.io/gitea/services/issue" ) const ( @@ -479,10 +481,11 @@ func AddBoardToProjectPost(ctx *context.Context) { } if err := project_model.NewBoard(ctx, &project_model.Board{ - ProjectID: project.ID, - Title: form.Title, - Color: form.Color, - CreatorID: ctx.Doer.ID, + ProjectID: project.ID, + Title: form.Title, + Color: form.Color, + CreatorID: ctx.Doer.ID, + CloseIssueOnMove: form.Close, }); err != nil { ctx.ServerError("NewProjectBoard", err) return @@ -692,5 +695,19 @@ func MoveIssues(ctx *context.Context) { return } + if board.CloseIssueOnMove { + issueID, err := strconv.ParseInt(ctx.FormString("issueID"), 10, 64) + if err != nil { + ctx.ServerError("moved issueID is required", err) + return + } + + err = issue_service.CloseIssue(ctx, issueID) + if err != nil { + ctx.ServerError("MoveIssuesOnProjectBoard unable to close issue", err) + return + } + } + ctx.JSONOK() } diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 86599000a5882..9cf5758f8267a 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -538,6 +538,7 @@ type EditProjectBoardForm struct { Title string `binding:"Required;MaxSize(100)"` Sorting int8 Color string `binding:"MaxSize(7)"` + Close bool } // _____ .__.__ __ diff --git a/services/issue/project.go b/services/issue/project.go new file mode 100644 index 0000000000000..b2a2df1b6bcfb --- /dev/null +++ b/services/issue/project.go @@ -0,0 +1,28 @@ +package issue + +import ( + "errors" + + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" +) + +func CloseIssue(ctx *context.Context, issueID int64) error { + + issue, err := issues_model.GetIssueByID(ctx, issueID) + if err != nil { + return errors.New("failed getting issue") + } + + if err := ChangeStatus(ctx, issue, ctx.Doer, "", true); err != nil { + log.Error("ChangeStatus: %v", err) + + if issues_model.IsErrDependenciesLeft(err) { + ctx.JSONError(ctx.Tr("repo.issues.dependency.issue_close_blocked")) + return err + } + } + + return nil +} diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index b3ad03c354988..3f31de5eba624 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -48,6 +48,15 @@ +
+ +
+ +
+
+
@@ -128,7 +137,7 @@ {{template "repo/issue/label_precolors"}}
- +
diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index 5a2a7e72ef335..7a61e23d933fd 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -11,10 +11,10 @@ function updateIssueCount(cards) { parent.getElementsByClassName('project-column-issue-count')[0].textContent = cnt; } -function createNewColumn(url, columnTitle, projectColorInput) { +function createNewColumn(url, columnTitle, projectColorInput, closeOnIssueMove) { $.ajax({ url, - data: JSON.stringify({title: columnTitle.val(), color: projectColorInput.val()}), + data: JSON.stringify({title: columnTitle.val(), color: projectColorInput.val(),close: closeOnIssueMove.prop('checked')}), headers: { 'X-Csrf-Token': csrfToken, }, @@ -39,7 +39,7 @@ function moveIssue({item, from, to, oldIndex}) { }; $.ajax({ - url: `${to.getAttribute('data-url')}/move`, + url: `${to.getAttribute('data-url')}/move?issueID=${item.getAttribute('data-issue')}`, data: JSON.stringify(columnSorting), headers: { 'X-Csrf-Token': csrfToken, @@ -187,11 +187,12 @@ export function initRepoProject() { e.preventDefault(); const columnTitle = $('#new_project_column'); const projectColorInput = $('#new_project_column_color_picker'); + const closeOnIssueMove = $('#new_project_column_project_close_on_move'); if (!columnTitle.val()) { return; } const url = $(this).data('url'); - createNewColumn(url, columnTitle, projectColorInput); + createNewColumn(url, columnTitle, projectColorInput, closeOnIssueMove); }); } From 347ffa3e798e18ec7785d9c7b08b651d6d65c1d1 Mon Sep 17 00:00:00 2001 From: iminfinity Date: Mon, 15 Jan 2024 17:27:36 +0530 Subject: [PATCH 2/3] add cron a weekly cron to remove closed issues from boards with close-on-move flag true --- options/locale/locale_en-US.ini | 1 + services/actions/projects.go | 125 ++++++++++++++++++++++++++++++++ services/cron/tasks_basic.go | 15 ++++ 3 files changed, 141 insertions(+) create mode 100644 services/actions/projects.go diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 3c564f518e5db..c42b9ab08c829 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2751,6 +2751,7 @@ dashboard.sync_external_users = Synchronize external user data dashboard.cleanup_hook_task_table = Cleanup hook_task table dashboard.cleanup_packages = Cleanup expired packages dashboard.cleanup_actions = Cleanup actions expired logs and artifacts +dashboard.project_board_actions = Cleanup project closed issues dashboard.server_uptime = Server Uptime dashboard.current_goroutine = Current Goroutines dashboard.current_memory_usage = Current Memory Usage diff --git a/services/actions/projects.go b/services/actions/projects.go new file mode 100644 index 0000000000000..f205b076b7884 --- /dev/null +++ b/services/actions/projects.go @@ -0,0 +1,125 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "fmt" + "time" + + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + project_model "code.gitea.io/gitea/models/project" + "code.gitea.io/gitea/modules/log" + "xorm.io/builder" +) + +// Cleanup removes closed issues from project board with close on move flag set to true +func CleanupProjectIssues(taskCtx context.Context, olderThan time.Duration) error { + log.Info("CRON: remove closed issues from boards with close on move flag set to true starting...") + + projects, err := getAllProjects(taskCtx) + + if err != nil { + return err + } + for _, project := range projects { + boards, err := getAllProjectBoardWithCloseOnMove(taskCtx, project) + if err != nil { + log.Error("Cannot get boards of project ID %d: %v", project.ID, err) + continue + } + log.Info("Found %d boards with close on move true", len(boards)) + for _, board := range boards { + issues, err := getAllIssuesOfBoard(taskCtx, board) + if err != nil { + log.Error("Cannot get issues of board ID %d: %v", board.ID, err) + continue + } + issuesToBeRemoved, err := getAllIssuesToBeRemoved(taskCtx, issues) + if err != nil { + log.Error("Cannot get issues of to be removed of board ID %d: %v", board.ID, err) + continue + } + for _, issueToBeRemoved := range issuesToBeRemoved { + err = removeIssueFromProject(taskCtx, issueToBeRemoved, project) + if err != nil { + log.Error("Cannot remove issue ID %d from board ID %d: %v", issueToBeRemoved.ID, board.ID, err) + continue + } + log.Info("Removed issue ID %d from board ID %d", issueToBeRemoved.ID, board.ID) + } + log.Info("completed removing closed issues from board ID %d", board.ID) + } + log.Info("completed removing closed issues project ID %d", project.ID) + } + + log.Info("CRON: remove closed issues from boards with close on move flag true completed.") + + return nil +} + +func getAllProjects(ctx context.Context) ([]project_model.Project, error) { + + var projects []project_model.Project + + err := db.GetEngine(ctx).Table("project").Select("*").Find(&projects) + if err != nil { + fmt.Println(err) + return projects, err + } + return projects, nil +} + +func getAllProjectBoardWithCloseOnMove(ctx context.Context, project project_model.Project) ([]project_model.Board, error) { + var boards []project_model.Board + + err := db.GetEngine(ctx).Table("project_board").Select("*").Where(builder.Eq{"project_id": project.ID}).Find(&boards) + if err != nil { + fmt.Println(err) + return boards, err + } + return boards, nil +} + +func getAllIssuesOfBoard(ctx context.Context, board project_model.Board) ([]int64, error) { + var issueIDs []int64 + + err := db.GetEngine(ctx).Table("project_issue").Select("issue_id").Where(builder.Eq{"project_id": board.ProjectID, "project_board_id": board.ID}).Find(&issueIDs) + if err != nil { + fmt.Println(err) + return issueIDs, err + } + return issueIDs, nil +} + +func getAllIssuesToBeRemoved(ctx context.Context, issueIDs []int64) ([]issues_model.Issue, error) { + + var issues []issues_model.Issue + + err := db.GetEngine(ctx).Table("issue").Select("*").Where(builder.Eq{"is_closed": 1}).Where(builder.In("id", issueIDs)).Find(&issues) + if err != nil { + fmt.Println(err) + return issues, err + } + + return issues, nil +} + +func removeIssueFromProject(ctx context.Context, issue issues_model.Issue, project project_model.Project) error { + + project_issue := &project_model.ProjectIssue{ + IssueID: issue.ID, + ProjectID: project.ID, + } + + _, err := db.GetEngine(ctx).Table("project_issue").Delete(&project_issue) + if err != nil { + fmt.Println(err) + return err + } + + return nil + +} diff --git a/services/cron/tasks_basic.go b/services/cron/tasks_basic.go index 3869382d22363..8e4e5025a3550 100644 --- a/services/cron/tasks_basic.go +++ b/services/cron/tasks_basic.go @@ -171,6 +171,20 @@ func registerActionsCleanup() { }) } +func registerProjectsIssuesCleanup() { + RegisterTaskFatal("project_board_actions", &OlderThanConfig{ + BaseConfig: BaseConfig{ + Enabled: true, + RunAtStart: true, + Schedule: "@weekly", + }, + OlderThan: 24 * time.Hour, + }, func(ctx context.Context, _ *user_model.User, config Config) error { + realConfig := config.(*OlderThanConfig) + return actions.CleanupProjectIssues(ctx, realConfig.OlderThan) + }) +} + func initBasicTasks() { if setting.Mirror.Enabled { registerUpdateMirrorTask() @@ -190,4 +204,5 @@ func initBasicTasks() { if setting.Actions.Enabled { registerActionsCleanup() } + registerProjectsIssuesCleanup() } From b4bb836dc8af43c33ed22b44f2b30ae8a7150319 Mon Sep 17 00:00:00 2001 From: iminfinity Date: Thu, 25 Jan 2024 18:52:59 +0530 Subject: [PATCH 3/3] fix lint --- services/actions/projects.go | 20 +++++++------------- services/issue/project.go | 4 +++- templates/projects/view.tmpl | 4 ++-- web_src/js/features/repo-projects.js | 2 +- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/services/actions/projects.go b/services/actions/projects.go index f205b076b7884..973027093343f 100644 --- a/services/actions/projects.go +++ b/services/actions/projects.go @@ -5,7 +5,6 @@ package actions import ( "context" - "fmt" "time" "code.gitea.io/gitea/models/db" @@ -20,7 +19,6 @@ func CleanupProjectIssues(taskCtx context.Context, olderThan time.Duration) erro log.Info("CRON: remove closed issues from boards with close on move flag set to true starting...") projects, err := getAllProjects(taskCtx) - if err != nil { return err } @@ -61,12 +59,11 @@ func CleanupProjectIssues(taskCtx context.Context, olderThan time.Duration) erro } func getAllProjects(ctx context.Context) ([]project_model.Project, error) { - var projects []project_model.Project err := db.GetEngine(ctx).Table("project").Select("*").Find(&projects) if err != nil { - fmt.Println(err) + log.Error("unable to read project db %v", err) return projects, err } return projects, nil @@ -77,7 +74,7 @@ func getAllProjectBoardWithCloseOnMove(ctx context.Context, project project_mode err := db.GetEngine(ctx).Table("project_board").Select("*").Where(builder.Eq{"project_id": project.ID}).Find(&boards) if err != nil { - fmt.Println(err) + log.Error("unable to read project_board db %v", err) return boards, err } return boards, nil @@ -88,19 +85,18 @@ func getAllIssuesOfBoard(ctx context.Context, board project_model.Board) ([]int6 err := db.GetEngine(ctx).Table("project_issue").Select("issue_id").Where(builder.Eq{"project_id": board.ProjectID, "project_board_id": board.ID}).Find(&issueIDs) if err != nil { - fmt.Println(err) + log.Error("unable to read project_issue db %v", err) return issueIDs, err } return issueIDs, nil } func getAllIssuesToBeRemoved(ctx context.Context, issueIDs []int64) ([]issues_model.Issue, error) { - var issues []issues_model.Issue err := db.GetEngine(ctx).Table("issue").Select("*").Where(builder.Eq{"is_closed": 1}).Where(builder.In("id", issueIDs)).Find(&issues) if err != nil { - fmt.Println(err) + log.Error("unable to read issue db %v", err) return issues, err } @@ -108,18 +104,16 @@ func getAllIssuesToBeRemoved(ctx context.Context, issueIDs []int64) ([]issues_mo } func removeIssueFromProject(ctx context.Context, issue issues_model.Issue, project project_model.Project) error { - - project_issue := &project_model.ProjectIssue{ + projectIssue := &project_model.ProjectIssue{ IssueID: issue.ID, ProjectID: project.ID, } - _, err := db.GetEngine(ctx).Table("project_issue").Delete(&project_issue) + _, err := db.GetEngine(ctx).Table("project_issue").Delete(&projectIssue) if err != nil { - fmt.Println(err) + log.Error("unable to delete project_issue db %v", err) return err } return nil - } diff --git a/services/issue/project.go b/services/issue/project.go index b2a2df1b6bcfb..6a8d05ccda5d5 100644 --- a/services/issue/project.go +++ b/services/issue/project.go @@ -1,3 +1,6 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package issue import ( @@ -9,7 +12,6 @@ import ( ) func CloseIssue(ctx *context.Context, issueID int64) error { - issue, err := issues_model.GetIssueByID(ctx, issueID) if err != nil { return errors.New("failed getting issue") diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 3f31de5eba624..8d3bfc12bd733 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -51,7 +51,7 @@
-
@@ -137,7 +137,7 @@ {{template "repo/issue/label_precolors"}}
- +
diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index 7a61e23d933fd..971eece6a9c89 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -14,7 +14,7 @@ function updateIssueCount(cards) { function createNewColumn(url, columnTitle, projectColorInput, closeOnIssueMove) { $.ajax({ url, - data: JSON.stringify({title: columnTitle.val(), color: projectColorInput.val(),close: closeOnIssueMove.prop('checked')}), + data: JSON.stringify({title: columnTitle.val(), color: projectColorInput.val(), close: closeOnIssueMove.prop('checked')}), headers: { 'X-Csrf-Token': csrfToken, },