diff --git a/models/project/board.go b/models/project/board.go index 3e2d8e0472c51..e73258acc949f 100644 --- a/models/project/board.go +++ b/models/project/board.go @@ -55,6 +55,7 @@ type Board struct { 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)"` + LabelID int64 `xorm:"DEFAULT 0"` 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..875d8566b5215 100644 --- a/models/project/issue.go +++ b/models/project/issue.go @@ -22,6 +22,9 @@ type ProjectIssue struct { //revive:disable-line:exported // the sorting order on the board Sorting int64 `xorm:"NOT NULL DEFAULT 0"` + + // label that is added when a issue is moved to this column + LabelID int64 `xorm:"DEFAULT 0"` } func init() { diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 03798a712c2ee..822aff90774ee 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 ( @@ -545,6 +546,7 @@ func AddBoardToProjectPost(ctx *context.Context) { ProjectID: project.ID, Title: form.Title, Color: form.Color, + LabelID: form.LabelID, CreatorID: ctx.Doer.ID, }); err != nil { ctx.ServerError("NewProjectBoard", err) @@ -742,10 +744,27 @@ func MoveIssues(ctx *context.Context) { } } + fromColumnLabelID, err := strconv.ParseInt(ctx.FormString("fromColumnLabelID"), 10, 64) + if err != nil { + ctx.ServerError("fromColumnLableId is required", errors.New("fromColumnLableId is required")) + return + } + + issueID, err := strconv.ParseInt(ctx.FormString("issueID"), 10, 64) + if err != nil { + ctx.ServerError("moved issueID is required", errors.New("moved issueID is required")) + return + } + if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs); err != nil { ctx.ServerError("MoveIssuesOnProjectBoard", err) return } + if err = issue_service.AddAndOrRemoveLabelFromIssue(ctx, issueID, fromColumnLabelID, board); err != nil { + ctx.ServerError("failed adding/removing label in addAndOrRemoveLabel", err) + return + } + ctx.JSONOK() } diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go index dd3e2803b4e43..b4727b8a22f1a 100644 --- a/routers/web/repo/issue_label.go +++ b/routers/web/repo/issue_label.go @@ -99,6 +99,17 @@ func RetrieveLabels(ctx *context.Context) { ctx.Data["SortType"] = ctx.FormString("sort") } +// RetrieveLabelsOrg clone of RetrieveLabels but without repo +func RetrieveLabelsOrg(ctx *context.Context) { + orgLabels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Org.Organization.ID, ctx.FormString("sort"), db.ListOptions{}) + if err != nil { + ctx.ServerError("GetLabelsByOrgID", err) + return + } + + ctx.Data["Labels"] = orgLabels +} + // NewLabel create new label for repository func NewLabel(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateLabelForm) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 4908bb796d9dc..a905e138149d0 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 ( @@ -487,6 +489,7 @@ func AddBoardToProjectPost(ctx *context.Context) { Title: form.Title, Color: form.Color, CreatorID: ctx.Doer.ID, + LabelID: form.LabelID, }); err != nil { ctx.ServerError("NewProjectBoard", err) return @@ -691,10 +694,27 @@ func MoveIssues(ctx *context.Context) { } } + fromColumnLabelID, err := strconv.ParseInt(ctx.FormString("fromColumnLabelID"), 10, 64) + if err != nil { + ctx.ServerError("fromColumnLableId is required", errors.New("fromColumnLableId is required")) + return + } + + issueID, err := strconv.ParseInt(ctx.FormString("issueID"), 10, 64) + if err != nil { + ctx.ServerError("moved issueID is required", errors.New("moved issueID is required")) + return + } + if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs); err != nil { ctx.ServerError("MoveIssuesOnProjectBoard", err) return } + if err = issue_service.AddAndOrRemoveLabelFromIssue(ctx, issueID, fromColumnLabelID, board); err != nil { + ctx.ServerError("failed adding/removing label in addAndOrRemoveLabel", err) + return + } + ctx.JSONOK() } diff --git a/routers/web/web.go b/routers/web/web.go index ff0ce0c2586bb..faae91a6dfbab 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -983,7 +983,7 @@ func registerRoutes(m *web.Route) { m.Group("/projects", func() { m.Group("", func() { m.Get("", org.Projects) - m.Get("/{id}", org.ViewProject) + m.Get("/{id}", repo.RetrieveLabelsOrg, org.ViewProject) }, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead, true)) m.Group("", func() { //nolint:dupl m.Get("/new", org.RenderNewProject) @@ -1322,7 +1322,7 @@ func registerRoutes(m *web.Route) { m.Group("/projects", func() { m.Get("", repo.Projects) - m.Get("/{id}", repo.ViewProject) + m.Get("/{id}", repo.RetrieveLabels, repo.ViewProject) m.Group("", func() { //nolint:dupl m.Get("/new", repo.RenderNewProject) m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost) diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 845eccf817d3a..7abfb97b8f2f8 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -531,6 +531,7 @@ type EditProjectBoardForm struct { Title string `binding:"Required;MaxSize(100)"` Sorting int8 Color string `binding:"MaxSize(7)"` + LabelID int64 } // _____ .__.__ __ diff --git a/services/issue/project.go b/services/issue/project.go new file mode 100644 index 0000000000000..798ec7fa89905 --- /dev/null +++ b/services/issue/project.go @@ -0,0 +1,51 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issue + +import ( + "errors" + + issues_model "code.gitea.io/gitea/models/issues" + project_model "code.gitea.io/gitea/models/project" + "code.gitea.io/gitea/modules/context" +) + +// AddAndOrRemoveLabelFromIssue updates issue label according to board LabelID +func AddAndOrRemoveLabelFromIssue(ctx *context.Context, currentIssueID, fromColumnLabelID int64, board *project_model.Board) error { + issue, err := issues_model.GetIssueByID(ctx, currentIssueID) + if err != nil { + return errors.New("failed getting issue") + } + var addedLabel *issues_model.Label + if board.LabelID != 0 { + addedLabel, err = issues_model.GetLabelByID(ctx, board.LabelID) + if err != nil { + return errors.New("failed getting add label") + } + } + var removedLabel *issues_model.Label + if fromColumnLabelID != 0 { + + removedLabel, err = issues_model.GetLabelByID(ctx, fromColumnLabelID) + if err != nil { + return errors.New("failed getting remove label") + } + } + + // Delete old label from current issue + if fromColumnLabelID != 0 { + if err := RemoveLabel(ctx, issue, ctx.Doer, removedLabel); err != nil { + return err + } + } + + // Add New Label to current issue + if board.LabelID != 0 { + if err := AddLabel(ctx, issue, ctx.Doer, addedLabel); err != nil { + return err + } + } + + return nil +} diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index b3ad03c354988..1c4b8f7ed55af 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -1,67 +1,91 @@ {{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}} -
-
-

{{.Project.Title}}

- {{if $canWriteProject}} - + +
@@ -165,9 +189,10 @@
+ {{- $currentColumnLabelID := .LabelID }}
{{range (index $.IssuesMap .ID)}} -
+
{{template "repo/issue/card" (dict "Issue" . "Page" $)}}
{{end}} diff --git a/web_src/js/features/project-label.js b/web_src/js/features/project-label.js new file mode 100644 index 0000000000000..85a6ff3ff9865 --- /dev/null +++ b/web_src/js/features/project-label.js @@ -0,0 +1,16 @@ +export function initNewProjectFormLabel() { + if (!document.querySelectorAll('#new-project-column-item form input#new_project_column_project_label').length) return; + + const labels = document.querySelectorAll('.labels-for-project-creation'); + const selectedLabel = document.querySelector('input#new_project_column_project_label'); + const selectedLabelId = document.querySelector('input#new_project_column_project_label_id'); + + if (!labels || !selectedLabel || !selectedLabelId) return; + + for (const l of labels) { + l.addEventListener('click', () => { + selectedLabel.value = l.textContent; + selectedLabelId.value = l.dataset.labelId; + }); + } +} diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index 5a2a7e72ef335..7dee9341cfa3c 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, labelId) { $.ajax({ url, - data: JSON.stringify({title: columnTitle.val(), color: projectColorInput.val()}), + data: JSON.stringify({title: columnTitle.val(), color: projectColorInput.val(), labelId: parseInt(labelId.val())}), 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?fromColumnLabelID=${item.getAttribute('data-column-label-id')}&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 labelId = $('#new_project_column_project_label_id'); if (!columnTitle.val()) { return; } const url = $(this).data('url'); - createNewColumn(url, columnTitle, projectColorInput); + createNewColumn(url, columnTitle, projectColorInput, labelId); }); } diff --git a/web_src/js/index.js b/web_src/js/index.js index 4713618506b0c..6c038d7b1233c 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -85,6 +85,7 @@ import {initRepoIssueList} from './features/repo-issue-list.js'; import {initCommonIssueListQuickGoto} from './features/common-issue-list.js'; import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js'; import {initDirAuto} from './modules/dirauto.js'; +import {initNewProjectFormLabel} from './features/project-label.js'; // Init Gitea's Fomantic settings initGiteaFomantic(); @@ -184,4 +185,6 @@ onDomReady(() => { initRepoDiffView(); initPdfViewer(); initScopedAccessTokenCategories(); + + initNewProjectFormLabel(); });