mirror of
https://github.com/go-gitea/gitea.git
synced 2026-02-25 03:46:16 +01:00
Fix
This commit is contained in:
parent
f7b8f6ed99
commit
b191ded690
@ -46,6 +46,9 @@ type Column struct {
|
||||
Color string `xorm:"VARCHAR(7)"`
|
||||
|
||||
ProjectID int64 `xorm:"INDEX NOT NULL"`
|
||||
|
||||
Project *Project `xorm:"-"`
|
||||
|
||||
CreatorID int64 `xorm:"NOT NULL"`
|
||||
|
||||
NumIssues int64 `xorm:"-"`
|
||||
@ -59,6 +62,19 @@ func (Column) TableName() string {
|
||||
return "project_board" // TODO: the legacy table name should be project_column
|
||||
}
|
||||
|
||||
func (c *Column) LoadProject(ctx context.Context) error {
|
||||
if c.Project != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
project, err := GetProjectByID(ctx, c.ProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Project = project
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Column) GetIssues(ctx context.Context) ([]*ProjectIssue, error) {
|
||||
issues := make([]*ProjectIssue, 0, 5)
|
||||
if err := db.GetEngine(ctx).Where("project_id=?", c.ProjectID).
|
||||
@ -213,16 +229,16 @@ func GetColumn(ctx context.Context, columnID int64) (*Column, error) {
|
||||
return column, nil
|
||||
}
|
||||
|
||||
func GetColumnByProjectIDAndColumnName(ctx context.Context, projectID int64, columnName string) (*Column, error) {
|
||||
board := new(Column)
|
||||
has, err := db.GetEngine(ctx).Where("project_id=? AND title=?", projectID, columnName).Get(board)
|
||||
func GetColumnByProjectIDAndColumnID(ctx context.Context, projectID, columnID int64) (*Column, error) {
|
||||
column := new(Column)
|
||||
has, err := db.GetEngine(ctx).Where("project_id=? AND id=?", projectID, columnID).Get(column)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrProjectColumnNotExist{ProjectID: projectID, Name: columnName}
|
||||
return nil, ErrProjectColumnNotExist{ProjectID: projectID, ColumnID: columnID}
|
||||
}
|
||||
|
||||
return board, nil
|
||||
return column, nil
|
||||
}
|
||||
|
||||
// UpdateColumn updates a project column
|
||||
|
||||
@ -94,8 +94,8 @@ const (
|
||||
)
|
||||
|
||||
type WorkflowFilter struct {
|
||||
Type WorkflowFilterType
|
||||
Value string // e.g., "issue", "pull_request", etc. depends on the filter type definition
|
||||
Type WorkflowFilterType `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type WorkflowActionType string
|
||||
@ -108,8 +108,8 @@ const (
|
||||
)
|
||||
|
||||
type WorkflowAction struct {
|
||||
ActionType WorkflowActionType
|
||||
ActionValue string
|
||||
Type WorkflowActionType `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// WorkflowEventCapabilities defines what filters and actions are available for each event
|
||||
|
||||
@ -561,9 +561,9 @@ var globalVars = sync.OnceValue(func() *globalVarsStruct {
|
||||
emailRegexp: regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"),
|
||||
|
||||
systemUserNewFuncs: map[int64]func() *User{
|
||||
GhostUserID: NewGhostUser,
|
||||
ActionsUserID: NewActionsUser,
|
||||
WorkflowsUserID: NewWorkflowsUser,
|
||||
GhostUserID: NewGhostUser,
|
||||
ActionsUserID: NewActionsUser,
|
||||
ProjectWorkflowsUserID: NewProjectWorkflowsUser,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@ -36,8 +36,8 @@ func GetPossibleUserFromMap(userID int64, usererMaps map[int64]*User) *User {
|
||||
return NewGhostUser()
|
||||
case ActionsUserID:
|
||||
return NewActionsUser()
|
||||
case WorkflowsUserID:
|
||||
return NewWorkflowsUser()
|
||||
case ProjectWorkflowsUserID:
|
||||
return NewProjectWorkflowsUser()
|
||||
case 0:
|
||||
return nil
|
||||
default:
|
||||
|
||||
@ -66,34 +66,34 @@ func (u *User) IsGiteaActions() bool {
|
||||
}
|
||||
|
||||
const (
|
||||
WorkflowsUserID int64 = -3
|
||||
WorkflowsUserName = "gitea-workflows"
|
||||
WorkflowsUserEmail = "workflows@gitea.io"
|
||||
ProjectWorkflowsUserID int64 = -3
|
||||
ProjectWorkflowsUserName = "project-workflows"
|
||||
ProjectWorkflowsUserEmail = "workflows@gitea.io"
|
||||
)
|
||||
|
||||
func IsGiteaWorkflowsUserName(name string) bool {
|
||||
return strings.EqualFold(name, WorkflowsUserName)
|
||||
return strings.EqualFold(name, ProjectWorkflowsUserName)
|
||||
}
|
||||
|
||||
// NewWorkflowsUser creates and returns a fake user for running the workflows.
|
||||
func NewWorkflowsUser() *User {
|
||||
// NewProjectWorkflowsUser creates and returns a fake user for running the project workflows.
|
||||
func NewProjectWorkflowsUser() *User {
|
||||
return &User{
|
||||
ID: WorkflowsUserID,
|
||||
Name: WorkflowsUserName,
|
||||
LowerName: WorkflowsUserName,
|
||||
ID: ProjectWorkflowsUserID,
|
||||
Name: ProjectWorkflowsUserName,
|
||||
LowerName: ProjectWorkflowsUserName,
|
||||
IsActive: true,
|
||||
FullName: "Gitea Workflows",
|
||||
Email: WorkflowsUserEmail,
|
||||
FullName: "Project Workflows",
|
||||
Email: ProjectWorkflowsUserEmail,
|
||||
KeepEmailPrivate: true,
|
||||
LoginName: WorkflowsUserName,
|
||||
LoginName: ProjectWorkflowsUserName,
|
||||
Type: UserTypeBot,
|
||||
AllowCreateOrganization: true,
|
||||
Visibility: structs.VisibleTypePublic,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) IsGiteaWorkflows() bool {
|
||||
return u != nil && u.ID == WorkflowsUserID
|
||||
func (u *User) IsProjectWorkflows() bool {
|
||||
return u != nil && u.ID == ProjectWorkflowsUserID
|
||||
}
|
||||
|
||||
func GetSystemUserByName(name string) *User {
|
||||
@ -104,7 +104,7 @@ func GetSystemUserByName(name string) *User {
|
||||
return NewActionsUser()
|
||||
}
|
||||
if IsGiteaWorkflowsUserName(name) {
|
||||
return NewWorkflowsUser()
|
||||
return NewProjectWorkflowsUser()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -58,14 +58,6 @@ func convertFormToFilters(formFilters map[string]string) []project_model.Workflo
|
||||
return filters
|
||||
}
|
||||
|
||||
func convertFiltersToMap(filters []project_model.WorkflowFilter) map[string]string {
|
||||
filterMap := make(map[string]string)
|
||||
for _, filter := range filters {
|
||||
filterMap[string(filter.Type)] = filter.Value
|
||||
}
|
||||
return filterMap
|
||||
}
|
||||
|
||||
// convertFormToActions converts form actions to WorkflowAction objects
|
||||
func convertFormToActions(formActions map[string]any) []project_model.WorkflowAction {
|
||||
actions := make([]project_model.WorkflowAction, 0)
|
||||
@ -73,12 +65,12 @@ func convertFormToActions(formActions map[string]any) []project_model.WorkflowAc
|
||||
for key, value := range formActions {
|
||||
switch key {
|
||||
case "column":
|
||||
if floatValue, ok := value.(float64); ok {
|
||||
floatValueInt := int64(floatValue)
|
||||
if floatValue, ok := value.(string); ok {
|
||||
floatValueInt, _ := strconv.ParseInt(floatValue, 10, 64)
|
||||
if floatValueInt > 0 {
|
||||
actions = append(actions, project_model.WorkflowAction{
|
||||
ActionType: project_model.WorkflowActionTypeColumn,
|
||||
ActionValue: strconv.FormatInt(floatValueInt, 10),
|
||||
Type: project_model.WorkflowActionTypeColumn,
|
||||
Value: strconv.FormatInt(floatValueInt, 10),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -87,8 +79,8 @@ func convertFormToActions(formActions map[string]any) []project_model.WorkflowAc
|
||||
for _, label := range labels {
|
||||
if label != "" {
|
||||
actions = append(actions, project_model.WorkflowAction{
|
||||
ActionType: project_model.WorkflowActionTypeAddLabels,
|
||||
ActionValue: label,
|
||||
Type: project_model.WorkflowActionTypeAddLabels,
|
||||
Value: label,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -98,8 +90,8 @@ func convertFormToActions(formActions map[string]any) []project_model.WorkflowAc
|
||||
for _, label := range labels {
|
||||
if label != "" {
|
||||
actions = append(actions, project_model.WorkflowAction{
|
||||
ActionType: project_model.WorkflowActionTypeRemoveLabels,
|
||||
ActionValue: label,
|
||||
Type: project_model.WorkflowActionTypeRemoveLabels,
|
||||
Value: label,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -107,8 +99,8 @@ func convertFormToActions(formActions map[string]any) []project_model.WorkflowAc
|
||||
case "closeIssue":
|
||||
if boolValue, ok := value.(bool); ok && boolValue {
|
||||
actions = append(actions, project_model.WorkflowAction{
|
||||
ActionType: project_model.WorkflowActionTypeClose,
|
||||
ActionValue: "true",
|
||||
Type: project_model.WorkflowActionTypeClose,
|
||||
Value: "true",
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -117,14 +109,6 @@ func convertFormToActions(formActions map[string]any) []project_model.WorkflowAc
|
||||
return actions
|
||||
}
|
||||
|
||||
func convertActionsToMap(actions []project_model.WorkflowAction) map[string]any {
|
||||
actionMap := make(map[string]any)
|
||||
for _, action := range actions {
|
||||
actionMap[string(action.ActionType)] = action.ActionValue
|
||||
}
|
||||
return actionMap
|
||||
}
|
||||
|
||||
func WorkflowsEvents(ctx *context.Context) {
|
||||
projectID := ctx.PathParamInt64("id")
|
||||
p, err := project_model.GetProjectByID(ctx, projectID)
|
||||
@ -156,8 +140,8 @@ func WorkflowsEvents(ctx *context.Context) {
|
||||
EventID string `json:"event_id"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Capabilities project_model.WorkflowEventCapabilities `json:"capabilities"`
|
||||
Filters map[string]string `json:"filters"`
|
||||
Actions map[string]any `json:"actions"`
|
||||
Filters []project_model.WorkflowFilter `json:"filters"`
|
||||
Actions []project_model.WorkflowAction `json:"actions"`
|
||||
FilterSummary string `json:"filter_summary"` // Human readable filter description
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
@ -183,8 +167,8 @@ func WorkflowsEvents(ctx *context.Context) {
|
||||
EventID: strconv.FormatInt(wf.ID, 10),
|
||||
DisplayName: string(ctx.Tr(wf.WorkflowEvent.LangKey())) + filterSummary,
|
||||
Capabilities: capabilities[event],
|
||||
Filters: convertFiltersToMap(wf.WorkflowFilters),
|
||||
Actions: convertActionsToMap(wf.WorkflowActions),
|
||||
Filters: wf.WorkflowFilters,
|
||||
Actions: wf.WorkflowActions,
|
||||
FilterSummary: filterSummary,
|
||||
Enabled: wf.Enabled,
|
||||
})
|
||||
|
||||
@ -27,6 +27,7 @@ import (
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
issues_servie "code.gitea.io/gitea/services/issue"
|
||||
project_service "code.gitea.io/gitea/services/projects"
|
||||
)
|
||||
|
||||
@ -446,7 +447,7 @@ func UpdateIssueProject(ctx *context.Context) {
|
||||
if issue.Project != nil && issue.Project.ID == projectID {
|
||||
continue
|
||||
}
|
||||
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectID, 0); err != nil {
|
||||
if err := issues_servie.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectID, 0); err != nil {
|
||||
if errors.Is(err, util.ErrPermissionDenied) {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -1421,6 +1421,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// FIXME: this should be moved in the function NewPullRequest
|
||||
if projectID > 0 && ctx.Repo.CanWrite(unit.TypeProjects) {
|
||||
if err := issues_model.IssueAssignOrRemoveProject(ctx, pullIssue, ctx.Doer, projectID, 0); err != nil {
|
||||
if !errors.Is(err, util.ErrPermissionDenied) {
|
||||
|
||||
31
services/issue/project.go
Normal file
31
services/issue/project.go
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package issue
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/services/notify"
|
||||
)
|
||||
|
||||
func IssueAssignOrRemoveProject(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, projectID int64, position int) error {
|
||||
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, doer, projectID, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var newProject *project_model.Project
|
||||
var err error
|
||||
if projectID > 0 {
|
||||
newProject, err = project_model.GetProjectByID(ctx, projectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
notify.IssueChangeProjects(ctx, doer, issue, newProject)
|
||||
return nil
|
||||
}
|
||||
@ -10,6 +10,7 @@ import (
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
@ -41,6 +42,7 @@ type Notifier interface {
|
||||
IssueChangeRef(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldRef string)
|
||||
IssueChangeLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue,
|
||||
addedLabels, removedLabels []*issues_model.Label)
|
||||
IssueChangeProjects(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, newProject *project_model.Project)
|
||||
|
||||
NewPullRequest(ctx context.Context, pr *issues_model.PullRequest, mentions []*user_model.User)
|
||||
MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest)
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
@ -274,6 +275,13 @@ func IssueChangeLabels(ctx context.Context, doer *user_model.User, issue *issues
|
||||
}
|
||||
}
|
||||
|
||||
// IssueChangeProjects notifies change projects to notifiers
|
||||
func IssueChangeProjects(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, newProject *project_model.Project) {
|
||||
for _, notifier := range notifiers {
|
||||
notifier.IssueChangeProjects(ctx, doer, issue, newProject)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateRepository notifies create repository to notifiers
|
||||
func CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
|
||||
for _, notifier := range notifiers {
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
@ -143,6 +144,9 @@ func (*NullNotifier) IssueChangeLabels(ctx context.Context, doer *user_model.Use
|
||||
addedLabels, removedLabels []*issues_model.Label) {
|
||||
}
|
||||
|
||||
func (*NullNotifier) IssueChangeProjects(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, newProject *project_model.Project) {
|
||||
}
|
||||
|
||||
// CreateRepository places a place holder function
|
||||
func (*NullNotifier) CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
|
||||
}
|
||||
|
||||
@ -205,3 +205,30 @@ func LoadIssueNumbersForProject(ctx context.Context, project *project_model.Proj
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func MoveIssueToAnotherColumn(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, column *project_model.Column) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if err := project_model.MoveIssueToAnotherColumn(ctx, issue.ID, column); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := column.LoadProject(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add timeline to issue
|
||||
if _, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
|
||||
Type: issues_model.CommentTypeProjectColumn,
|
||||
Doer: doer,
|
||||
Repo: issue.Repo,
|
||||
Issue: issue,
|
||||
ProjectID: column.ProjectID,
|
||||
ProjectTitle: column.Project.Title,
|
||||
ProjectColumnID: column.ID,
|
||||
ProjectColumnTitle: column.Title,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ package projects
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
@ -52,14 +53,23 @@ func (m *workflowNotifier) NewIssue(ctx context.Context, issue *issues_model.Iss
|
||||
return
|
||||
}
|
||||
|
||||
// Find workflows for the ItemAddedToProject event
|
||||
// Find workflows for the ItemOpened event
|
||||
for _, workflow := range workflows {
|
||||
if workflow.WorkflowEvent == project_model.WorkflowEventItemAddedToProject {
|
||||
if workflow.WorkflowEvent == project_model.WorkflowEventItemOpened {
|
||||
fireIssueWorkflow(ctx, workflow, issue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *workflowNotifier) NewPullRequest(ctx context.Context, pr *issues_model.PullRequest, mentions []*user_model.User) {
|
||||
if err := pr.LoadIssue(ctx); err != nil {
|
||||
log.Error("NewIssue: LoadIssue: %v", err)
|
||||
return
|
||||
}
|
||||
issue := pr.Issue
|
||||
m.NewIssue(ctx, issue, mentions)
|
||||
}
|
||||
|
||||
func (m *workflowNotifier) IssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, actionComment *issues_model.Comment, isClosed bool) {
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
log.Error("IssueChangeStatus: LoadRepo: %v", err)
|
||||
@ -88,6 +98,111 @@ func (m *workflowNotifier) IssueChangeStatus(ctx context.Context, doer *user_mod
|
||||
}
|
||||
}
|
||||
|
||||
func (*workflowNotifier) IssueChangeProjects(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, newProject *project_model.Project) {
|
||||
if newProject == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
log.Error("IssueChangeStatus: LoadRepo: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := issue.LoadProject(ctx); err != nil {
|
||||
log.Error("NewIssue: LoadProject: %v", err)
|
||||
return
|
||||
}
|
||||
if issue.Project == nil || issue.Project.ID != newProject.ID {
|
||||
return
|
||||
}
|
||||
|
||||
workflows, err := project_model.FindWorkflowsByProjectID(ctx, issue.Project.ID)
|
||||
if err != nil {
|
||||
log.Error("IssueChangeStatus: FindWorkflowsByProjectID: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Find workflows for the ItemOpened event
|
||||
for _, workflow := range workflows {
|
||||
if workflow.WorkflowEvent == project_model.WorkflowEventItemAddedToProject {
|
||||
fireIssueWorkflow(ctx, workflow, issue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (*workflowNotifier) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
|
||||
if err := pr.LoadIssue(ctx); err != nil {
|
||||
log.Error("NewIssue: LoadIssue: %v", err)
|
||||
return
|
||||
}
|
||||
issue := pr.Issue
|
||||
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
log.Error("IssueChangeStatus: LoadRepo: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := issue.LoadProject(ctx); err != nil {
|
||||
log.Error("NewIssue: LoadProject: %v", err)
|
||||
return
|
||||
}
|
||||
if issue.Project == nil {
|
||||
return
|
||||
}
|
||||
|
||||
workflows, err := project_model.FindWorkflowsByProjectID(ctx, issue.Project.ID)
|
||||
if err != nil {
|
||||
log.Error("IssueChangeStatus: FindWorkflowsByProjectID: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Find workflows for the PullRequestMerged event
|
||||
for _, workflow := range workflows {
|
||||
if workflow.WorkflowEvent == project_model.WorkflowEventPullRequestMerged {
|
||||
fireIssueWorkflow(ctx, workflow, issue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *workflowNotifier) AutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
|
||||
m.MergePullRequest(ctx, doer, pr)
|
||||
}
|
||||
|
||||
func (*workflowNotifier) PullRequestReview(ctx context.Context, pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) {
|
||||
if err := pr.LoadIssue(ctx); err != nil {
|
||||
log.Error("NewIssue: LoadIssue: %v", err)
|
||||
return
|
||||
}
|
||||
issue := pr.Issue
|
||||
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
log.Error("IssueChangeStatus: LoadRepo: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := issue.LoadProject(ctx); err != nil {
|
||||
log.Error("NewIssue: LoadProject: %v", err)
|
||||
return
|
||||
}
|
||||
if issue.Project == nil {
|
||||
return
|
||||
}
|
||||
|
||||
workflows, err := project_model.FindWorkflowsByProjectID(ctx, issue.Project.ID)
|
||||
if err != nil {
|
||||
log.Error("IssueChangeStatus: FindWorkflowsByProjectID: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Find workflows for the PullRequestMerged event
|
||||
for _, workflow := range workflows {
|
||||
if (workflow.WorkflowEvent == project_model.WorkflowEventCodeChangesRequested && review.Type == issues_model.ReviewTypeReject) ||
|
||||
(workflow.WorkflowEvent == project_model.WorkflowEventCodeReviewApproved && review.Type == issues_model.ReviewTypeApprove) {
|
||||
fireIssueWorkflow(ctx, workflow, issue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fireIssueWorkflow(ctx context.Context, workflow *project_model.Workflow, issue *issues_model.Issue) {
|
||||
for _, filter := range workflow.WorkflowFilters {
|
||||
switch filter.Type {
|
||||
@ -103,26 +218,33 @@ func fireIssueWorkflow(ctx context.Context, workflow *project_model.Workflow, is
|
||||
}
|
||||
|
||||
for _, action := range workflow.WorkflowActions {
|
||||
switch action.ActionType {
|
||||
switch action.Type {
|
||||
case project_model.WorkflowActionTypeColumn:
|
||||
column, err := project_model.GetColumnByProjectIDAndColumnName(ctx, issue.Project.ID, action.ActionValue)
|
||||
if err != nil {
|
||||
log.Error("GetColumnByProjectIDAndColumnName: %v", err)
|
||||
columnID, _ := strconv.ParseInt(action.Value, 10, 64)
|
||||
if columnID == 0 {
|
||||
log.Error("Invalid column ID: %s", action.Value)
|
||||
continue
|
||||
}
|
||||
if err := project_model.AddIssueToColumn(ctx, issue.ID, column); err != nil {
|
||||
log.Error("AddIssueToColumn: %v", err)
|
||||
column, err := project_model.GetColumnByProjectIDAndColumnID(ctx, issue.Project.ID, columnID)
|
||||
if err != nil {
|
||||
log.Error("GetColumnByProjectIDAndColumnID: %v", err)
|
||||
continue
|
||||
}
|
||||
if err := MoveIssueToAnotherColumn(ctx, user_model.NewProjectWorkflowsUser(), issue, column); err != nil {
|
||||
log.Error("MoveIssueToAnotherColumn: %v", err)
|
||||
continue
|
||||
}
|
||||
case project_model.WorkflowActionTypeAddLabels:
|
||||
// TODO: implement adding labels
|
||||
case project_model.WorkflowActionTypeRemoveLabels:
|
||||
// TODO: implement removing labels
|
||||
case project_model.WorkflowActionTypeClose:
|
||||
if err := issue_service.CloseIssue(ctx, issue, user_model.NewWorkflowsUser(), ""); err != nil {
|
||||
if err := issue_service.CloseIssue(ctx, issue, user_model.NewProjectWorkflowsUser(), ""); err != nil {
|
||||
log.Error("CloseIssue: %v", err)
|
||||
continue
|
||||
}
|
||||
default:
|
||||
log.Error("Unsupported action type: %s", action.ActionType)
|
||||
log.Error("Unsupported action type: %s", action.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -373,36 +373,6 @@ const isItemSelected = (item) => {
|
||||
return store.selectedItem === item.base_event_type;
|
||||
};
|
||||
|
||||
const _getActionsSummary = (workflow) => {
|
||||
if (!workflow.actions || workflow.actions.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const actions = [];
|
||||
for (const action of workflow.actions) {
|
||||
if (action.action_type === 'column') {
|
||||
const column = store.projectColumns.find((c) => c.id === action.action_value);
|
||||
if (column) {
|
||||
actions.push(`Move to "${column.title}"`);
|
||||
}
|
||||
} else if (action.action_type === 'add_labels') {
|
||||
const label = store.projectLabels.find((l) => l.id === action.action_value);
|
||||
if (label) {
|
||||
actions.push(`Add label "${label.name}"`);
|
||||
}
|
||||
} else if (action.action_type === 'remove_labels') {
|
||||
const label = store.projectLabels.find((l) => l.id === action.action_value);
|
||||
if (label) {
|
||||
actions.push(`Remove label "${label.name}"`);
|
||||
}
|
||||
} else if (action.action_type === 'close') {
|
||||
actions.push('Close issue');
|
||||
}
|
||||
}
|
||||
|
||||
return actions.join(', ');
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
// Load all necessary data
|
||||
store.workflowEvents = await store.loadEvents();
|
||||
@ -626,113 +596,111 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--<form class="ui form form-fetch-action" :action="props.projectLink+'/workflows/'+store.selectedWorkflow.id" method="post">-->
|
||||
<div class="editor-content">
|
||||
<div class="form" :class="{ 'readonly': !isInEditMode }">
|
||||
<div class="field">
|
||||
<label>When</label>
|
||||
<div class="segment">
|
||||
<div class="description">
|
||||
This workflow will run when: <strong>{{ store.selectedWorkflow.display_name }}</strong>
|
||||
<div class="editor-content">
|
||||
<div class="form" :class="{ 'readonly': !isInEditMode }">
|
||||
<div class="field">
|
||||
<label>When</label>
|
||||
<div class="segment">
|
||||
<div class="description">
|
||||
This workflow will run when: <strong>{{ store.selectedWorkflow.display_name }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="field" v-if="hasAvailableFilters">
|
||||
<label>Filters</label>
|
||||
<div class="segment">
|
||||
<div class="field" v-if="hasFilter('issue_type')">
|
||||
<label>Apply to</label>
|
||||
<select
|
||||
v-if="isInEditMode"
|
||||
class="form-select"
|
||||
v-model="store.workflowFilters.issue_type"
|
||||
>
|
||||
<option value="">Issues And Pull Requests</option>
|
||||
<option value="issue">Issues</option>
|
||||
<option value="pull_request">Pull requests</option>
|
||||
</select>
|
||||
<div v-else class="readonly-value">
|
||||
{{ store.workflowFilters.issue_type === 'issue' ? 'Issues' :
|
||||
store.workflowFilters.issue_type === 'pull_request' ? 'Pull requests' :
|
||||
'Issues And Pull Requests' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="field" v-if="hasAvailableFilters">
|
||||
<label>Filters</label>
|
||||
<div class="segment">
|
||||
<div class="field" v-if="hasFilter('issue_type')">
|
||||
<label>Apply to</label>
|
||||
<select
|
||||
v-if="isInEditMode"
|
||||
class="form-select"
|
||||
v-model="store.workflowFilters.issue_type"
|
||||
>
|
||||
<option value="">Issues And Pull Requests</option>
|
||||
<option value="issue">Issues</option>
|
||||
<option value="pull_request">Pull requests</option>
|
||||
</select>
|
||||
<div v-else class="readonly-value">
|
||||
{{ store.workflowFilters.issue_type === 'issue' ? 'Issues' :
|
||||
store.workflowFilters.issue_type === 'pull_request' ? 'Pull requests' :
|
||||
'Issues And Pull Requests' }}
|
||||
</div>
|
||||
<!-- Actions Section -->
|
||||
<div class="field">
|
||||
<label>Actions</label>
|
||||
<div class="segment">
|
||||
<div class="field" v-if="hasAction('column')">
|
||||
<label>Move to column</label>
|
||||
<select
|
||||
v-if="isInEditMode"
|
||||
class="form-select"
|
||||
v-model="store.workflowActions.column"
|
||||
>
|
||||
<option value="">Select column...</option>
|
||||
<option v-for="column in store.projectColumns" :key="column.id" :value="String(column.id)">
|
||||
{{ column.title }}
|
||||
</option>
|
||||
</select>
|
||||
<div v-else class="readonly-value">
|
||||
{{ store.projectColumns.find(c => String(c.id) === store.workflowActions.column)?.title || 'None' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions Section -->
|
||||
<div class="field">
|
||||
<label>Actions</label>
|
||||
<div class="segment">
|
||||
<div class="field" v-if="hasAction('column')">
|
||||
<label>Move to column</label>
|
||||
<select
|
||||
v-if="isInEditMode"
|
||||
class="form-select"
|
||||
v-model="store.workflowActions.column"
|
||||
>
|
||||
<option value="">Select column...</option>
|
||||
<option v-for="column in store.projectColumns" :key="column.id" :value="column.id">
|
||||
{{ column.title }}
|
||||
</option>
|
||||
</select>
|
||||
<div v-else class="readonly-value">
|
||||
{{ store.projectColumns.find(c => c.id === store.workflowActions.column)?.title || 'None' }}
|
||||
</div>
|
||||
<div class="field" v-if="hasAction('label')">
|
||||
<label>Add labels</label>
|
||||
<select
|
||||
v-if="isInEditMode"
|
||||
class="form-select"
|
||||
v-model="store.workflowActions.add_labels"
|
||||
multiple
|
||||
>
|
||||
<option value="">Select labels...</option>
|
||||
<option v-for="label in store.projectLabels" :key="label.id" :value="String(label.id)">
|
||||
{{ label.name }}
|
||||
</option>
|
||||
</select>
|
||||
<div v-else class="readonly-value">
|
||||
{{ store.workflowActions.add_labels?.map(id =>
|
||||
store.projectLabels.find(l => String(l.id) === id)?.name).join(', ') || 'None' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field" v-if="hasAction('label')">
|
||||
<label>Add labels</label>
|
||||
<select
|
||||
v-if="isInEditMode"
|
||||
class="form-select"
|
||||
v-model="store.workflowActions.add_labels"
|
||||
multiple
|
||||
>
|
||||
<option value="">Select labels...</option>
|
||||
<option v-for="label in store.projectLabels" :key="label.id" :value="label.id">
|
||||
{{ label.name }}
|
||||
</option>
|
||||
</select>
|
||||
<div v-else class="readonly-value">
|
||||
{{ store.workflowActions.add_labels?.map(id =>
|
||||
store.projectLabels.find(l => l.id === id)?.name).join(', ') || 'None' }}
|
||||
</div>
|
||||
<div class="field" v-if="hasAction('close')">
|
||||
<div v-if="isInEditMode" class="form-check">
|
||||
<input type="checkbox" v-model="store.workflowActions.closeIssue" id="close-issue">
|
||||
<label for="close-issue">Close issue</label>
|
||||
</div>
|
||||
|
||||
<div class="field" v-if="hasAction('close')">
|
||||
<div v-if="isInEditMode" class="form-check">
|
||||
<input type="checkbox" v-model="store.workflowActions.closeIssue" id="close-issue">
|
||||
<label for="close-issue">Close issue</label>
|
||||
</div>
|
||||
<div v-else class="readonly-value">
|
||||
<label>Close issue</label>
|
||||
<div>{{ store.workflowActions.closeIssue ? 'Yes' : 'No' }}</div>
|
||||
</div>
|
||||
<div v-else class="readonly-value">
|
||||
<label>Close issue</label>
|
||||
<div>{{ store.workflowActions.closeIssue ? 'Yes' : 'No' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fixed bottom actions (only show in edit mode) -->
|
||||
<div v-if="isInEditMode" class="editor-actions">
|
||||
<button class="btn btn-primary" @click="saveWorkflow" :disabled="store.saving">
|
||||
<i class="save icon"/>
|
||||
Save Workflow
|
||||
</button>
|
||||
<button
|
||||
v-if="store.selectedWorkflow && store.selectedWorkflow.id > 0"
|
||||
class="btn btn-danger"
|
||||
@click="deleteWorkflow"
|
||||
>
|
||||
<i class="trash icon"/>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
<!--</form>-->
|
||||
<!-- Fixed bottom actions (only show in edit mode) -->
|
||||
<div v-if="isInEditMode" class="editor-actions">
|
||||
<button class="btn btn-primary" @click="saveWorkflow" :disabled="store.saving">
|
||||
<i class="save icon"/>
|
||||
Save Workflow
|
||||
</button>
|
||||
<button
|
||||
v-if="store.selectedWorkflow && store.selectedWorkflow.id > 0"
|
||||
class="btn btn-danger"
|
||||
@click="deleteWorkflow"
|
||||
>
|
||||
<i class="trash icon"/>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -33,6 +33,10 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
|
||||
try {
|
||||
const response = await GET(`${props.projectLink}/workflows/columns`);
|
||||
store.projectColumns = await response.json();
|
||||
console.log('[WorkflowStore] Loaded columns:', store.projectColumns);
|
||||
if (store.projectColumns.length > 0) {
|
||||
console.log('[WorkflowStore] First column.id type:', typeof store.projectColumns[0].id, 'value:', store.projectColumns[0].id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load project columns:', error);
|
||||
store.projectColumns = [];
|
||||
@ -48,38 +52,37 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
|
||||
|
||||
// Find the workflow from existing workflowEvents
|
||||
const workflow = store.workflowEvents.find((e) => e.event_id === eventId);
|
||||
if (workflow && workflow.filters && workflow.actions) {
|
||||
console.log('[WorkflowStore] loadWorkflowData - eventId:', eventId);
|
||||
console.log('[WorkflowStore] loadWorkflowData - found workflow:', workflow);
|
||||
|
||||
// Load existing configuration from the workflow data
|
||||
// Convert backend filter format to frontend format
|
||||
const frontendFilters = {issue_type: ''};
|
||||
if (workflow.filters && Array.isArray(workflow.filters)) {
|
||||
for (const filter of workflow.filters) {
|
||||
if (filter.type === 'issue_type') {
|
||||
frontendFilters.issue_type = filter.value;
|
||||
}
|
||||
const frontendFilters = {issue_type: ''};
|
||||
// Convert backend action format to frontend format
|
||||
const frontendActions = {column: '', add_labels: [], closeIssue: false};
|
||||
|
||||
if (workflow) {
|
||||
for (const filter of workflow.filters) {
|
||||
if (filter.type === 'issue_type') {
|
||||
frontendFilters.issue_type = filter.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert backend action format to frontend format
|
||||
const frontendActions = {column: '', add_labels: [], closeIssue: false};
|
||||
if (workflow.actions && Array.isArray(workflow.actions)) {
|
||||
for (const action of workflow.actions) {
|
||||
if (action.action_type === 'column') {
|
||||
frontendActions.column = action.action_value;
|
||||
} else if (action.action_type === 'add_labels') {
|
||||
frontendActions.add_labels.push(action.action_value);
|
||||
} else if (action.action_type === 'close') {
|
||||
frontendActions.closeIssue = action.action_value === 'true';
|
||||
}
|
||||
for (const action of workflow.actions) {
|
||||
if (action.type === 'column') {
|
||||
// Backend returns string, keep as string to match column.id type
|
||||
frontendActions.column = action.value;
|
||||
} else if (action.type === 'add_labels') {
|
||||
// Backend returns string, keep as string to match label.id type
|
||||
frontendActions.add_labels.push(action.value);
|
||||
} else if (action.type === 'close') {
|
||||
frontendActions.closeIssue = action.value === 'true';
|
||||
}
|
||||
}
|
||||
|
||||
store.workflowFilters = frontendFilters;
|
||||
store.workflowActions = frontendActions;
|
||||
} else {
|
||||
// Reset to defaults for new workflow
|
||||
store.resetWorkflowData();
|
||||
}
|
||||
|
||||
store.workflowFilters = frontendFilters;
|
||||
store.workflowActions = frontendActions;
|
||||
} finally {
|
||||
store.loading = false;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user